kafka-python 2.1.5__tar.gz → 2.2.1__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 (170) hide show
  1. {kafka_python-2.1.5 → kafka_python-2.2.1}/CHANGES.md +55 -0
  2. {kafka_python-2.1.5 → kafka_python-2.2.1}/PKG-INFO +24 -1
  3. {kafka_python-2.1.5 → kafka_python-2.2.1}/README.rst +23 -0
  4. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/client.py +6 -6
  5. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/client_async.py +40 -2
  6. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/cluster.py +18 -13
  7. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/conn.py +15 -5
  8. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/consumer/fetcher.py +290 -187
  9. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/consumer/group.py +56 -54
  10. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/consumer/subscription_state.py +79 -35
  11. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/base.py +37 -19
  12. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/consumer.py +22 -31
  13. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/errors.py +68 -93
  14. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/producer/future.py +3 -3
  15. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/producer/kafka.py +265 -30
  16. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/producer/record_accumulator.py +293 -187
  17. kafka_python-2.2.1/kafka/producer/sender.py +762 -0
  18. kafka_python-2.2.1/kafka/producer/transaction_manager.py +981 -0
  19. kafka_python-2.2.1/kafka/protocol/add_offsets_to_txn.py +59 -0
  20. kafka_python-2.2.1/kafka/protocol/add_partitions_to_txn.py +63 -0
  21. kafka_python-2.2.1/kafka/protocol/end_txn.py +58 -0
  22. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/fetch.py +6 -0
  23. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/group.py +17 -3
  24. kafka_python-2.2.1/kafka/protocol/init_producer_id.py +46 -0
  25. kafka_python-2.2.1/kafka/protocol/txn_offset_commit.py +78 -0
  26. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/abc.py +10 -0
  27. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/default_records.py +101 -12
  28. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/legacy_records.py +12 -3
  29. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/memory_records.py +54 -6
  30. kafka_python-2.2.1/kafka/version.py +1 -0
  31. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka_python.egg-info/PKG-INFO +24 -1
  32. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka_python.egg-info/SOURCES.txt +16 -5
  33. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka_python.egg-info/top_level.txt +1 -0
  34. kafka_python-2.2.1/test/integration/__init__.py +0 -0
  35. kafka_python-2.2.1/test/integration/conftest.py +168 -0
  36. kafka_python-2.2.1/test/integration/fixtures.py +765 -0
  37. {kafka_python-2.1.5/test → kafka_python-2.2.1/test/integration}/test_admin_integration.py +2 -2
  38. {kafka_python-2.1.5/test → kafka_python-2.2.1/test/integration}/test_consumer_integration.py +3 -2
  39. kafka_python-2.1.5/test/test_producer.py → kafka_python-2.2.1/test/integration/test_producer_integration.py +78 -18
  40. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_client_async.py +135 -136
  41. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_coordinator.py +21 -23
  42. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_fetcher.py +211 -67
  43. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_metrics.py +2 -18
  44. kafka_python-2.2.1/test/test_producer.py +35 -0
  45. kafka_python-2.2.1/test/test_record_accumulator.py +266 -0
  46. kafka_python-2.2.1/test/test_sender.py +242 -0
  47. kafka_python-2.1.5/kafka/producer/sender.py +0 -489
  48. kafka_python-2.1.5/kafka/version.py +0 -1
  49. kafka_python-2.1.5/test/test_sender.py +0 -39
  50. {kafka_python-2.1.5 → kafka_python-2.2.1}/AUTHORS.md +0 -0
  51. {kafka_python-2.1.5 → kafka_python-2.2.1}/LICENSE +0 -0
  52. {kafka_python-2.1.5 → kafka_python-2.2.1}/MANIFEST.in +0 -0
  53. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/__init__.py +0 -0
  54. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/__init__.py +0 -0
  55. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/acl_resource.py +0 -0
  56. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/config_resource.py +0 -0
  57. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/new_partitions.py +0 -0
  58. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/admin/new_topic.py +0 -0
  59. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/__init__.py +0 -0
  60. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/consumer_performance.py +0 -0
  61. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/load_example.py +0 -0
  62. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/producer_performance.py +0 -0
  63. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/record_batch_compose.py +0 -0
  64. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/record_batch_read.py +0 -0
  65. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/benchmarks/varint_speed.py +0 -0
  66. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/codec.py +0 -0
  67. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/consumer/__init__.py +0 -0
  68. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/__init__.py +0 -0
  69. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/__init__.py +0 -0
  70. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/abstract.py +0 -0
  71. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/range.py +0 -0
  72. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/roundrobin.py +0 -0
  73. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  74. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  75. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  76. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  77. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/heartbeat.py +0 -0
  78. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/coordinator/protocol.py +0 -0
  79. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/future.py +0 -0
  80. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/__init__.py +0 -0
  81. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/compound_stat.py +0 -0
  82. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/dict_reporter.py +0 -0
  83. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/kafka_metric.py +0 -0
  84. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/measurable.py +0 -0
  85. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/measurable_stat.py +0 -0
  86. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/metric_config.py +0 -0
  87. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/metric_name.py +0 -0
  88. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/metrics.py +0 -0
  89. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/metrics_reporter.py +0 -0
  90. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/quota.py +0 -0
  91. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stat.py +0 -0
  92. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/__init__.py +0 -0
  93. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/avg.py +0 -0
  94. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/count.py +0 -0
  95. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/histogram.py +0 -0
  96. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/max_stat.py +0 -0
  97. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/min_stat.py +0 -0
  98. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/percentile.py +0 -0
  99. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/percentiles.py +0 -0
  100. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/rate.py +0 -0
  101. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/sampled_stat.py +0 -0
  102. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/sensor.py +0 -0
  103. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/metrics/stats/total.py +0 -0
  104. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/partitioner/__init__.py +0 -0
  105. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/partitioner/default.py +0 -0
  106. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/producer/__init__.py +0 -0
  107. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/__init__.py +0 -0
  108. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/abstract.py +0 -0
  109. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/admin.py +0 -0
  110. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/api.py +0 -0
  111. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/api_versions.py +0 -0
  112. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/broker_api_versions.py +0 -0
  113. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/commit.py +0 -0
  114. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/find_coordinator.py +0 -0
  115. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/frame.py +0 -0
  116. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/list_offsets.py +0 -0
  117. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/message.py +0 -0
  118. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/metadata.py +0 -0
  119. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/offset_for_leader_epoch.py +0 -0
  120. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/parser.py +0 -0
  121. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/pickle.py +0 -0
  122. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/produce.py +0 -0
  123. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/sasl_authenticate.py +0 -0
  124. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/sasl_handshake.py +0 -0
  125. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/struct.py +0 -0
  126. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/protocol/types.py +0 -0
  127. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/__init__.py +0 -0
  128. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/_crc32c.py +0 -0
  129. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/record/util.py +0 -0
  130. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/__init__.py +0 -0
  131. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/abc.py +0 -0
  132. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/gssapi.py +0 -0
  133. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/msk.py +0 -0
  134. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/oauth.py +0 -0
  135. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/plain.py +0 -0
  136. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/scram.py +0 -0
  137. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/sasl/sspi.py +0 -0
  138. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/serializer/__init__.py +0 -0
  139. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/serializer/abstract.py +0 -0
  140. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/socks5_wrapper.py +0 -0
  141. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/structs.py +0 -0
  142. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/util.py +0 -0
  143. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/vendor/__init__.py +0 -0
  144. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/vendor/enum34.py +0 -0
  145. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/vendor/selectors34.py +0 -0
  146. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/vendor/six.py +0 -0
  147. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka/vendor/socketpair.py +0 -0
  148. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka_python.egg-info/dependency_links.txt +0 -0
  149. {kafka_python-2.1.5 → kafka_python-2.2.1}/kafka_python.egg-info/requires.txt +0 -0
  150. {kafka_python-2.1.5 → kafka_python-2.2.1}/pyproject.toml +0 -0
  151. {kafka_python-2.1.5 → kafka_python-2.2.1}/setup.cfg +0 -0
  152. {kafka_python-2.1.5 → kafka_python-2.2.1}/setup.py +0 -0
  153. {kafka_python-2.1.5/test → kafka_python-2.2.1/test/integration}/test_consumer_group.py +0 -0
  154. {kafka_python-2.1.5/test → kafka_python-2.2.1/test/integration}/test_sasl_integration.py +0 -0
  155. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_acl_comparisons.py +0 -0
  156. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_admin.py +0 -0
  157. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_api_object_implementation.py +0 -0
  158. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_assignors.py +0 -0
  159. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_cluster.py +0 -0
  160. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_codec.py +0 -0
  161. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_conn.py +0 -0
  162. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_consumer.py +0 -0
  163. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_object_conversion.py +0 -0
  164. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_package.py +0 -0
  165. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_partition_movements.py +0 -0
  166. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_partitioner.py +0 -0
  167. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_protocol.py +0 -0
  168. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_subscription_state.py +0 -0
  169. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/test_util.py +0 -0
  170. {kafka_python-2.1.5 → kafka_python-2.2.1}/test/testutil.py +0 -0
