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):