Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-python-binary-memcached for 
openSUSE:Factory checked in at 2026-05-04 12:54:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-python-binary-memcached (Old)
 and      /work/SRC/openSUSE:Factory/.python-python-binary-memcached.new.30200 
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-python-binary-memcached"

Mon May  4 12:54:46 2026 rev:5 rq:1350587 version:0.32.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-python-binary-memcached/python-python-binary-memcached.changes
    2025-07-06 17:18:45.181870797 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-python-binary-memcached.new.30200/python-python-binary-memcached.changes
 2026-05-04 12:58:25.293590663 +0200
@@ -1,0 +2,6 @@
+Sun May  3 20:54:42 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.32.0:
+  * Add repository contribution guidelines.
+
+-------------------------------------------------------------------

Old:
----
  python_binary_memcached-0.31.4.tar.gz

New:
----
  python_binary_memcached-0.32.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-python-binary-memcached.spec ++++++
--- /var/tmp/diff_new_pack.g8qmgq/_old  2026-05-04 12:58:25.777610582 +0200
+++ /var/tmp/diff_new_pack.g8qmgq/_new  2026-05-04 12:58:25.777610582 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-python-binary-memcached
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python-python-binary-memcached
-Version:        0.31.4
+Version:        0.32.0
 Release:        0
 Summary:        Access memcached via its binary protocol with SASL auth support
 License:        MIT

++++++ python_binary_memcached-0.31.4.tar.gz -> 
python_binary_memcached-0.32.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python_binary_memcached-0.31.4/CHANGELOG.md 
new/python_binary_memcached-0.32.0/CHANGELOG.md
--- old/python_binary_memcached-0.31.4/CHANGELOG.md     2025-01-08 
22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/CHANGELOG.md     2026-04-28 
21:06:49.000000000 +0200
@@ -1,3 +1,9 @@
+## v0.32.0 (2026-04-28)
+
+### Docs
+
+- Add repository contribution guidelines.
+
 ## v0.31.2 (2022-12-14)
 
 ### Fix
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python_binary_memcached-0.31.4/PKG-INFO 
new/python_binary_memcached-0.32.0/PKG-INFO
--- old/python_binary_memcached-0.31.4/PKG-INFO 2025-01-08 22:18:11.481290800 
+0100
+++ new/python_binary_memcached-0.32.0/PKG-INFO 2026-04-28 21:07:17.580670000 
+0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.2
+Metadata-Version: 2.4
 Name: python-binary-memcached
-Version: 0.31.4
+Version: 0.32.0
 Summary: A pure python module to access memcached via its binary protocol with 
SASL auth support
 Home-page: https://github.com/jaysonsantos/python-binary-memcached
 Author: Jayson Reis
@@ -8,13 +8,11 @@
 Classifier: Development Status :: 4 - Beta
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 License-File: LICENSE
 Requires-Dist: six
 Requires-Dist: uhashring
@@ -23,6 +21,7 @@
 Dynamic: classifier
 Dynamic: description
 Dynamic: home-page
+Dynamic: license-file
 Dynamic: requires-dist
 Dynamic: summary
 
@@ -111,6 +110,14 @@
 .. _`pytest`: https://pypi.org/project/pytest/
 .. _`tox`: https://pypi.org/project/tox/
 
+v0.32.0 (2026-04-28)
+====================
+
+Docs
+----
+
+-  Add repository contribution guidelines.
+
 v0.31.2 (2022-12-14)
 ====================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python_binary_memcached-0.31.4/README.rst 
new/python_binary_memcached-0.32.0/README.rst
--- old/python_binary_memcached-0.31.4/README.rst       2025-01-08 
22:18:07.000000000 +0100
+++ new/python_binary_memcached-0.32.0/README.rst       2026-04-28 
21:07:07.000000000 +0200
@@ -83,6 +83,14 @@
 .. _`pytest`: https://pypi.org/project/pytest/
 .. _`tox`: https://pypi.org/project/tox/
 
+v0.32.0 (2026-04-28)
+====================
+
+Docs
+----
+
+-  Add repository contribution guidelines.
+
 v0.31.2 (2022-12-14)
 ====================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/bmemcached/client/distributed.py 
new/python_binary_memcached-0.32.0/bmemcached/client/distributed.py
--- old/python_binary_memcached-0.31.4/bmemcached/client/distributed.py 
2025-01-08 22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/bmemcached/client/distributed.py 
2026-04-28 21:06:49.000000000 +0200
@@ -39,7 +39,7 @@
             servers[server_key].append(key)
         return all([server.delete_multi(keys_) for server, keys_ in 
servers.items()])
 