@@ -1,3 +1,58 @@
1
+ # 2.2.1 (Apr 29, 2025)
2
+
3
+ Fixes
4
+ * Always try ApiVersionsRequest v0, even on broker disconnect (#2603)
5
+ * Fix SubscriptionState AttributeError in KafkaConsumer (#2599)
6
+
7
+ Documentation
8
+ * Add transactional examples to docs
9
+
10
+ # 2.2.0 (Apr 28, 2025)
11
+
12
+ KafkaProducer
13
+ * KIP-98: Add idempotent producer support (#2569)
14
+ * KIP-98: Transactional Producer (#2587)
15
+ * KIP-98: Add offsets support to transactional KafkaProducer (#2590)
16
+ * Prefix producer logs w/ client id and transactional id (#2591)
17
+ * KAFKA-5429: Ignore produce response if batch was previously aborted
18
+ * KIP-91: KafkaProducer `delivery_timeout_ms`
19
+ * Default retries -> infinite
20
+ * Expand KafkaProducer docstring w/ idempotent and transactional notes
21
+ * RecordAccumulator: Use helper method to get/set `_tp_locks`; get dq with lock in reenqueue()
22
+
23
+ KafkaConsumer
24
+ * KIP-98: Add Consumer support for `READ_COMMITTED` (#2582)
25
+ * KIP-394: handle `MEMBER_ID_REQUIRED` error w/ second join group request (#2598)
26
+ * KAFKA-5078: Defer fetch record exception if iterator has already moved across a valid record
27
+ * KAFKA-5075: Defer consumer fetcher exception if fetch position has already increased
28
+ * KAFKA-4937: Batch offset fetches in the Consumer
29
+ * KAFKA-4547: Avoid resetting paused partitions to committed offsets
30
+ * KAFKA-6397: Consumer should not block setting positions of unavailable partitions (#2593)
31
+
32
+ Potentially Breaking Changes (internal)
33
+ * Rename CorruptRecordException -> CorruptRecordError
34
+ * Rename Coordinator errors to generic not group (#2585)
35
+ * Rename `ClusterMetadata.add_group_coordinator` -> `add_coordinator` + support txn type
36
+ * Use SaslAuthenticationFailedError in kafka.conn connection failure; Drop unused AuthenticationFailedError
37
+ * Remove old/unused errors; reorder; KafkaTimeout -> retriable
38
+ * Drop `log_start_offset` from producer RecordMetadata
39
+
40
+ Internal
41
+ * MemoryRecords iterator; MemoryRecordsBuilder records() helper
42
+ * Convert `DefaultRecordsBuilder.size_in_bytes` to classmethod
43
+
44
+ Fixes
45
+ * Resolve datetime deprecation warnings (#2589)
46
+ * Avoid self refcount in log messages; test thread close on all pythons
47
+ * Fix client.wakeup() race from producer/sender close
48
+ * Fix ElectionNotNeededError handling in admin client
49
+
50
+ Tests
51
+ * Move integration tests and fixtures to test/integration/; simplify unit fixtures (#2588)
52
+ * Expand Sender test coverage (#2586)
53
+ * py2 test fixups
54
+ * Drop unused KafkaClient import from `test_fetcher`
55
+
1
56
  # 2.1.5 (Apr 4, 2025)
2
57
 
3
58
  Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.1.5
3
+ Version: 2.2.1
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
@@ -138,6 +138,14 @@ that expose basic message attributes: topic, partition, offset, key, and value:
138
138
  for msg in consumer:
139
139
  print (msg.headers)
140
140
 
141
+ .. code-block:: python
142
+
143
+ # Read only committed messages from transactional topic
144
+ consumer = KafkaConsumer(isolation_level='read_committed')
145
+ consumer.subscribe(['txn_topic'])
146
+ for msg in consumer:
147
+ print(msg)
148
+
141
149
  .. code-block:: python
142
150
 
143
151
  # Get consumer metrics
@@ -197,6 +205,21 @@ for more details.
197
205
  for i in range(1000):
198
206
  producer.send('foobar', b'msg %d' % i)
199
207
 
208
+ .. code-block:: python
209
+
210
+ # Use transactions
211
+ producer = KafkaProducer(transactional_id='fizzbuzz')
212
+ producer.init_transactions()
213
+ producer.begin_transaction()
214
+ future = producer.send('txn_topic', value=b'yes')
215
+ future.get() # wait for successful produce
216
+ producer.commit_transaction() # commit the transaction
217
+
218
+ producer.begin_transaction()
219
+ future = producer.send('txn_topic', value=b'no')
220
+ future.get() # wait for successful produce
221
+ producer.abort_transaction() # abort the transaction
222
+
200
223
  .. code-block:: python
201
224
 
202
225
  # Include record headers. The format is list of tuples with string key
@@ -94,6 +94,14 @@ that expose basic message attributes: topic, partition, offset, key, and value:
94
94
  for msg in consumer:
95
95
  print (msg.headers)
96
96
 
97
+ .. code-block:: python
98
+
99
+ # Read only committed messages from transactional topic
100
+ consumer = KafkaConsumer(isolation_level='read_committed')
101
+ consumer.subscribe(['txn_topic'])
102
+ for msg in consumer:
103
+ print(msg)
104
+
97
105
  .. code-block:: python
98
106
 
99
107
  # Get consumer metrics
@@ -153,6 +161,21 @@ for more details.
153
161
  for i in range(1000):
154
162
  producer.send('foobar', b'msg %d' % i)
155
163
 
164
+ .. code-block:: python
165
+
166
+ # Use transactions
167
+ producer = KafkaProducer(transactional_id='fizzbuzz')
168
+ producer.init_transactions()
169
+ producer.begin_transaction()
170
+ future = producer.send('txn_topic', value=b'yes')
171
+ future.get() # wait for successful produce
172
+ producer.commit_transaction() # commit the transaction
173
+
174
+ producer.begin_transaction()
175
+ future = producer.send('txn_topic', value=b'no')
176
+ future.get() # wait for successful produce
177
+ producer.abort_transaction() # abort the transaction
178
+
156
179
  .. code-block:: python
157
180
 
158
181
  # Include record headers. The format is list of tuples with string key
@@ -15,7 +15,7 @@ from kafka.client_async import KafkaClient, selectors
15
15
  from kafka.coordinator.protocol import ConsumerProtocolMemberMetadata, ConsumerProtocolMemberAssignment, ConsumerProtocol
16
16
  import kafka.errors as Errors
17
17
  from kafka.errors import (
18
- IncompatibleBrokerVersion, KafkaConfigurationError, NotControllerError, UnknownTopicOrPartitionError,
18
+ IncompatibleBrokerVersion, KafkaConfigurationError, UnknownTopicOrPartitionError,
19
19
  UnrecognizedBrokerVersion, IllegalArgumentError)
20
20
  from kafka.metrics import MetricConfig, Metrics
21
21
  from kafka.protocol.admin import (
@@ -411,7 +411,7 @@ class KafkaAdminClient(object):
411
411
  # extra values (usually the error_message)
412
412
  for topic, error_code in map(lambda e: e[:2], topic_error_tuples):
413
413
  error_type = Errors.for_code(error_code)
414
- if tries and error_type is NotControllerError:
414
+ if tries and error_type is Errors.NotControllerError:
415
415
  # No need to inspect the rest of the errors for
416
416
  # non-retriable errors because NotControllerError should
417
417
  # either be thrown for all errors or no errors.
@@ -431,13 +431,13 @@ class KafkaAdminClient(object):
431
431
  for topic, partition_results in response.replication_election_results:
432
432
  for partition_id, error_code in map(lambda e: e[:2], partition_results):
433
433
  error_type = Errors.for_code(error_code)
434
- if tries and error_type is NotControllerError:
434
+ if tries and error_type is Errors.NotControllerError:
435
435
  # No need to inspect the rest of the errors for
436
436
  # non-retriable errors because NotControllerError should
437
437
  # either be thrown for all errors or no errors.
438
438
  self._refresh_controller_id()
439
439
  return False
440
- elif error_type not in [Errors.NoError, Errors.ElectionNotNeeded]:
440
+ elif error_type not in (Errors.NoError, Errors.ElectionNotNeededError):
441
441
  raise error_type(
442
442
  "Request '{}' failed with response '{}'."
443
443
  .format(request, response))
@@ -1460,9 +1460,9 @@ class KafkaAdminClient(object):
1460
1460
  list: List of tuples of Consumer Groups.
1461
1461
 
1462
1462
  Raises:
1463
- GroupCoordinatorNotAvailableError: The coordinator is not
1463
+ CoordinatorNotAvailableError: The coordinator is not
1464
1464
  available, so cannot process requests.
1465
- GroupLoadInProgressError: The coordinator is loading and
1465
+ CoordinatorLoadInProgressError: The coordinator is loading and
1466
1466
  hence can't process requests.
1467
1467
  """
1468
1468
  # While we return a list, internally use a set to prevent duplicates
@@ -27,7 +27,7 @@ from kafka.metrics.stats import Avg, Count, Rate
27
27
  from kafka.metrics.stats.rate import TimeUnit
28
28
  from kafka.protocol.broker_api_versions import BROKER_API_VERSIONS
29
29
  from kafka.protocol.metadata import MetadataRequest
30
- from kafka.util import Dict, WeakMethod, ensure_valid_topic_name
30
+ from kafka.util import Dict, WeakMethod, ensure_valid_topic_name, timeout_ms_fn
31
31
  # Although this looks unused, it actually monkey-patches socket.socketpair()
32
32
  # and should be left in as long as we're using socket.socketpair() in this file
33
33
  from kafka.vendor import socketpair # noqa: F401
@@ -400,6 +400,11 @@ class KafkaClient(object):
400
400
  return True
401
401
  return False
402
402
 
403
+ def connection_failed(self, node_id):
404
+ if node_id not in self._conns:
405
+ return False
406
+ return self._conns[node_id].connect_failed()
407
+
403
408
  def _should_recycle_connection(self, conn):
404
409
  # Never recycle unless disconnected
405
410
  if not conn.disconnected():
@@ -1110,7 +1115,7 @@ class KafkaClient(object):
1110
1115
  return version
1111
1116
 
1112
1117
  def wakeup(self):
1113
- if self._waking or self._wake_w is None:
1118
+ if self._closed or self._waking or self._wake_w is None:
1114
1119
  return
1115
1120
  with self._wake_lock:
1116
1121
  try:
@@ -1157,6 +1162,39 @@ class KafkaClient(object):
1157
1162
  else:
1158
1163
  return False
1159
1164
 
1165
+ def await_ready(self, node_id, timeout_ms=30000):
1166
+ """
1167
+ Invokes `poll` to discard pending disconnects, followed by `client.ready` and 0 or more `client.poll`
1168
+ invocations until the connection to `node` is ready, the timeoutMs expires or the connection fails.
1169
+
1170
+ It returns `true` if the call completes normally or `false` if the timeoutMs expires. If the connection fails,
1171
+ an `IOException` is thrown instead. Note that if the `NetworkClient` has been configured with a positive
1172
+ connection timeoutMs, it is possible for this method to raise an `IOException` for a previous connection which
1173
+ has recently disconnected.
1174
+
1175
+ This method is useful for implementing blocking behaviour on top of the non-blocking `NetworkClient`, use it with
1176
+ care.
1177
+ """
1178
+ inner_timeout_ms = timeout_ms_fn(timeout_ms, None)
1179
+ self.poll(timeout_ms=0)
1180
+ if self.is_ready(node_id):
1181
+ return True
1182
+
1183
+ while not self.is_ready(node_id) and inner_timeout_ms() > 0:
1184
+ if self.connection_failed(node_id):
1185
+ raise Errors.KafkaConnectionError("Connection to %s failed." % (node_id,))
1186
+ self.maybe_connect(node_id)
1187
+ self.poll(timeout_ms=inner_timeout_ms())
1188
+ return self.is_ready(node_id)
1189
+
1190
+ def send_and_receive(self, node_id, request):
1191
+ future = self.send(node_id, request)
1192
+ self.poll(future=future)
1193
+ assert future.is_done
1194
+ if future.failed():
1195
+ raise future.exception
1196
+ return future.value
1197
+
1160
1198
 
1161
1199
  # OrderedDict requires python2.7+
1162
1200
  try:
@@ -47,7 +47,7 @@ class ClusterMetadata(object):
47
47
  self._brokers = {} # node_id -> BrokerMetadata
48
48
  self._partitions = {} # topic -> partition -> PartitionMetadata
49
49
  self._broker_partitions = collections.defaultdict(set) # node_id -> {TopicPartition...}
50
- self._groups = {} # group_name -> node_id
50
+ self._coordinators = {} # (coord_type, coord_key) -> node_id
51
51
  self._last_refresh_ms = 0
52
52
  self._last_successful_refresh_ms = 0
53
53
  self._need_update = True
@@ -167,7 +167,7 @@ class ClusterMetadata(object):
167
167
  node_id (int or str) for group coordinator, -1 if coordinator unknown
168
168
  None if the group does not exist.
169
169
  """
170
- return self._groups.get(group)
170
+ return self._coordinators.get(('group', group))
171
171
 
172
172
  def ttl(self):
173
173
  """Milliseconds until metadata should be refreshed"""
@@ -202,6 +202,10 @@ class ClusterMetadata(object):
202
202
  self._future = Future()
203
203
  return self._future
204
204
 
205
+ @property
206
+ def need_update(self):
207
+ return self._need_update
208
+
205
209
  def topics(self, exclude_internal_topics=True):
206
210
  """Get set of known topics.
207
211
 
@@ -364,24 +368,25 @@ class ClusterMetadata(object):
364
368
  """Remove a previously added listener callback"""
365
369
  self._listeners.remove(listener)
366
370
 
367
- def add_group_coordinator(self, group, response):
368
- """Update with metadata for a group coordinator
371
+ def add_coordinator(self, response, coord_type, coord_key):
372
+ """Update with metadata for a group or txn coordinator
369
373
 
370
374
  Arguments:
371
- group (str): name of group from FindCoordinatorRequest
372
375
  response (FindCoordinatorResponse): broker response
376
+ coord_type (str): 'group' or 'transaction'
377
+ coord_key (str): consumer_group or transactional_id
373
378
 
374
379
  Returns:
375
380
  string: coordinator node_id if metadata is updated, None on error
376
381
  """
377
- log.debug("Updating coordinator for %s: %s", group, response)
382
+ log.debug("Updating coordinator for %s/%s: %s", coord_type, coord_key, response)
378
383
  error_type = Errors.for_code(response.error_code)
379
384
  if error_type is not Errors.NoError:
380
385
  log.error("FindCoordinatorResponse error: %s", error_type)
381
- self._groups[group] = -1
386
+ self._coordinators[(coord_type, coord_key)] = -1
382
387
  return
383
388
 
384
- # Use a coordinator-specific node id so that group requests
389
+ # Use a coordinator-specific node id so that requests
385
390
  # get a dedicated connection
386
391
  node_id = 'coordinator-{}'.format(response.coordinator_id)
387
392
  coordinator = BrokerMetadata(
@@ -390,9 +395,9 @@ class ClusterMetadata(object):
390
395
  response.port,
391
396
  None)
392
397
 
393
- log.info("Group coordinator for %s is %s", group, coordinator)
398
+ log.info("Coordinator for %s/%s is %s", coord_type, coord_key, coordinator)
394
399
  self._coordinator_brokers[node_id] = coordinator
395
- self._groups[group] = node_id
400
+ self._coordinators[(coord_type, coord_key)] = node_id
396
401
  return node_id
397
402
 
398
403
  def with_partitions(self, partitions_to_add):
@@ -401,7 +406,7 @@ class ClusterMetadata(object):
401
406
  new_metadata._brokers = copy.deepcopy(self._brokers)
402
407
  new_metadata._partitions = copy.deepcopy(self._partitions)
403
408
  new_metadata._broker_partitions = copy.deepcopy(self._broker_partitions)
404
- new_metadata._groups = copy.deepcopy(self._groups)
409
+ new_metadata._coordinators = copy.deepcopy(self._coordinators)
405
410
  new_metadata.internal_topics = copy.deepcopy(self.internal_topics)
406
411
  new_metadata.unauthorized_topics = copy.deepcopy(self.unauthorized_topics)
407
412
 
@@ -415,5 +420,5 @@ class ClusterMetadata(object):
415
420
  return new_metadata
416
421
 
417
422
  def __str__(self):
418
- return 'ClusterMetadata(brokers: %d, topics: %d, groups: %d)' % \
419
- (len(self._brokers), len(self._partitions), len(self._groups))
423
+ return 'ClusterMetadata(brokers: %d, topics: %d, coordinators: %d)' % \
424
+ (len(self._brokers), len(self._partitions), len(self._coordinators))
@@ -301,6 +301,7 @@ class BrokerConnection(object):
301
301
  if self.config['ssl_context'] is not None:
302
302
  self._ssl_context = self.config['ssl_context']
303
303
  self._api_versions_future = None
304
+ self._api_versions_check_timeout = self.config['api_version_auto_timeout_ms']
304
305
  self._sasl_auth_future = None
305
306
  self.last_attempt = 0
306
307
  self._gai = []
@@ -557,7 +558,8 @@ class BrokerConnection(object):
557
558
  else:
558
559
  request = ApiVersionsRequest[version]()
559
560
  future = Future()
560
- response = self._send(request, blocking=True, request_timeout_ms=(self.config['api_version_auto_timeout_ms'] * 0.8))
561
+ self._api_versions_check_timeout /= 2
562
+ response = self._send(request, blocking=True, request_timeout_ms=self._api_versions_check_timeout)
561
563
  response.add_callback(self._handle_api_versions_response, future)
562
564
  response.add_errback(self._handle_api_versions_failure, future)
563
565
  self._api_versions_future = future
@@ -566,7 +568,8 @@ class BrokerConnection(object):
566
568
  elif self._check_version_idx < len(self.VERSION_CHECKS):
567
569
  version, request = self.VERSION_CHECKS[self._check_version_idx]
568
570
  future = Future()
569
- response = self._send(request, blocking=True, request_timeout_ms=(self.config['api_version_auto_timeout_ms'] * 0.8))
571
+ self._api_versions_check_timeout /= 2
572
+ response = self._send(request, blocking=True, request_timeout_ms=self._api_versions_check_timeout)
570
573
  response.add_callback(self._handle_check_version_response, future, version)
571
574
  response.add_errback(self._handle_check_version_failure, future)
572
575
  self._api_versions_future = future
@@ -618,7 +621,13 @@ class BrokerConnection(object):
618
621
 
619
622
  def _handle_api_versions_failure(self, future, ex):
620
623
  future.failure(ex)
621
- self._check_version_idx = 0
624
+ # Modern brokers should not disconnect on unrecognized api-versions request,
625
+ # but in case they do we always want to try v0 as a fallback
626
+ # otherwise switch to check_version probe.
627
+ if self._api_versions_idx > 0:
628
+ self._api_versions_idx = 0
629
+ else:
630
+ self._check_version_idx = 0
622
631
  # after failure connection is closed, so state should already be DISCONNECTED
623
632
 
624
633
  def _handle_check_version_response(self, future, version, _response):
@@ -813,7 +822,7 @@ class BrokerConnection(object):
813
822
  log.info('%s: %s', self, self._sasl_mechanism.auth_details())
814
823
  return future.success(True)
815
824
  else:
816
- return future.failure(Errors.AuthenticationFailedError('Failed to authenticate via SASL %s' % self.config['sasl_mechanism']))
825
+ return future.failure(Errors.SaslAuthenticationFailedError('Failed to authenticate via SASL %s' % self.config['sasl_mechanism']))
817
826
 
818
827
  def blacked_out(self):
819
828
  """
@@ -934,7 +943,8 @@ class BrokerConnection(object):
934
943
  if self.state is ConnectionStates.DISCONNECTED:
935
944
  return
936
945
  log.log(logging.ERROR if error else logging.INFO, '%s: Closing connection. %s', self, error or '')
937
- self._update_reconnect_backoff()
946
+ if error:
947
+ self._update_reconnect_backoff()
938
948
  self._api_versions_future = None
939
949
  self._sasl_auth_future = None
940
950
  self._init_sasl_mechanism()