From 8d07f4cc9fb9304a66bbcfa0f3f6abd5d2a36be0 Mon Sep 17 00:00:00 2001 From: Dana Powers Date: Tue, 10 Mar 2026 11:30:11 -0700 Subject: [PATCH 1/3] kafka.admin: Fix DESCRIBE_TOKENS ACLOperation enum; support authorized_operations=None --- kafka/admin/acl_resource.py | 2 +- kafka/admin/client.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kafka/admin/acl_resource.py b/kafka/admin/acl_resource.py index 2ac4b4f43..a323f69d9 100644 --- a/kafka/admin/acl_resource.py +++ b/kafka/admin/acl_resource.py @@ -38,7 +38,7 @@ class ACLOperation(IntEnum): ALTER_CONFIGS = 11, IDEMPOTENT_WRITE = 12, CREATE_TOKENS = 13, - DESCRIBE_TOKENS = 13 + DESCRIBE_TOKENS = 14 class ACLPermissionType(IntEnum): diff --git a/kafka/admin/client.py b/kafka/admin/client.py index 0319127b8..69ac0c4b1 100644 --- a/kafka/admin/client.py +++ b/kafka/admin/client.py @@ -532,10 +532,10 @@ def delete_topics(self, topics, timeout_ms=None): def _process_metadata_response(self, metadata_response): obj = metadata_response.to_object() - if 'authorized_operations' in obj: + if obj.get('authorized_operations', None) is not None: obj['authorized_operations'] = list(map(lambda acl: acl.name, valid_acl_operations(obj['authorized_operations']))) for t in obj['topics']: - if 'authorized_operations' in t: + if t.get('authorized_operations', None) is not None: t['authorized_operations'] = list(map(lambda acl: acl.name, valid_acl_operations(t['authorized_operations']))) return obj From f9c9999103011ab70c1a5e2c97b9015fb4a75ad8 Mon Sep 17 00:00:00 2001 From: Dana Powers Date: Tue, 10 Mar 2026 11:31:46 -0700 Subject: [PATCH 2/3] Convert BitField sentinel {31} <=> None --- kafka/protocol/types.py | 9 ++++++++- test/protocol/test_parser.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/kafka/protocol/types.py b/kafka/protocol/types.py index 855d1ae19..ad6cf7b26 100644 --- a/kafka/protocol/types.py +++ b/kafka/protocol/types.py @@ -339,6 +339,8 @@ def decode(cls, data): @classmethod def encode(cls, value): + if value is None: + value = {} ret = UnsignedVarInt32.encode(len(value)) for k, v in value.items(): # do we allow for other data types ?? It could get complicated really fast @@ -388,10 +390,15 @@ def decode(self, data): class BitField(AbstractType): @classmethod def decode(cls, data): - return cls.from_32_bit_field(Int32.decode(data)) + vals = cls.from_32_bit_field(Int32.decode(data)) + if vals == {31}: + vals = None + return vals @classmethod def encode(cls, vals): + if vals is None: + vals = {31} # to_32_bit_field returns unsigned val, so we need to # encode >I to avoid crash if/when byte 31 is set # (note that decode as signed still works fine) diff --git a/test/protocol/test_parser.py b/test/protocol/test_parser.py index e29ad96dd..e4aaad3f6 100644 --- a/test/protocol/test_parser.py +++ b/test/protocol/test_parser.py @@ -72,7 +72,7 @@ 'response': ( b'\x00\x00\x00E\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\tlocalhost\x00\x00\xb9}\xff\xff\x00\x1634wjNp3hRJixCRvMlK9Znw\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00', 3, - MetadataResponse[8](throttle_time_ms=0, brokers=[(0, 'localhost', 47485, None)], cluster_id='34wjNp3hRJixCRvMlK9Znw', controller_id=0, topics=[], authorized_operations={31}), + MetadataResponse[8](throttle_time_ms=0, brokers=[(0, 'localhost', 47485, None)], cluster_id='34wjNp3hRJixCRvMlK9Znw', controller_id=0, topics=[], authorized_operations=None), ), }, { @@ -83,7 +83,7 @@ 'response': ( b'\x00\x00\x00E\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\tlocalhost\x00\x00\xb9}\xff\xff\x00\x1634wjNp3hRJixCRvMlK9Znw\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00', 4, - MetadataResponse[8](throttle_time_ms=0, brokers=[(0, 'localhost', 47485, None)], cluster_id='34wjNp3hRJixCRvMlK9Znw', controller_id=0, topics=[], authorized_operations={31}), + MetadataResponse[8](throttle_time_ms=0, brokers=[(0, 'localhost', 47485, None)], cluster_id='34wjNp3hRJixCRvMlK9Znw', controller_id=0, topics=[], authorized_operations=None), ), } ), From ba538d66dc418f37bd8b880591645cc0b7fa282a Mon Sep 17 00:00:00 2001 From: Dana Powers Date: Tue, 10 Mar 2026 11:43:55 -0700 Subject: [PATCH 3/3] tests --- test/protocol/test_bit_field.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/protocol/test_bit_field.py b/test/protocol/test_bit_field.py index 5db155241..8c4b9defc 100644 --- a/test/protocol/test_bit_field.py +++ b/test/protocol/test_bit_field.py @@ -6,8 +6,14 @@ @pytest.mark.parametrize(('test_set',), [ - (set([0, 1, 5, 10, 31]),), - (set(range(32)),), + (set([0, 1, 5, 10]),), + (set(range(15)),), + (None,), ]) def test_bit_field(test_set): assert BitField.decode(io.BytesIO(BitField.encode(test_set))) == test_set + + +def test_bit_field_null(): + assert BitField.from_32_bit_field(-2147483648) == {31} + assert BitField.decode(io.BytesIO(BitField.encode({31}))) is None