-    def set(self, key, value, time=0, compress_level=-1):
+    def set(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Set a value for a key on server.
 
@@ -53,11 +53,15 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True in case of success and False in case of failure, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
         """
         server = self._get_server(key)
-        return server.set(key, value, time, compress_level)
+        return server.set(key, value, time, compress_level, get_cas=get_cas)
 
     def set_multi(self, mappings, time=0, compress_level=-1):
         """
@@ -86,7 +90,37 @@
 
         return list(returns)
 
-    def add(self, key, value, time=0, compress_level=-1):
+    def set_multi_cas(self, mappings, time=0, compress_level=-1):
+        """
+        Set multiple keys with their values on server, returning the new CAS
+        value for each successfully stored key.
+
+        :param mappings: A dict with keys/values. Keys may be (key, cas)
+            tuples as in set_multi.
+        :type mappings: dict
+        :param time: Time in seconds that your key will expire.
+        :type time: int
+        :param compress_level: How much to compress.
+            0 = no compression, 1 = fastest, 9 = slowest but best,
+            -1 = default compression level.
+        :type compress_level: int
+        :return: A dict keyed by the string key of every input mapping. The
+            value is the new CAS int on success or None on failure.
+        :rtype: dict
+        """
+        if not mappings:
+            return {}
+        result = {}
+        server_mappings = defaultdict(dict)
+        for key, value in mappings.items():
+            str_key = key[0] if isinstance(key, tuple) else key
+            server_key = self._get_server(str_key)
+            server_mappings[server_key][key] = value
+        for server, m in server_mappings.items():
+            result.update(server.set_multi_cas(m, time, compress_level))
+        return result
+
+    def add(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Add a key/value to server ony if it does not exist.
 
@@ -100,13 +134,17 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is added False if key already exists
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True if key is added False if key already exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
         """
         server = self._get_server(key)
-        return server.add(key, value, time, compress_level)
+        return server.add(key, value, time, compress_level, get_cas=get_cas)
 
-    def replace(self, key, value, time=0, compress_level=-1):
+    def replace(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Replace a key/value to server ony if it does exist.
 
@@ -120,11 +158,15 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is replace False if key does not exists
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True if key is replace False if key does not exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
         """
         server = self._get_server(key)
-        return server.replace(key, value, time, compress_level)
+        return server.replace(key, value, time, compress_level, 
get_cas=get_cas)
 
     def get(self, key, default=None, get_cas=False):
         """
@@ -182,7 +224,7 @@
         server = self._get_server(key)
         return server.get(key)
 
-    def cas(self, key, value, cas, time=0, compress_level=-1):
+    def cas(self, key, value, cas, time=0, compress_level=-1, get_cas=False):
         """
         Set a value for a key on server if its CAS value matches cas.
 
@@ -198,11 +240,15 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
+        :param get_cas: If true, return (success, new_cas) where new_cas is
+            the item's new CAS after the operation, or None on failure.
+        :type get_cas: bool
+        :return: True in case of success and False in case of failure, or a
+            (success, new_cas) tuple if get_cas=True.
+        :rtype: bool or tuple
         """
         server = self._get_server(key)
-        return server.cas(key, value, cas, time, compress_level)
+        return server.cas(key, value, cas, time, compress_level, 
get_cas=get_cas)
 
     def incr(self, key, value, default=0, time=1000000):
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/bmemcached/client/mixin.py 
new/python_binary_memcached-0.32.0/bmemcached/client/mixin.py
--- old/python_binary_memcached-0.31.4/bmemcached/client/mixin.py       
2025-01-08 22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/bmemcached/client/mixin.py       
2026-04-28 21:06:49.000000000 +0200
@@ -132,19 +132,22 @@
     def get_multi(self, keys, get_cas=False):
         raise NotImplementedError()
 
-    def set(self, key, value, time=0, compress_level=-1):
+    def set(self, key, value, time=0, compress_level=-1, get_cas=False):
         raise NotImplementedError()
 
-    def cas(self, key, value, cas, time=0, compress_level=-1):
+    def cas(self, key, value, cas, time=0, compress_level=-1, get_cas=False):
         raise NotImplementedError()
 
     def set_multi(self, mappings, time=0, compress_level=-1):
         raise NotImplementedError()
 
-    def add(self, key, value, time=0, compress_level=-1):
+    def set_multi_cas(self, mappings, time=0, compress_level=-1):
         raise NotImplementedError()
 
-    def replace(self, key, value, time=0, compress_level=-1):
+    def add(self, key, value, time=0, compress_level=-1, get_cas=False):
+        raise NotImplementedError()
+
+    def replace(self, key, value, time=0, compress_level=-1, get_cas=False):
         raise NotImplementedError()
 
     def delete(self, key, cas=0):  # type: (six.string_types, int) -> bool
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/bmemcached/client/replicating.py 
new/python_binary_memcached-0.32.0/bmemcached/client/replicating.py
--- old/python_binary_memcached-0.31.4/bmemcached/client/replicating.py 
2025-01-08 22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/bmemcached/client/replicating.py 
2026-04-28 21:06:49.000000000 +0200
@@ -1,3 +1,5 @@
+import warnings
+
 from bmemcached.client.mixin import ClientMixin
 
 
@@ -6,8 +8,36 @@
     This is intended to be a client class which implement standard cache 
interface that common libs do...
 
     It replicates values over servers and get a response from the first one it 
can.
+
+    .. warning::
+        CAS operations are fundamentally incompatible with multi-server
+        replication. Each server maintains its own independent CAS counter,
+        so a CAS value obtained from one replica will not match any other
+        replica. As a consequence:
+
+        * :meth:`cas` against more than one replica causes at most one
+          server to accept the write; the rest silently reject it, leaving
+          the replicas divergent. The same hazard applies to
+          :meth:`set_multi` mappings that use ``(key, cas)`` tuple keys.
+        * :meth:`gets`, :meth:`get` with ``get_cas=True``, and
+          :meth:`get_multi` with ``get_cas=True`` return a CAS from
+          whichever replica happens to respond first. That value cannot
+          be safely passed back to :meth:`cas` on a multi-replica client,
+          for the reason above.
+
+        If you need CAS semantics, configure this client with exactly one
+        server (or use :class:`DistributedClient`).
     """
 
+    def _warn_multi_replica_cas(self, op, hazard):
+        if len(self._servers) > 1:
+            warnings.warn(
+                "{} on a ReplicatingClient with more than one server {}. "
+                "See the class docstring.".format(op, hazard),
+                UserWarning,
+                stacklevel=3,
+            )
+
     def _set_retry_delay(self, value):
         for server in self._servers:
             server.set_retry_delay(value)
@@ -31,6 +61,12 @@
         """
         Get a key from server.
 
+        .. warning::
+            When called with ``get_cas=True`` against more than one replica,
+            the returned CAS is from whichever replica responded first and
+            cannot be safely passed to :meth:`cas` on this client. See the
+            class-level note on CAS and replication.
+
         :param key: Key's name
         :type key: six.string_types
         :param default: In case memcached does not find a key, return a 
default value
@@ -39,6 +75,11 @@
         :return: Returns a key data from server.
         :rtype: object
         """
+        if get_cas:
+            self._warn_multi_replica_cas(
+                "get(get_cas=True)",
+                "returns a CAS that cannot be safely passed back to cas() on 
this client",
+            )
         for server in self.servers:
             value, cas = server.get(key)
             if value is not None:
@@ -59,11 +100,21 @@
 
         This method is for API compatibility with other implementations.
 
+        .. warning::
+            Against more than one replica, the returned CAS is from
+            whichever replica responded first and cannot be safely passed
+            to :meth:`cas` on this client. See the class-level note on
+            CAS and replication.
+
         :param key: Key's name
         :type key: six.string_types
         :return: Returns (key data, value), or (None, None) if the value is 
not in cache.
         :rtype: object
         """
+        self._warn_multi_replica_cas(
+            "gets()",
+            "returns a CAS that cannot be safely passed back to cas() on this 
client",
+        )
         for server in self.servers:
             value, cas = server.get(key)
             if value is not None:
@@ -74,6 +125,13 @@
         """
         Get multiple keys from server.
 
+        .. warning::
+            When called with ``get_cas=True`` against more than one replica,
+            each key's returned CAS is from whichever replica returned that
+            key first; none of those values can be safely passed to
+            :meth:`cas` on this client. See the class-level note on CAS
+            and replication.
+
         :param keys: A list of keys to from server.
         :type keys: list
         :param get_cas: If get_cas is true, each value is (data, cas), with 
each result's CAS value.
@@ -81,6 +139,11 @@
         :return: A dict with all requested keys.
         :rtype: dict
         """
+        if get_cas:
+            self._warn_multi_replica_cas(
+                "get_multi(get_cas=True)",
+                "returns CAS values that cannot be safely passed back to cas() 
on this client",
+            )
         d = {}
         if keys:
             for server in self.servers:
@@ -95,7 +158,7 @@
                     break
         return d
 
-    def set(self, key, value, time=0, compress_level=-1):
+    def set(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Set a value for a key on server.
 
@@ -109,19 +172,40 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure. Only supported when
+            the client is configured with a single server; see the class
+            docstring for why CAS and multi-server replication don't mix.
+        :type get_cas: bool
+        :return: True in case of success and False in case of failure, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        :raises NotImplementedError: if get_cas=True and more than one
+            server is configured.
         """
+        if get_cas:
+            if len(self._servers) > 1:
+                raise NotImplementedError(
+                    "get_cas=True is not supported on ReplicatingClient with "
+                    "more than one server."
+                )
+            return self._servers[0].set(key, value, time, 
compress_level=compress_level, get_cas=True)
+
         returns = []
         for server in self.servers:
             returns.append(server.set(key, value, time, 
compress_level=compress_level))
-
         return any(returns)
 
-    def cas(self, key, value, cas, time=0, compress_level=-1):
+    def cas(self, key, value, cas, time=0, compress_level=-1, get_cas=False):
         """
         Set a value for a key on server if its CAS value matches cas.
 
+        .. warning::
+            See the class-level note on CAS and replication. Each replica has
+            its own CAS counter, so a single CAS value cannot match on more
+            than one server. Calling this against multiple replicas will
+            silently diverge them -- at most one replica accepts the write.
+
         :param key: Key's name
         :type key: six.string_types
         :param value: A value to be stored on server.
@@ -134,19 +218,44 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
+        :param get_cas: If true, return (success, new_cas) where new_cas is
+            the item's new CAS after the operation, or None on failure. Only
+            supported when the client is configured with a single server;
+            see the class docstring.
+        :type get_cas: bool
+        :return: True in case of success and False in case of failure, or a
+            (success, new_cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        :raises NotImplementedError: if get_cas=True and more than one
+            server is configured.
         """
+        if get_cas:
+            if len(self._servers) > 1:
+                raise NotImplementedError(
+                    "get_cas=True is not supported on ReplicatingClient with "
+                    "more than one server."
+                )
+            return self._servers[0].cas(key, value, cas, time, 
compress_level=compress_level, get_cas=True)
+
+        self._warn_multi_replica_cas(
+            "cas()",
+            "will silently diverge replicas: at most one server can match a 
given CAS",
+        )
         returns = []
         for server in self.servers:
             returns.append(server.cas(key, value, cas, time, 
compress_level=compress_level))
-
         return any(returns)
 
     def set_multi(self, mappings, time=0, compress_level=-1):
         """
         Set multiple keys with it's values on server.
 
+        .. warning::
+            If any key is given as a ``(key, cas)`` tuple, the same CAS-plus-
+            replication hazard documented on :meth:`cas` applies: the CAS
+            value can match at most one replica, so those entries will
+            silently diverge across servers.
+
         :param mappings: A dict with keys/values
         :type mappings: dict
         :param time: Time in seconds that your key will expire.
@@ -158,6 +267,11 @@
         :return: List of keys that failed to be set on any server.
         :rtype: list
         """
+        if len(self._servers) > 1 and any(isinstance(k, tuple) for k in 
mappings):
+            self._warn_multi_replica_cas(
+                "set_multi() with (key, cas) tuple keys",
+                "will silently diverge replicas for those entries: at most one 
server can match a given CAS",
+            )
         returns = set()
         if mappings:
             for server in self.servers:
@@ -165,7 +279,39 @@
 
         return list(returns)
 
-    def add(self, key, value, time=0, compress_level=-1):
+    def set_multi_cas(self, mappings, time=0, compress_level=-1):
+        """
+        Set multiple keys with their values on the server, returning the new
+        CAS value for each successfully stored key.
+
+        Only supported when the client is configured with a single server;
+        see the class docstring for why CAS and multi-server replication
+        don't mix.
+
+        :param mappings: A dict with keys/values. Keys may be (key, cas)
+            tuples as in set_multi.
+        :type mappings: dict
+        :param time: Time in seconds that your key will expire.
+        :type time: int
+        :param compress_level: How much to compress.
+            0 = no compression, 1 = fastest, 9 = slowest but best,
+            -1 = default compression level.
+        :type compress_level: int
+        :return: A dict keyed by the string key of every input mapping. The
+            value is the new CAS int on success or None on failure.
+        :rtype: dict
+        :raises NotImplementedError: if more than one server is configured.
+        """
+        if len(self._servers) > 1:
+            raise NotImplementedError(
+                "set_multi_cas is not supported on ReplicatingClient with "
+                "more than one server."
+            )
+        if not mappings:
+            return {}
+        return self._servers[0].set_multi_cas(mappings, time, 
compress_level=compress_level)
+
+    def add(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Add a key/value to server ony if it does not exist.
 
@@ -179,16 +325,31 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is added False if key already exists
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure. Only supported when
+            the client is configured with a single server; see the class
+            docstring.
+        :type get_cas: bool
+        :return: True if key is added False if key already exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        :raises NotImplementedError: if get_cas=True and more than one
+            server is configured.
         """
+        if get_cas:
+            if len(self._servers) > 1:
+                raise NotImplementedError(
+                    "get_cas=True is not supported on ReplicatingClient with "
+                    "more than one server."
+                )
+            return self._servers[0].add(key, value, time, 
compress_level=compress_level, get_cas=True)
+
         returns = []
         for server in self.servers:
             returns.append(server.add(key, value, time, 
compress_level=compress_level))
-
         return any(returns)
 
-    def replace(self, key, value, time=0, compress_level=-1):
+    def replace(self, key, value, time=0, compress_level=-1, get_cas=False):
         """
         Replace a key/value to server ony if it does exist.
 
@@ -202,13 +363,28 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is replace False if key does not exists
-        :rtype: bool
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure. Only supported when
+            the client is configured with a single server; see the class
+            docstring.
+        :type get_cas: bool
+        :return: True if key is replace False if key does not exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        :raises NotImplementedError: if get_cas=True and more than one
+            server is configured.
         """
+        if get_cas:
+            if len(self._servers) > 1:
+                raise NotImplementedError(
+                    "get_cas=True is not supported on ReplicatingClient with "
+                    "more than one server."
+                )
+            return self._servers[0].replace(key, value, time, 
compress_level=compress_level, get_cas=True)
+
         returns = []
         for server in self.servers:
             returns.append(server.replace(key, value, time, 
compress_level=compress_level))
-
         return any(returns)
 
     def delete(self, key, cas=0):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/bmemcached/protocol.py 
new/python_binary_memcached-0.32.0/bmemcached/protocol.py
--- old/python_binary_memcached-0.31.4/bmemcached/protocol.py   2025-01-08 
22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/bmemcached/protocol.py   2026-04-28 
21:06:49.000000000 +0200
@@ -573,8 +573,9 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
+        :return: A (success, cas) tuple. success is True on success and False
+            on failure; cas is the new CAS value on success and None otherwise.
+        :rtype: tuple
         """
         time = time if time >= 0 else self.MAXIMUM_EXPIRE_TIME
         logger.debug('Setting/adding/replacing key %s.', key)
@@ -596,16 +597,16 @@
 
         if status != self.STATUS['success']:
             if status == self.STATUS['key_exists']:
-                return False
+                return False, None
             elif status == self.STATUS['key_not_found']:
-                return False
+                return False, None
             elif status == self.STATUS['server_disconnected']:
-                return False
+                return False, None
             raise MemcachedException('Code: %d Message: %s' % (status, 
extra_content), status)
 
-        return True
+        return True, cas
 
-    def set(self, key, value, time, compress_level=-1):
+    def set(self, key, value, time, compress_level=-1, get_cas=False):
         """
         Set a value for a key on server.
 
@@ -619,12 +620,19 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True in case of success and False in case of failure
-        :rtype: bool
-        """
-        return self._set_add_replace('set', key, value, time, 
compress_level=compress_level)
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True in case of success and False in case of failure, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        """
+        success, cas = self._set_add_replace('set', key, value, time, 
compress_level=compress_level)
+        if get_cas:
+            return success, cas
+        return success
 
-    def cas(self, key, value, cas, time, compress_level=-1):
+    def cas(self, key, value, cas, time, compress_level=-1, get_cas=False):
         """
         Add a key/value to server ony if it does not exist.
 
@@ -638,8 +646,12 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is added False if key already exists and has a 
different CAS
-        :rtype: bool
+        :param get_cas: If true, return (success, new_cas) where new_cas is
+            the item's new CAS after the operation, or None on failure.
+        :type get_cas: bool
+        :return: True if key is added False if key already exists and has a
+            different CAS, or a (success, new_cas) tuple if get_cas=True.
+        :rtype: bool or tuple
         """
         # The protocol CAS value 0 means "no cas".  Calling cas() with that 
value is
         # probably unintentional.  Don't allow it, since it would overwrite 
the value
@@ -649,11 +661,14 @@
         # If we get a cas of None, interpret that as "compare against 
nonexistant and set",
         # which is simply Add.
         if cas is None:
-            return self._set_add_replace('add', key, value, time, 
compress_level=compress_level)
+            success, new_cas = self._set_add_replace('add', key, value, time, 
compress_level=compress_level)
         else:
-            return self._set_add_replace('set', key, value, time, cas=cas, 
compress_level=compress_level)
+            success, new_cas = self._set_add_replace('set', key, value, time, 
cas=cas, compress_level=compress_level)
+        if get_cas:
+            return success, new_cas
+        return success
 
-    def add(self, key, value, time, compress_level=-1):
+    def add(self, key, value, time, compress_level=-1, get_cas=False):
         """
         Add a key/value to server ony if it does not exist.
 
@@ -667,12 +682,19 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is added False if key already exists
-        :rtype: bool
-        """
-        return self._set_add_replace('add', key, value, time, 
compress_level=compress_level)
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True if key is added False if key already exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        """
+        success, cas = self._set_add_replace('add', key, value, time, 
compress_level=compress_level)
+        if get_cas:
+            return success, cas
+        return success
 
-    def replace(self, key, value, time, compress_level=-1):
+    def replace(self, key, value, time, compress_level=-1, get_cas=False):
         """
         Replace a key/value to server ony if it does exist.
 
@@ -686,10 +708,17 @@
             0 = no compression, 1 = fastest, 9 = slowest but best,
             -1 = default compression level.
         :type compress_level: int
-        :return: True if key is replace False if key does not exists
-        :rtype: bool
-        """
-        return self._set_add_replace('replace', key, value, time, 
compress_level=compress_level)
+        :param get_cas: If true, return (success, cas) where cas is the new
+            CAS value on success and None on failure.
+        :type get_cas: bool
+        :return: True if key is replace False if key does not exists, or a
+            (success, cas) tuple if get_cas=True.
+        :rtype: bool or tuple
+        """
+        success, cas = self._set_add_replace('replace', key, value, time, 
compress_level=compress_level)
+        if get_cas:
+            return success, cas
+        return success
 
     def set_multi(self, mappings, time=100, compress_level=-1):
         """
@@ -760,6 +789,72 @@
 
         return failed
 
+    def set_multi_cas(self, mappings, time=100, compress_level=-1):
+        """
+        Set multiple keys with their values on server and return the new CAS
+        value for each successfully stored key.
+
+        If a key is a (key, cas) tuple, insert as if cas(key, value, cas) had
+        been called. A cas of 0 means add-if-not-exists.
+
+        Unlike set_multi, this uses the non-quiet set/add opcodes so that the
+        server responds to every request; this costs one response per key but
+        is what allows per-key CAS values to be returned.
+
+        :param mappings: A dict with keys/values
+        :type mappings: dict
+        :param time: Time in seconds that your key will expire.
+        :type time: int
+        :param compress_level: How much to compress.
+            0 = no compression, 1 = fastest, 9 = slowest but best,
+            -1 = default compression level.
+        :type compress_level: int
+        :return: A dict keyed by the string key of every input mapping. The
+            value is the new CAS int on success or None on failure.
+        :rtype: dict
+        """
+        mappings = list(mappings.items())
+        msg = bytearray()
+        result = {}
+
+        for opaque, (key, value) in enumerate(mappings):
+            if isinstance(key, tuple):
+                str_key, cas = key
+            else:
+                str_key, cas = key, None
+            result[str_key] = None
+
+            if cas == 0:
+                command = 'add'
+            else:
+                command = 'set'
+
+            keybytes = str_to_bytes(str_key)
+            flags, value = self.serialize(value, compress_level=compress_level)
+            msg += struct.pack(self.HEADER_STRUCT +
+                               self.COMMANDS[command]['struct'] % 
(len(keybytes), len(value)),
+                               self.MAGIC['request'],
+                               self.COMMANDS[command]['command'],
+                               len(keybytes),
+                               8, 0, 0, len(keybytes) + len(value) + 8, 
opaque, cas or 0,
+                               flags, time, keybytes, value)
+
+        self._send(msg)
+
+        # Non-quiet set/add return exactly one response per request, so we can
+        # read a fixed count rather than relying on a trailing noop sentinel.
+        for _ in range(len(mappings)):
+            (magic, opcode, keylen, extlen, datatype, status, bodylen, opaque,
+             cas, extra_content) = self._get_response()
+            if status == self.STATUS['server_disconnected']:
+                return result
+            if status == self.STATUS['success']:
+                key, value = mappings[opaque]
+                str_key = key[0] if isinstance(key, tuple) else key
+                result[str_key] = cas
+
+        return result
+
     def _incr_decr(self, command, key, value, default, time):
         """
         Function which increments and decrements.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python_binary_memcached-0.31.4/docs/conf.py 
new/python_binary_memcached-0.32.0/docs/conf.py
--- old/python_binary_memcached-0.31.4/docs/conf.py     2025-01-08 
22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/docs/conf.py     2026-04-28 
21:06:49.000000000 +0200
@@ -50,9 +50,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '0.31'
+version = '0.32'
 # The full version, including alpha/beta/rc tags.
-release = '0.31.4'
+release = '0.32.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/python_binary_memcached.egg-info/PKG-INFO 
new/python_binary_memcached-0.32.0/python_binary_memcached.egg-info/PKG-INFO
--- 
old/python_binary_memcached-0.31.4/python_binary_memcached.egg-info/PKG-INFO    
    2025-01-08 22:18:11.000000000 +0100
+++ 
new/python_binary_memcached-0.32.0/python_binary_memcached.egg-info/PKG-INFO    
    2026-04-28 21:07:17.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.2
+Metadata-Version: 2.4
 Name: python-binary-memcached
-Version: 0.31.4
+Version: 0.32.0
 Summary: A pure python module to access memcached via its binary protocol with 
SASL auth support
 Home-page: https://github.com/jaysonsantos/python-binary-memcached
 Author: Jayson Reis
@@ -8,13 +8,11 @@
 Classifier: Development Status :: 4 - Beta
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
 Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
 License-File: LICENSE
 Requires-Dist: six
 Requires-Dist: uhashring
@@ -23,6 +21,7 @@
 Dynamic: classifier
 Dynamic: description
 Dynamic: home-page
+Dynamic: license-file
 Dynamic: requires-dist
 Dynamic: summary
 
@@ -111,6 +110,14 @@
 .. _`pytest`: https://pypi.org/project/pytest/
 .. _`tox`: https://pypi.org/project/tox/
 
+v0.32.0 (2026-04-28)
+====================
+
+Docs
+----
+
+-  Add repository contribution guidelines.
+
 v0.31.2 (2022-12-14)
 ====================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python_binary_memcached-0.31.4/setup.py 
new/python_binary_memcached-0.32.0/setup.py
--- old/python_binary_memcached-0.31.4/setup.py 2025-01-08 22:17:54.000000000 
+0100
+++ new/python_binary_memcached-0.32.0/setup.py 2026-04-28 21:06:49.000000000 
+0200
@@ -14,7 +14,7 @@
 
 setup(
     name="python-binary-memcached",
-    version="0.31.4",
+    version="0.32.0",
     author="Jayson Reis",
     author_email="[email protected]",
     description="A pure python module to access memcached via its binary 
protocol with SASL auth support",
@@ -25,13 +25,11 @@
         "Development Status :: 4 - Beta",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
-        "Programming Language :: Python :: 2.7",
-        "Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3.12",
     ],
     install_requires=[
         "six",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python_binary_memcached-0.31.4/test/test_simple_functions.py 
new/python_binary_memcached-0.32.0/test/test_simple_functions.py
--- old/python_binary_memcached-0.31.4/test/test_simple_functions.py    
2025-01-08 22:17:54.000000000 +0100
+++ new/python_binary_memcached-0.32.0/test/test_simple_functions.py    
2026-04-28 21:06:49.000000000 +0200
@@ -1,5 +1,6 @@
 import os
 import unittest
+import warnings
 
 import six
 import struct
@@ -27,6 +28,7 @@
     def reset(self):
         self.client.delete('test_key')
         self.client.delete('test_key2')
+        self.client.delete('fresh_key')
 
     def testSet(self):
         self.assertTrue(self.client.set('test_key', 'test'))
@@ -120,6 +122,51 @@
         }), [])
         self.assertEqual(self.client.get('test_key'), 'value4')
 
+    def testSetMultiCas(self):
+        # All-success plain keys: every input gets a non-None CAS, and each
+        # returned CAS matches what gets() reports afterwards.
+        result = self.client.set_multi_cas({
+            'test_key': 'value1',
+            'test_key2': 'value2',
+        })
+        self.assertEqual(set(result.keys()), {'test_key', 'test_key2'})
+        self.assertTrue(result['test_key'] is not None)
+        self.assertTrue(result['test_key2'] is not None)
+        _, cas1 = self.client.gets('test_key')
+        _, cas2 = self.client.gets('test_key2')
+        self.assertEqual(result['test_key'], cas1)
+        self.assertEqual(result['test_key2'], cas2)
+
+        # CAS failure: add-if-not-exists when the key already exists returns
+        # None for that key; unrelated keys still succeed.
+        result = self.client.set_multi_cas({
+            ('test_key', 0): 'shouldnt_store',
+            'fresh_key': 'fresh',
+        })
+        self.assertTrue(result['test_key'] is None)
+        self.assertTrue(result['fresh_key'] is not None)
+        self.assertEqual(self.client.get('test_key'), 'value1')
+        self.client.delete('fresh_key')
+
+        # Stale-CAS failure: capture cas, mutate out of band, then 
set_multi_cas
+        # with the stale cas must fail and leave the out-of-band value intact.
+        _, stale_cas = self.client.gets('test_key')
+        self.client.set('test_key', 'other')
+        result = self.client.set_multi_cas({
+            ('test_key', stale_cas): 'should_fail',
+        })
+        self.assertTrue(result['test_key'] is None)
+        self.assertEqual(self.client.get('test_key'), 'other')
+
+        # Returned CAS is usable directly in cas() without a gets() round-trip.
+        self.client.delete('test_key')
+        result = self.client.set_multi_cas({'test_key': 'v'})
+        self.assertTrue(self.client.cas('test_key', 'v2', result['test_key']))
+        self.assertEqual(self.client.get('test_key'), 'v2')
+
+    def testSetMultiCasEmpty(self):
+        self.assertEqual(self.client.set_multi_cas({}), {})
+
     def testGetMultiCas(self):
         self.client.set('test_key', 'value1')
         self.client.set('test_key2', 'value2')
@@ -133,6 +180,47 @@
         self.assertEqual(values.get('test_key')[0], 'value1')
         self.assertEqual(values.get('test_key2')[0], 'value2')
 
+    def testCasMultiReplicaWarns(self):
+        # Pre-existing CAS-touching methods on ReplicatingClient produce
+        # silently-wrong behavior when run against more than one replica
+        # (each server has its own CAS counter). Confirm each fires a
+        # UserWarning at runtime so callers have some signal that they
+        # should reconfigure.
+        client = bmemcached.Client(
+            ['/tmp/memcached.sock', 
'{}:11211'.format(os.environ['MEMCACHED_HOST'])],
+            'user', 'password',
+        )
+        try:
+            with warnings.catch_warnings(record=True) as caught:
+                warnings.simplefilter("always")
+                client.cas('test_key', 'v', None)
+                client.gets('test_key')
+                client.get('test_key', get_cas=True)
+                client.get_multi(['test_key'], get_cas=True)
+                client.set_multi({('test_key', 0): 'v'})
+            messages = [str(w.message) for w in caught
+                        if issubclass(w.category, UserWarning)]
+            self.assertEqual(len(messages), 5)
+            self.assertTrue(any('cas() on a ReplicatingClient' in m for m in 
messages))
+            self.assertTrue(any('gets() on a ReplicatingClient' in m for m in 
messages))
+            self.assertTrue(any('get(get_cas=True) on a ReplicatingClient' in 
m for m in messages))
+            self.assertTrue(any('get_multi(get_cas=True) on a 
ReplicatingClient' in m for m in messages))
+            self.assertTrue(any('set_multi() with (key, cas) tuple keys on a 
ReplicatingClient' in m for m in messages))
+
+            # Non-CAS calls do not warn, even on multi-replica.
+            with warnings.catch_warnings(record=True) as caught:
+                warnings.simplefilter("always")
+                client.set('test_key', 'v')
+                client.get('test_key')
+                client.get_multi(['test_key'])
+                client.set_multi({'test_key': 'v'})
+            user_warnings = [w for w in caught
+                             if issubclass(w.category, UserWarning)]
+            self.assertEqual(user_warnings, [])
+        finally:
+            client.delete('test_key')
+            client.disconnect_all()
+
     def testGetEmptyString(self):
         self.client.set('test_key', '')
         self.assertEqual('', self.client.get('test_key'))
@@ -196,6 +284,83 @@
         self.client.add('test_key', 'value')
         self.assertFalse(self.client.add('test_key', 'test'))
 
+    def testAddCas(self):
+        success, cas = self.client.add('test_key', 'value', get_cas=True)
+        self.assertTrue(success)
+        self.assertTrue(cas is not None)
+
+        # The CAS returned by add() must equal the CAS later returned by 
gets().
+        _, gets_cas = self.client.gets('test_key')
+        self.assertEqual(cas, gets_cas)
+
+        # A second add of the same key fails; cas is None.
+        success2, cas2 = self.client.add('test_key', 'value2', get_cas=True)
+        self.assertFalse(success2)
+        self.assertTrue(cas2 is None)
+
+        # The CAS returned from add() can be used directly in cas() without
+        # a separate gets() round-trip.
+        self.assertTrue(self.client.cas('test_key', 'value3', cas))
+        self.assertEqual('value3', self.client.get('test_key'))
+
+        # Backward compatibility: with no get_cas kwarg, add() still returns a 
plain bool.
+        result = self.client.add('test_key2', 'value')
+        self.assertEqual(True, result)
+
+    def testSetCas(self):
+        # set() with get_cas=True returns (True, cas) and cas matches gets().
+        success, cas = self.client.set('test_key', 'v1', get_cas=True)
+        self.assertTrue(success)
+        self.assertTrue(cas is not None)
+        _, gets_cas = self.client.gets('test_key')
+        self.assertEqual(cas, gets_cas)
+
+        # The returned CAS is usable directly in cas() without a gets() 
round-trip.
+        self.assertTrue(self.client.cas('test_key', 'v2', cas))
+        self.assertEqual('v2', self.client.get('test_key'))
+
+        # Backward compatibility: no get_cas kwarg still returns a plain bool.
+        self.assertEqual(True, self.client.set('test_key2', 'v'))
+
+    def testReplaceCas(self):
+        # Replace on a nonexistent key fails; cas is None.
+        success, cas = self.client.replace('test_key', 'v', get_cas=True)
+        self.assertFalse(success)
+        self.assertTrue(cas is None)
+
+        # Replace on an existing key succeeds and returns the new CAS.
+        self.client.set('test_key', 'original')
+        success, cas = self.client.replace('test_key', 'new', get_cas=True)
+        self.assertTrue(success)
+        self.assertTrue(cas is not None)
+        _, gets_cas = self.client.gets('test_key')
+        self.assertEqual(cas, gets_cas)
+
+        # Backward compatibility: no get_cas kwarg still returns a plain bool.
+        self.assertEqual(True, self.client.replace('test_key', 'x'))
+
+    def testCasCas(self):
+        # cas() with get_cas=True, invoked as add (cas=None): returns new CAS.
+        success, cas = self.client.cas('test_key', 'v1', None, get_cas=True)
+        self.assertTrue(success)
+        self.assertTrue(cas is not None)
+
+        # Chain a second CAS using the returned value directly (no gets()).
+        success2, cas2 = self.client.cas('test_key', 'v2', cas, get_cas=True)
+        self.assertTrue(success2)
+        self.assertTrue(cas2 is not None)
+        self.assertNotEqual(cas, cas2)
+        self.assertEqual('v2', self.client.get('test_key'))
+
+        # A stale CAS fails; the returned new_cas is None.
+        success3, cas3 = self.client.cas('test_key', 'v3', cas, get_cas=True)
+        self.assertFalse(success3)
+        self.assertTrue(cas3 is None)
+        self.assertEqual('v2', self.client.get('test_key'))
+
+        # Backward compatibility: no get_cas kwarg still returns a plain bool.
+        self.assertEqual(True, self.client.cas('test_key', 'v4', cas2))
+
     def testReplacePass(self):
         self.client.add('test_key', 'value')
         self.assertTrue(self.client.replace('test_key', 'value2'))
@@ -237,6 +402,33 @@
         self.client.disconnect_all()
         self.assertEqual('test', self.client.get('test_key'))
 
+    def testGetCasMultiReplicaRaises(self):
+        # A ReplicatingClient with >1 server can't safely return a per-server
+        # CAS, since each replica has its own CAS counter. Confirm every new
+        # get_cas path raises NotImplementedError rather than returning a
+        # value the caller can't use.
+        client = bmemcached.Client(
+            ['/tmp/memcached.sock', 
'{}:11211'.format(os.environ['MEMCACHED_HOST'])],
+            'user', 'password',
+        )
+        try:
+            with self.assertRaises(NotImplementedError):
+                client.add('test_key', 'v', get_cas=True)
+            with self.assertRaises(NotImplementedError):
+                client.set('test_key', 'v', get_cas=True)
+            with self.assertRaises(NotImplementedError):
+                client.replace('test_key', 'v', get_cas=True)
+            with self.assertRaises(NotImplementedError):
+                client.cas('test_key', 'v', None, get_cas=True)
+            with self.assertRaises(NotImplementedError):
+                client.set_multi_cas({'test_key': 'v'})
+
+            # get_cas=False (default) still works fine on multi-replica.
+            self.assertTrue(client.set('test_key', 'v'))
+        finally:
+            client.delete('test_key')
+            client.disconnect_all()
+
 
 class TimeoutMemcachedTests(unittest.TestCase):
     def setUp(self):

Reply via email to