kafka-python 2.2.17__tar.gz → 2.2.19__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {kafka_python-2.2.17 → kafka_python-2.2.19}/CHANGES.md +16 -0
  2. {kafka_python-2.2.17 → kafka_python-2.2.19}/PKG-INFO +1 -1
  3. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/codec.py +2 -4
  4. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/consumer/fetcher.py +5 -4
  5. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/consumer.py +2 -1
  6. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/record_accumulator.py +4 -0
  7. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/types.py +21 -20
  8. kafka_python-2.2.19/kafka/version.py +1 -0
  9. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka_python.egg-info/PKG-INFO +1 -1
  10. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka_python.egg-info/SOURCES.txt +0 -1
  11. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/test_admin_integration.py +2 -10
  12. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/test_consumer_integration.py +1 -1
  13. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/test_sasl_integration.py +2 -0
  14. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_codec.py +11 -0
  15. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_fetcher.py +187 -0
  16. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_record_accumulator.py +16 -0
  17. kafka_python-2.2.17/kafka/version.py +0 -1
  18. kafka_python-2.2.17/test/test_protocol.py +0 -334
  19. {kafka_python-2.2.17 → kafka_python-2.2.19}/AUTHORS.md +0 -0
  20. {kafka_python-2.2.17 → kafka_python-2.2.19}/LICENSE +0 -0
  21. {kafka_python-2.2.17 → kafka_python-2.2.19}/MANIFEST.in +0 -0
  22. {kafka_python-2.2.17 → kafka_python-2.2.19}/README.rst +0 -0
  23. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/__init__.py +0 -0
  24. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/__init__.py +0 -0
  25. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/acl_resource.py +0 -0
  26. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/client.py +0 -0
  27. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/config_resource.py +0 -0
  28. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/new_partitions.py +0 -0
  29. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/admin/new_topic.py +0 -0
  30. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/__init__.py +0 -0
  31. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/consumer_performance.py +0 -0
  32. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/load_example.py +0 -0
  33. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/producer_performance.py +0 -0
  34. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/record_batch_compose.py +0 -0
  35. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/record_batch_read.py +0 -0
  36. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/benchmarks/varint_speed.py +0 -0
  37. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/client_async.py +0 -0
  38. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/cluster.py +0 -0
  39. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/conn.py +0 -0
  40. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/consumer/__init__.py +0 -0
  41. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/consumer/group.py +0 -0
  42. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/consumer/subscription_state.py +0 -0
  43. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/__init__.py +0 -0
  44. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/__init__.py +0 -0
  45. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/abstract.py +0 -0
  46. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/range.py +0 -0
  47. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/roundrobin.py +0 -0
  48. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  49. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  50. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  51. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  52. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/base.py +0 -0
  53. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/heartbeat.py +0 -0
  54. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/coordinator/protocol.py +0 -0
  55. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/errors.py +0 -0
  56. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/future.py +0 -0
  57. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/__init__.py +0 -0
  58. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/compound_stat.py +0 -0
  59. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/dict_reporter.py +0 -0
  60. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/kafka_metric.py +0 -0
  61. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/measurable.py +0 -0
  62. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/measurable_stat.py +0 -0
  63. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/metric_config.py +0 -0
  64. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/metric_name.py +0 -0
  65. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/metrics.py +0 -0
  66. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/metrics_reporter.py +0 -0
  67. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/quota.py +0 -0
  68. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stat.py +0 -0
  69. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/__init__.py +0 -0
  70. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/avg.py +0 -0
  71. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/count.py +0 -0
  72. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/histogram.py +0 -0
  73. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/max_stat.py +0 -0
  74. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/min_stat.py +0 -0
  75. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/percentile.py +0 -0
  76. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/percentiles.py +0 -0
  77. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/rate.py +0 -0
  78. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/sampled_stat.py +0 -0
  79. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/sensor.py +0 -0
  80. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/metrics/stats/total.py +0 -0
  81. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/partitioner/__init__.py +0 -0
  82. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/partitioner/default.py +0 -0
  83. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/__init__.py +0 -0
  84. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/future.py +0 -0
  85. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/kafka.py +0 -0
  86. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/sender.py +0 -0
  87. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/producer/transaction_manager.py +0 -0
  88. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/__init__.py +0 -0
  89. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/abstract.py +0 -0
  90. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/add_offsets_to_txn.py +0 -0
  91. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/add_partitions_to_txn.py +0 -0
  92. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/admin.py +0 -0
  93. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/api.py +0 -0
  94. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/api_versions.py +0 -0
  95. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/broker_api_versions.py +0 -0
  96. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/commit.py +0 -0
  97. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/end_txn.py +0 -0
  98. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/fetch.py +0 -0
  99. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/find_coordinator.py +0 -0
  100. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/frame.py +0 -0
  101. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/group.py +0 -0
  102. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/init_producer_id.py +0 -0
  103. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/list_offsets.py +0 -0
  104. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/message.py +0 -0
  105. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/metadata.py +0 -0
  106. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/offset_for_leader_epoch.py +0 -0
  107. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/parser.py +0 -0
  108. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/pickle.py +0 -0
  109. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/produce.py +0 -0
  110. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/sasl_authenticate.py +0 -0
  111. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/sasl_handshake.py +0 -0
  112. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/struct.py +0 -0
  113. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/protocol/txn_offset_commit.py +0 -0
  114. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/__init__.py +0 -0
  115. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/_crc32c.py +0 -0
  116. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/abc.py +0 -0
  117. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/default_records.py +0 -0
  118. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/legacy_records.py +0 -0
  119. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/memory_records.py +0 -0
  120. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/record/util.py +0 -0
  121. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/__init__.py +0 -0
  122. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/abc.py +0 -0
  123. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/gssapi.py +0 -0
  124. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/msk.py +0 -0
  125. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/oauth.py +0 -0
  126. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/plain.py +0 -0
  127. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/scram.py +0 -0
  128. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/sasl/sspi.py +0 -0
  129. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/serializer/__init__.py +0 -0
  130. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/serializer/abstract.py +0 -0
  131. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/socks5_wrapper.py +0 -0
  132. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/structs.py +0 -0
  133. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/util.py +0 -0
  134. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/vendor/__init__.py +0 -0
  135. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/vendor/enum34.py +0 -0
  136. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/vendor/selectors34.py +0 -0
  137. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/vendor/six.py +0 -0
  138. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka/vendor/socketpair.py +0 -0
  139. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka_python.egg-info/dependency_links.txt +0 -0
  140. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka_python.egg-info/requires.txt +0 -0
  141. {kafka_python-2.2.17 → kafka_python-2.2.19}/kafka_python.egg-info/top_level.txt +0 -0
  142. {kafka_python-2.2.17 → kafka_python-2.2.19}/pyproject.toml +0 -0
  143. {kafka_python-2.2.17 → kafka_python-2.2.19}/setup.cfg +0 -0
  144. {kafka_python-2.2.17 → kafka_python-2.2.19}/setup.py +0 -0
  145. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/__init__.py +0 -0
  146. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/conftest.py +0 -0
  147. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/fixtures.py +0 -0
  148. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/test_consumer_group.py +0 -0
  149. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/integration/test_producer_integration.py +0 -0
  150. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_acl_comparisons.py +0 -0
  151. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_admin.py +0 -0
  152. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_api_object_implementation.py +0 -0
  153. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_assignors.py +0 -0
  154. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_client_async.py +0 -0
  155. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_cluster.py +0 -0
  156. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_conn.py +0 -0
  157. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_consumer.py +0 -0
  158. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_coordinator.py +0 -0
  159. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_metrics.py +0 -0
  160. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_object_conversion.py +0 -0
  161. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_package.py +0 -0
  162. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_partition_movements.py +0 -0
  163. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_partitioner.py +0 -0
  164. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_producer.py +0 -0
  165. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_sender.py +0 -0
  166. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_subscription_state.py +0 -0
  167. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/test_util.py +0 -0
  168. {kafka_python-2.2.17 → kafka_python-2.2.19}/test/testutil.py +0 -0
@@ -1,3 +1,19 @@
1
+ # 2.2.19 (Apr 9, 2026)
2
+
3
+ Fixes
4
+ * Fix TaggedFields value encoding; add test coverage (#2725)
5
+ * Fix zstd multi-frame decompression failure (#2717)
6
+ * Fix KeyError in KafkaConsumer.committed() (#2710)
7
+ * Fix VarInt/VarLong encoding; move tests to test/protocol/ (#2706)
8
+ * Fix `Fetcher._fetch_offsets_by_times retry handling` (#2833)
9
+ * Fixes to support integration testing with external KAFKA_URI (#2838)
10
+ * Minor py2 fixes: consumer integration test; done pip install python-snappy
11
+
12
+ # 2.2.18 (Nov 20, 2025)
13
+
14
+ Fixes
15
+ * Add ProducerBatch.__lt__ for heapq (#2698)
16
+
1
17
  # 2.2.17 (Nov 20, 2025)
2
18
 
3
19
  Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.2.17
3
+ Version: 2.2.19
4
4
  Summary: Pure Python client for Apache Kafka
5
5
  Author-email: Dana Powers <dana.powers@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/dpkp/kafka-python
@@ -327,7 +327,5 @@ def zstd_encode(payload):
327
327
  def zstd_decode(payload):
328
328
  if not zstd:
329
329
  raise NotImplementedError("Zstd codec is not available")
330
- try:
331
- return zstd.ZstdDecompressor().decompress(payload)
332
- except zstd.ZstdError:
333
- return zstd.ZstdDecompressor().decompress(payload, max_output_size=ZSTD_MAX_OUTPUT_SIZE)
330
+ with zstd.ZstdDecompressor().stream_reader(io.BytesIO(payload), read_across_frames=True) as reader:
331
+ return reader.read()
@@ -250,16 +250,17 @@ class Fetcher(six.Iterator):
250
250
  break
251
251
 
252
252
  if future.succeeded():
253
- fetched_offsets.update(future.value[0])
254
- if not future.value[1]:
253
+ offsets, retry = future.value
254
+ fetched_offsets.update(offsets)
255
+ if not retry:
255
256
  return fetched_offsets
256
257
 
257
- timestamps = {tp: timestamps[tp] for tp in future.value[1]}
258
+ timestamps = {tp: timestamps[tp] for tp in retry}
258
259
 
259
260
  elif not future.retriable():
260
261
  raise future.exception # pylint: disable-msg=raising-bad-type
261
262
 
262
- if future.exception.invalid_metadata or self._client.cluster.need_update:
263
+ elif future.exception.invalid_metadata or self._client.cluster.need_update:
263
264
  refresh_future = self._client.cluster.request_update()
264
265
  self._client.poll(future=refresh_future, timeout_ms=timer.timeout_ms)
265
266
 
@@ -443,7 +443,8 @@ class ConsumerCoordinator(BaseCoordinator):
443
443
  self._client.poll(future=future, timeout_ms=timer.timeout_ms)
444
444
 
445
445
  if future.is_done:
446
- del self._offset_fetch_futures[future_key]
446
+ if future_key in self._offset_fetch_futures:
447
+ del self._offset_fetch_futures[future_key]
447
448
 
448
449
  if future.succeeded():
449
450
  return future.value
@@ -179,6 +179,10 @@ class ProducerBatch(object):
179
179
  return 'ProducerBatch(topic_partition=%s, record_count=%d)' % (
180
180
  self.topic_partition, self.records.next_offset())
181
181
 
182
+ # for heapq
183
+ def __lt__(self, other):
184
+ return self.created < other.created
185
+
182
186
 
183
187
  class RecordAccumulator(object):
184
188
  """
@@ -213,6 +213,17 @@ class Array(AbstractType):
213
213
 
214
214
 
215
215
  class UnsignedVarInt32(AbstractType):
216
+ @classmethod
217
+ def decode(cls, data):
218
+ value = VarInt32.decode(data)
219
+ return (value << 1) ^ (value >> 31)
220
+
221
+ @classmethod
222
+ def encode(cls, value):
223
+ return VarInt32.encode((value >> 1) ^ -(value & 1))
224
+
225
+
226
+ class VarInt32(AbstractType):
216
227
  @classmethod
217
228
  def decode(cls, data):
218
229
  value, i = 0, 0
@@ -225,10 +236,12 @@ class UnsignedVarInt32(AbstractType):
225
236
  if i > 28:
226
237
  raise ValueError('Invalid value {}'.format(value))
227
238
  value |= b << i
228
- return value
239
+ return (value >> 1) ^ -(value & 1)
229
240
 
230
241
  @classmethod
231
242
  def encode(cls, value):
243
+ # bring it in line with the java binary repr
244
+ value = (value << 1) ^ (value >> 31)
232
245
  value &= 0xffffffff
233
246
  ret = b''
234
247
  while (value & 0xffffff80) != 0:
@@ -239,25 +252,12 @@ class UnsignedVarInt32(AbstractType):
239
252
  return ret
240
253
 
241
254
 
242
- class VarInt32(AbstractType):
243
- @classmethod
244
- def decode(cls, data):
245
- value = UnsignedVarInt32.decode(data)
246
- return (value >> 1) ^ -(value & 1)
247
-
248
- @classmethod
249
- def encode(cls, value):
250
- # bring it in line with the java binary repr
251
- value &= 0xffffffff
252
- return UnsignedVarInt32.encode((value << 1) ^ (value >> 31))
253
-
254
-
255
255
  class VarInt64(AbstractType):
256
256
  @classmethod
257
257
  def decode(cls, data):
258
258
  value, i = 0, 0
259
259
  while True:
260
- b = data.read(1)
260
+ b, = struct.unpack('B', data.read(1))
261
261
  if not (b & 0x80):
262
262
  break
263
263
  value |= (b & 0x7f) << i
@@ -270,14 +270,14 @@ class VarInt64(AbstractType):
270
270
  @classmethod
271
271
  def encode(cls, value):
272
272
  # bring it in line with the java binary repr
273
+ value = (value << 1) ^ (value >> 63)
273
274
  value &= 0xffffffffffffffff
274
- v = (value << 1) ^ (value >> 63)
275
275
  ret = b''
276
- while (v & 0xffffffffffffff80) != 0:
276
+ while (value & 0xffffffffffffff80) != 0:
277
277
  b = (value & 0x7f) | 0x80
278
278
  ret += struct.pack('B', b)
279
- v >>= 7
280
- ret += struct.pack('B', v)
279
+ value >>= 7
280
+ ret += struct.pack('B', value)
281
281
  return ret
282
282
 
283
283
 
@@ -322,8 +322,9 @@ class TaggedFields(AbstractType):
322
322
  for k, v in value.items():
323
323
  # do we allow for other data types ?? It could get complicated really fast
324
324
  assert isinstance(v, bytes), 'Value {} is not a byte array'.format(v)
325
- assert isinstance(k, int) and k > 0, 'Key {} is not a positive integer'.format(k)
325
+ assert isinstance(k, int) and k >= 0, 'Key {} is not a non-negative integer'.format(k)
326
326
  ret += UnsignedVarInt32.encode(k)
327
+ ret += UnsignedVarInt32.encode(len(v))
327
328
  ret += v
328
329
  return ret
329
330
 
@@ -0,0 +1 @@
1
+ __version__ = '2.2.19'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.2.17
3
+ Version: 2.2.19
4
4
  Summary: Pure Python client for Apache Kafka
5
5
  Author-email: Dana Powers <dana.powers@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/dpkp/kafka-python
@@ -149,7 +149,6 @@ test/test_package.py
149
149
  test/test_partition_movements.py
150
150
  test/test_partitioner.py
151
151
  test/test_producer.py
152
- test/test_protocol.py
153
152
  test/test_record_accumulator.py
154
153
  test/test_sender.py
155
154
  test/test_subscription_state.py
@@ -93,7 +93,7 @@ def test_create_describe_delete_acls(kafka_admin_client):
93
93
  def test_describe_configs_broker_resource_returns_configs(kafka_admin_client):
94
94
  """Tests that describe config returns configs for broker
95
95
  """
96
- broker_id = kafka_admin_client._client.cluster._brokers[0].nodeId
96
+ broker_id = kafka_admin_client._client.least_loaded_node()
97
97
  configs = kafka_admin_client.describe_configs([ConfigResource(ConfigResourceType.BROKER, broker_id)])
98
98
 
99
99
  assert len(configs) == 1
@@ -121,7 +121,7 @@ def test_describe_configs_topic_resource_returns_configs(topic, kafka_admin_clie
121
121
  def test_describe_configs_mixed_resources_returns_configs(topic, kafka_admin_client):
122
122
  """Tests that describe config returns configs for mixed resource types (topic + broker)
123
123
  """
124
- broker_id = kafka_admin_client._client.cluster._brokers[0].nodeId
124
+ broker_id = kafka_admin_client._client.least_loaded_node()
125
125
  configs = kafka_admin_client.describe_configs([
126
126
  ConfigResource(ConfigResourceType.TOPIC, topic),
127
127
  ConfigResource(ConfigResourceType.BROKER, broker_id)])
@@ -146,14 +146,6 @@ def test_describe_configs_invalid_broker_id_raises(kafka_admin_client):
146
146
  kafka_admin_client.describe_configs([ConfigResource(ConfigResourceType.BROKER, broker_id)])
147
147
 
148
148
 
149
- @pytest.mark.skipif(env_kafka_version() < (0, 11), reason='Describe consumer group requires broker >=0.11')
150
- def test_describe_consumer_group_does_not_exist(kafka_admin_client):
151
- """Tests that the describe consumer group call fails if the group coordinator is not available
152
- """
153
- with pytest.raises(CoordinatorNotAvailableError):
154
- kafka_admin_client.describe_consumer_groups(['test'])
155
-
156
-
157
149
  @pytest.mark.skipif(env_kafka_version() < (0, 11), reason='Describe consumer group requires broker >=0.11')
158
150
  def test_describe_consumer_group_exists(kafka_admin_client, kafka_consumer_factory, topic):
159
151
  """Tests that the describe consumer group call returns valid consumer group information
@@ -323,4 +323,4 @@ def test_kafka_consumer_position_after_seek_to_end(kafka_consumer_factory, topic
323
323
  position = consumer.position(tp, timeout_ms=1000)
324
324
 
325
325
  # Verify we got the expected position
326
- assert position == 10, f"Expected position 10, got {position}"
326
+ assert position == 10, "Expected position 10, got {}".format(position)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  import uuid
3
4
  import time
4
5
 
@@ -8,6 +9,7 @@ from kafka.admin import NewTopic
8
9
  from kafka.protocol.metadata import MetadataRequest_v1
9
10
  from test.testutil import assert_message_count, env_kafka_version, random_string, special_to_underscore
10
11
 
12
+ pytestmark = pytest.mark.skipif("KAFKA_URI" in os.environ, reason="Testing on external Kafka Broker")
11
13
 
12
14
  @pytest.fixture(
13
15
  params=[
@@ -124,3 +124,14 @@ def test_zstd():
124
124
  b1 = random_string(100).encode('utf-8')
125
125
  b2 = zstd_decode(zstd_encode(b1))
126
126
  assert b1 == b2
127
+
128
+
129
+ @pytest.mark.skipif(not has_zstd(), reason="Zstd not available")
130
+ def test_zstd_multi_frame():
131
+ """Test that zstd_decode handles multiple concatenated zstd frames."""
132
+ frame1_data = b'some payload data ' * 100
133
+ frame2_data = b'another frame of data ' * 100
134
+ # Concatenate two independently compressed zstd frames
135
+ multi_frame_payload = zstd_encode(frame1_data) + zstd_encode(frame2_data)
136
+ result = zstd_decode(multi_frame_payload)
137
+ assert result == frame1_data + frame2_data
@@ -770,3 +770,190 @@ def test_seek_before_exception(client, mocker):
770
770
  # Should not throw OffsetOutOfRangeError after the seek
771
771
  records, partial = fetcher.fetched_records()
772
772
  assert len(records) == 0
773
+
774
+
775
+ class TestFetchOffsetsByTimes:
776
+ def _make_fetcher(self, client, mocker):
777
+ subscription_state = SubscriptionState()
778
+ subscription_state.subscribe(topics=['test'])
779
+ tp = TopicPartition('test', 0)
780
+ subscription_state.assign_from_subscribed([tp])
781
+ subscription_state.seek(tp, 0)
782
+ return Fetcher(client, subscription_state)
783
+
784
+ def test_empty_timestamps(self, client, metrics, mocker):
785
+ fetcher = self._make_fetcher(client, mocker)
786
+ assert fetcher._fetch_offsets_by_times({}) == {}
787
+
788
+ def test_success_no_retry(self, client, mocker):
789
+ fetcher = self._make_fetcher(client, mocker)
790
+ tp = TopicPartition('test', 0)
791
+ timestamps = {tp: 1000}
792
+ expected_offset = OffsetAndTimestamp(10, 1000, -1)
793
+
794
+ future = Future()
795
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', return_value=future)
796
+ mocker.patch.object(fetcher._client, 'poll', side_effect=lambda **kw: future.success(({tp: expected_offset}, set())))
797
+
798
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
799
+ assert result == {tp: expected_offset}
800
+
801
+ def test_success_with_retry(self, client, mocker):
802
+ fetcher = self._make_fetcher(client, mocker)
803
+ tp0 = TopicPartition('test', 0)
804
+ tp1 = TopicPartition('test', 1)
805
+ timestamps = {tp0: 1000, tp1: 2000}
806
+ offset0 = OffsetAndTimestamp(10, 1000, -1)
807
+ offset1 = OffsetAndTimestamp(20, 2000, -1)
808
+
809
+ # First call succeeds for tp0 but needs retry for tp1
810
+ future1 = Future()
811
+ future2 = Future()
812
+ futures = iter([future1, future2])
813
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', side_effect=lambda ts: next(futures))
814
+
815
+ def poll_side_effect(**kw):
816
+ f = kw.get('future')
817
+ if f is future1:
818
+ f.success(({tp0: offset0}, {tp1}))
819
+ elif f is future2:
820
+ f.success(({tp1: offset1}, set()))
821
+
822
+ mocker.patch.object(fetcher._client, 'poll', side_effect=poll_side_effect)
823
+
824
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
825
+ assert result == {tp0: offset0, tp1: offset1}
826
+
827
+ def test_timeout_raises(self, client, mocker):
828
+ fetcher = self._make_fetcher(client, mocker)
829
+ tp = TopicPartition('test', 0)
830
+ timestamps = {tp: 1000}
831
+
832
+ future = Future()
833
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', return_value=future)
834
+ # poll does not complete the future
835
+ mocker.patch.object(fetcher._client, 'poll')
836
+
837
+ with pytest.raises(Errors.KafkaTimeoutError):
838
+ fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
839
+
840
+ def test_non_retriable_error_raises(self, client, mocker):
841
+ fetcher = self._make_fetcher(client, mocker)
842
+ tp = TopicPartition('test', 0)
843
+ timestamps = {tp: 1000}
844
+
845
+ future = Future()
846
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', return_value=future)
847
+ # AuthorizationError is not retriable
848
+ error = Errors.TopicAuthorizationFailedError()
849
+ mocker.patch.object(fetcher._client, 'poll', side_effect=lambda **kw: future.failure(error))
850
+
851
+ with pytest.raises(Errors.TopicAuthorizationFailedError):
852
+ fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
853
+
854
+ def test_retriable_invalid_metadata_triggers_refresh(self, client, mocker):
855
+ fetcher = self._make_fetcher(client, mocker)
856
+ tp = TopicPartition('test', 0)
857
+ timestamps = {tp: 1000}
858
+ expected_offset = OffsetAndTimestamp(10, 1000, -1)
859
+
860
+ # First call fails with invalid_metadata error, second succeeds
861
+ future1 = Future()
862
+ future2 = Future()
863
+ futures = iter([future1, future2])
864
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', side_effect=lambda ts: next(futures))
865
+
866
+ refresh_future = Future()
867
+ mocker.patch.object(fetcher._client.cluster, 'request_update', return_value=refresh_future)
868
+
869
+ call_count = [0]
870
+ def poll_side_effect(**kw):
871
+ f = kw.get('future')
872
+ if f is future1:
873
+ f.failure(NotLeaderForPartitionError())
874
+ elif f is refresh_future:
875
+ refresh_future.success(None)
876
+ elif f is future2:
877
+ f.success(({tp: expected_offset}, set()))
878
+ call_count[0] += 1
879
+
880
+ mocker.patch.object(fetcher._client, 'poll', side_effect=poll_side_effect)
881
+
882
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
883
+ assert result == {tp: expected_offset}
884
+ fetcher._client.cluster.request_update.assert_called_once()
885
+
886
+ def test_retriable_non_metadata_error_sleeps(self, client, mocker):
887
+ fetcher = self._make_fetcher(client, mocker)
888
+ tp = TopicPartition('test', 0)
889
+ timestamps = {tp: 1000}
890
+ expected_offset = OffsetAndTimestamp(10, 1000, -1)
891
+
892
+ # RequestTimedOutError is retriable but not invalid_metadata
893
+ future1 = Future()
894
+ future2 = Future()
895
+ futures = iter([future1, future2])
896
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', side_effect=lambda ts: next(futures))
897
+
898
+ # Ensure cluster does not need update
899
+ mocker.patch.object(type(fetcher._client.cluster), 'need_update', new_callable=mocker.PropertyMock, return_value=False)
900
+
901
+ def poll_side_effect(**kw):
902
+ f = kw.get('future')
903
+ if f is future1:
904
+ f.failure(Errors.RequestTimedOutError())
905
+ elif f is future2:
906
+ f.success(({tp: expected_offset}, set()))
907
+
908
+ mocker.patch.object(fetcher._client, 'poll', side_effect=poll_side_effect)
909
+ mock_sleep = mocker.patch('time.sleep')
910
+
911
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
912
+ assert result == {tp: expected_offset}
913
+ mock_sleep.assert_called_once()
914
+
915
+ def test_success_does_not_check_exception(self, client, mocker):
916
+ """Regression: successful future should not fall through to check future.exception."""
917
+ fetcher = self._make_fetcher(client, mocker)
918
+ tp0 = TopicPartition('test', 0)
919
+ tp1 = TopicPartition('test', 1)
920
+ timestamps = {tp0: 1000, tp1: 2000}
921
+ offset0 = OffsetAndTimestamp(10, 1000, -1)
922
+ offset1 = OffsetAndTimestamp(20, 2000, -1)
923
+
924
+ future1 = Future()
925
+ future2 = Future()
926
+ futures = iter([future1, future2])
927
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', side_effect=lambda ts: next(futures))
928
+
929
+ def poll_side_effect(**kw):
930
+ f = kw.get('future')
931
+ if f is future1:
932
+ # Succeeds but has retry partitions -- the bug was that code
933
+ # would fall through to check future.exception (which is None),
934
+ # causing an AttributeError
935
+ f.success(({tp0: offset0}, {tp1}))
936
+ elif f is future2:
937
+ f.success(({tp1: offset1}, set()))
938
+
939
+ mocker.patch.object(fetcher._client, 'poll', side_effect=poll_side_effect)
940
+
941
+ # Should not raise AttributeError
942
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=10000)
943
+ assert result == {tp0: offset0, tp1: offset1}
944
+
945
+ def test_no_timeout_passes_none(self, client, mocker):
946
+ fetcher = self._make_fetcher(client, mocker)
947
+ tp = TopicPartition('test', 0)
948
+ timestamps = {tp: 1000}
949
+ expected_offset = OffsetAndTimestamp(10, 1000, -1)
950
+
951
+ future = Future()
952
+ mocker.patch.object(fetcher, '_send_list_offsets_requests', return_value=future)
953
+ mocker.patch.object(fetcher._client, 'poll', side_effect=lambda **kw: future.success(({tp: expected_offset}, set())))
954
+
955
+ result = fetcher._fetch_offsets_by_times(timestamps, timeout_ms=None)
956
+ assert result == {tp: expected_offset}
957
+ # With timeout_ms=None, poll should receive None timeout
958
+ fetcher._client.poll.assert_called_once()
959
+ assert fetcher._client.poll.call_args[1]['timeout_ms'] is None
@@ -120,6 +120,22 @@ def test_batch_cannot_complete_twice():
120
120
  assert record_metadata.offset == 500
121
121
  assert record_metadata.timestamp == 10
122
122
 
123
+ def test_producer_batch_lt(tp):
124
+ records = MemoryRecordsBuilder(
125
+ magic=2, compression_type=0, batch_size=100000)
126
+ b1 = ProducerBatch(tp, records, now=1)
127
+ b2 = ProducerBatch(tp, records, now=2)
128
+
129
+ assert b1 < b2
130
+ assert not b1 < b1
131
+
132
+ import heapq
133
+ q = []
134
+ heapq.heappush(q, b2)
135
+ heapq.heappush(q, b1)
136
+ assert q[0] == b1
137
+ assert q[1] == b2
138
+
123
139
  def test_linger(tp, cluster):
124
140
  now = 0
125
141
  accum = RecordAccumulator(linger_ms=10)
@@ -1 +0,0 @@
1
- __version__ = '2.2.17'