kafka-python 2.1.2__tar.gz → 2.1.4__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 (152) hide show
  1. {kafka_python-2.1.2 → kafka_python-2.1.4}/CHANGES.md +35 -0
  2. {kafka_python-2.1.2 → kafka_python-2.1.4}/PKG-INFO +3 -2
  3. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/client.py +1 -1
  4. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/client_async.py +12 -3
  5. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/cluster.py +1 -0
  6. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/conn.py +54 -29
  7. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/consumer/fetcher.py +87 -60
  8. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/consumer/group.py +18 -6
  9. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/consumer/subscription_state.py +68 -56
  10. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/base.py +11 -9
  11. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/consumer.py +12 -5
  12. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/kafka.py +5 -0
  13. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/admin.py +2 -1
  14. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/api.py +8 -6
  15. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/api_versions.py +45 -1
  16. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/broker_api_versions.py +2 -0
  17. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/metadata.py +1 -0
  18. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/parser.py +7 -6
  19. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/oauth.py +15 -2
  20. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/util.py +24 -0
  21. kafka_python-2.1.4/kafka/version.py +1 -0
  22. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka_python.egg-info/PKG-INFO +3 -2
  23. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka_python.egg-info/SOURCES.txt +1 -0
  24. {kafka_python-2.1.2 → kafka_python-2.1.4}/pyproject.toml +1 -0
  25. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_client_async.py +3 -2
  26. kafka_python-2.1.4/test/test_consumer.py +52 -0
  27. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_consumer_group.py +7 -6
  28. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_consumer_integration.py +2 -2
  29. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_coordinator.py +1 -0
  30. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_fetcher.py +8 -6
  31. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_producer.py +1 -1
  32. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_sasl_integration.py +1 -1
  33. kafka_python-2.1.4/test/test_subscription_state.py +57 -0
  34. kafka_python-2.1.2/test/test_subscription_state.py → kafka_python-2.1.4/test/test_util.py +2 -3
  35. kafka_python-2.1.2/kafka/version.py +0 -1
  36. kafka_python-2.1.2/test/test_consumer.py +0 -26
  37. {kafka_python-2.1.2 → kafka_python-2.1.4}/AUTHORS.md +0 -0
  38. {kafka_python-2.1.2 → kafka_python-2.1.4}/LICENSE +0 -0
  39. {kafka_python-2.1.2 → kafka_python-2.1.4}/MANIFEST.in +0 -0
  40. {kafka_python-2.1.2 → kafka_python-2.1.4}/README.rst +0 -0
  41. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/__init__.py +0 -0
  42. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/__init__.py +0 -0
  43. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/acl_resource.py +0 -0
  44. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/config_resource.py +0 -0
  45. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/new_partitions.py +0 -0
  46. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/admin/new_topic.py +0 -0
  47. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/codec.py +0 -0
  48. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/consumer/__init__.py +0 -0
  49. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/__init__.py +0 -0
  50. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/__init__.py +0 -0
  51. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/abstract.py +0 -0
  52. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/range.py +0 -0
  53. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/roundrobin.py +0 -0
  54. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  55. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  56. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  57. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -0
  58. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/heartbeat.py +0 -0
  59. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/coordinator/protocol.py +0 -0
  60. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/errors.py +0 -0
  61. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/future.py +0 -0
  62. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/__init__.py +0 -0
  63. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/compound_stat.py +0 -0
  64. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/dict_reporter.py +0 -0
  65. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/kafka_metric.py +0 -0
  66. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/measurable.py +0 -0
  67. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/measurable_stat.py +0 -0
  68. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/metric_config.py +0 -0
  69. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/metric_name.py +0 -0
  70. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/metrics.py +0 -0
  71. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/metrics_reporter.py +0 -0
  72. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/quota.py +0 -0
  73. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stat.py +0 -0
  74. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/__init__.py +0 -0
  75. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/avg.py +0 -0
  76. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/count.py +0 -0
  77. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/histogram.py +0 -0
  78. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/max_stat.py +0 -0
  79. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/min_stat.py +0 -0
  80. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/percentile.py +0 -0
  81. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/percentiles.py +0 -0
  82. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/rate.py +0 -0
  83. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/sampled_stat.py +0 -0
  84. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/sensor.py +0 -0
  85. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/metrics/stats/total.py +0 -0
  86. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/partitioner/__init__.py +0 -0
  87. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/partitioner/default.py +0 -0
  88. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/__init__.py +0 -0
  89. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/buffer.py +0 -0
  90. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/future.py +0 -0
  91. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/record_accumulator.py +0 -0
  92. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/producer/sender.py +0 -0
  93. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/__init__.py +0 -0
  94. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/abstract.py +0 -0
  95. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/commit.py +0 -0
  96. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/fetch.py +0 -0
  97. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/find_coordinator.py +0 -0
  98. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/frame.py +0 -0
  99. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/group.py +0 -0
  100. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/list_offsets.py +0 -0
  101. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/message.py +0 -0
  102. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/offset_for_leader_epoch.py +0 -0
  103. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/pickle.py +0 -0
  104. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/produce.py +0 -0
  105. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/sasl_authenticate.py +0 -0
  106. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/sasl_handshake.py +0 -0
  107. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/struct.py +0 -0
  108. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/protocol/types.py +0 -0
  109. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/__init__.py +0 -0
  110. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/_crc32c.py +0 -0
  111. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/abc.py +0 -0
  112. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/default_records.py +0 -0
  113. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/legacy_records.py +0 -0
  114. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/memory_records.py +0 -0
  115. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/record/util.py +0 -0
  116. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/__init__.py +0 -0
  117. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/abc.py +0 -0
  118. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/gssapi.py +0 -0
  119. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/msk.py +0 -0
  120. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/plain.py +0 -0
  121. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/scram.py +0 -0
  122. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/sasl/sspi.py +0 -0
  123. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/serializer/__init__.py +0 -0
  124. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/serializer/abstract.py +0 -0
  125. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/socks5_wrapper.py +0 -0
  126. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/structs.py +0 -0
  127. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/vendor/__init__.py +0 -0
  128. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/vendor/enum34.py +0 -0
  129. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/vendor/selectors34.py +0 -0
  130. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/vendor/six.py +0 -0
  131. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka/vendor/socketpair.py +0 -0
  132. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka_python.egg-info/dependency_links.txt +0 -0
  133. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka_python.egg-info/requires.txt +0 -0
  134. {kafka_python-2.1.2 → kafka_python-2.1.4}/kafka_python.egg-info/top_level.txt +0 -0
  135. {kafka_python-2.1.2 → kafka_python-2.1.4}/setup.cfg +0 -0
  136. {kafka_python-2.1.2 → kafka_python-2.1.4}/setup.py +0 -0
  137. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_acl_comparisons.py +0 -0
  138. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_admin.py +0 -0
  139. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_admin_integration.py +0 -0
  140. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_api_object_implementation.py +0 -0
  141. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_assignors.py +0 -0
  142. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_cluster.py +0 -0
  143. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_codec.py +0 -0
  144. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_conn.py +0 -0
  145. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_metrics.py +0 -0
  146. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_object_conversion.py +0 -0
  147. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_package.py +0 -0
  148. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_partition_movements.py +0 -0
  149. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_partitioner.py +0 -0
  150. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_protocol.py +0 -0
  151. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/test_sender.py +0 -0
  152. {kafka_python-2.1.2 → kafka_python-2.1.4}/test/testutil.py +0 -0
@@ -1,3 +1,38 @@
1
+ # 2.1.4 (Mar 28, 2025)
2
+
3
+ Fixes
4
+ * Dont block pending FetchRequests when Metadata update requested (#2576)
5
+ * Fix MetadataRequest for no topics (#2573)
6
+ * Send final error byte x01 on Sasl OAuth failure (#2572)
7
+ * Reset SASL state on disconnect (#2571)
8
+ * Try import new Sequence before old to avoid DeprecationWarning
9
+
10
+ Improvements
11
+ * Update Makefile default to 4.0 broker; add make fixture
12
+ * Improve connection state logging (#2574)
13
+
14
+ # 2.1.3 (Mar 25, 2025)
15
+
16
+ Fixes
17
+ * Fix crash when switching to closest compatible api_version in KafkaClient (#2567)
18
+ * Fix maximum version to send an OffsetFetchRequest in KafkaAdminClient (#2563)
19
+ * Return empty set from consumer.partitions_for_topic when topic not found (#2556)
20
+
21
+ Improvements
22
+ * KIP-511: Use ApiVersions v4 on initial connect w/ client_software_name + version (#2558)
23
+ * KIP-74: Manage assigned partition order in consumer (#2562)
24
+ * KIP-70: Auto-commit offsets on consumer.unsubscribe(), defer assignment changes to rejoin (#2560)
25
+ * Use SubscriptionType to track topics/pattern/user assignment (#2565)
26
+ * Add optional timeout_ms kwarg to consumer.close() (#2564)
27
+ * Move ensure_valid_topic_name to kafka.util; use in client and producer (#2561)
28
+
29
+ Testing
30
+ * Support KRaft / 4.0 brokers in tests (#2559)
31
+ * Test older pythons against 4.0 broker
32
+
33
+ Compatibility
34
+ * Add python 3.13 to compatibility list
35
+
1
36
  # 2.1.2 (Mar 17, 2025)
2
37
 
3
38
  Fixes
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: kafka-python
3
- Version: 2.1.2
3
+ Version: 2.1.4
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
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
22
  Classifier: Programming Language :: Python :: 3.11
23
23
  Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
24
25
  Classifier: Programming Language :: Python :: Implementation :: CPython
25
26
  Classifier: Programming Language :: Python :: Implementation :: PyPy
26
27
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
@@ -1496,7 +1496,7 @@ class KafkaAdminClient(object):
1496
1496
  A message future
1497
1497
  """
1498
1498
  version = self._client.api_version(OffsetFetchRequest, max_version=5)
1499
- if version <= 3:
1499
+ if version <= 5:
1500
1500
  if partitions is None:
1501
1501
  if version <= 1:
1502
1502
  raise ValueError(
@@ -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
30
+ from kafka.util import Dict, WeakMethod, ensure_valid_topic_name
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
@@ -276,6 +276,7 @@ class KafkaClient(object):
276
276
  if compatible_version:
277
277
  log.warning('Configured api_version %s not supported; using %s',
278
278
  self.config['api_version'], compatible_version)
279
+ self.config['api_version'] = compatible_version
279
280
  self._api_versions = BROKER_API_VERSIONS[compatible_version]
280
281
  else:
281
282
  raise Errors.UnrecognizedBrokerVersion(self.config['api_version'])
@@ -364,7 +365,7 @@ class KafkaClient(object):
364
365
  self._connecting.remove(node_id)
365
366
  try:
366
367
  self._selector.unregister(sock)
367
- except KeyError:
368
+ except (KeyError, ValueError):
368
369
  pass
369
370
 
370
371
  if self._sensors:
@@ -909,7 +910,13 @@ class KafkaClient(object):
909
910
 
910
911
  Returns:
911
912
  Future: resolves after metadata request/response
913
+
914
+ Raises:
915
+ TypeError: if topic is not a string
916
+ ValueError: if topic is invalid: must be chars (a-zA-Z0-9._-), and less than 250 length
912
917
  """
918
+ ensure_valid_topic_name(topic)
919
+
913
920
  if topic in self._topics:
914
921
  return Future().success(set(self._topics))
915
922
 
@@ -971,8 +978,10 @@ class KafkaClient(object):
971
978
  topics = list(self.config['bootstrap_topics_filter'])
972
979
 
973
980
  api_version = self.api_version(MetadataRequest, max_version=7)
974
- if self.cluster.need_all_topic_metadata or not topics:
981
+ if self.cluster.need_all_topic_metadata:
975
982
  topics = MetadataRequest[api_version].ALL_TOPICS
983
+ elif not topics:
984
+ topics = MetadataRequest[api_version].NO_TOPICS
976
985
  if api_version >= 4:
977
986
  request = MetadataRequest[api_version](topics, self.config['allow_auto_create_topics'])
978
987
  else:
@@ -112,6 +112,7 @@ class ClusterMetadata(object):
112
112
 
113
113
  Returns:
114
114
  set: {partition (int), ...}
115
+ None if topic not found.
115
116
  """
116
117
  if topic not in self._partitions:
117
118
  return None
@@ -101,6 +101,10 @@ class BrokerConnection(object):
101
101
  server-side log entries that correspond to this client. Also
102
102
  submitted to GroupCoordinator for logging with respect to
103
103
  consumer group administration. Default: 'kafka-python-{version}'
104
+ client_software_name (str): Sent to kafka broker for KIP-511.
105
+ Default: 'kafka-python'
106
+ client_software_version (str): Sent to kafka broker for KIP-511.
107
+ Default: The kafka-python version (via kafka.version).
104
108
  reconnect_backoff_ms (int): The amount of time in milliseconds to
105
109
  wait before attempting to reconnect to a given host.
106
110
  Default: 50.
@@ -191,6 +195,8 @@ class BrokerConnection(object):
191
195
 
192
196
  DEFAULT_CONFIG = {
193
197
  'client_id': 'kafka-python-' + __version__,
198
+ 'client_software_name': 'kafka-python',
199
+ 'client_software_version': __version__,
194
200
  'node_id': 0,
195
201
  'request_timeout_ms': 30000,
196
202
  'reconnect_backoff_ms': 50,
@@ -242,7 +248,7 @@ class BrokerConnection(object):
242
248
  self._api_versions = None
243
249
  self._api_version = None
244
250
  self._check_version_idx = None
245
- self._api_versions_idx = 2
251
+ self._api_versions_idx = 4 # version of ApiVersionsRequest to try on first connect
246
252
  self._throttle_time = None
247
253
  self._socks5_proxy = None
248
254
 
@@ -265,12 +271,10 @@ class BrokerConnection(object):
265
271
  assert self.config['security_protocol'] in self.SECURITY_PROTOCOLS, (
266
272
  'security_protocol must be in ' + ', '.join(self.SECURITY_PROTOCOLS))
267
273
 
268
- self._sasl_mechanism = None
269
274
  if self.config['security_protocol'] in ('SSL', 'SASL_SSL'):
270
275
  assert ssl_available, "Python wasn't built with SSL support"
271
276
 
272
- if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
273
- self._sasl_mechanism = get_sasl_mechanism(self.config['sasl_mechanism'])(**self.config)
277
+ self._init_sasl_mechanism()
274
278
 
275
279
  # This is not a general lock / this class is not generally thread-safe yet
276
280
  # However, to avoid pushing responsibility for maintaining
@@ -306,11 +310,17 @@ class BrokerConnection(object):
306
310
  self.config['metric_group_prefix'],
307
311
  self.node_id)
308
312
 
313
+ def _init_sasl_mechanism(self):
314
+ if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
315
+ self._sasl_mechanism = get_sasl_mechanism(self.config['sasl_mechanism'])(**self.config)
316
+ else:
317
+ self._sasl_mechanism = None
318
+
309
319
  def _dns_lookup(self):
310
320
  self._gai = dns_lookup(self.host, self.port, self.afi)
311
321
  if not self._gai:
312
- log.error('DNS lookup failed for %s:%i (%s)',
313
- self.host, self.port, self.afi)
322
+ log.error('%s: DNS lookup failed for %s:%i (%s)',
323
+ self, self.host, self.port, self.afi)
314
324
  return False
315
325
  return True
316
326
 
@@ -356,6 +366,7 @@ class BrokerConnection(object):
356
366
  def connect(self):
357
367
  """Attempt to connect and return ConnectionState"""
358
368
  if self.state is ConnectionStates.DISCONNECTED and not self.blacked_out():
369
+ self.state = ConnectionStates.CONNECTING
359
370
  self.last_attempt = time.time()
360
371
  next_lookup = self._next_afi_sockaddr()
361
372
  if not next_lookup:
@@ -380,7 +391,6 @@ class BrokerConnection(object):
380
391
  self._sock.setsockopt(*option)
381
392
 
382
393
  self._sock.setblocking(False)
383
- self.state = ConnectionStates.CONNECTING
384
394
  self.config['state_change_callback'](self.node_id, self._sock, self)
385
395
  log.info('%s: connecting to %s:%d [%s %s]', self, self.host,
386
396
  self.port, self._sock_addr, AFI_NAMES[self._sock_afi])
@@ -402,20 +412,20 @@ class BrokerConnection(object):
402
412
  log.debug('%s: established TCP connection', self)
403
413
 
404
414
  if self.config['security_protocol'] in ('SSL', 'SASL_SSL'):
405
- log.debug('%s: initiating SSL handshake', self)
406
415
  self.state = ConnectionStates.HANDSHAKE
416
+ log.debug('%s: initiating SSL handshake', self)
407
417
  self.config['state_change_callback'](self.node_id, self._sock, self)
408
418
  # _wrap_ssl can alter the connection state -- disconnects on failure
409
419
  self._wrap_ssl()
410
420
  else:
411
- log.debug('%s: checking broker Api Versions', self)
412
421
  self.state = ConnectionStates.API_VERSIONS_SEND
422
+ log.debug('%s: checking broker Api Versions', self)
413
423
  self.config['state_change_callback'](self.node_id, self._sock, self)
414
424
 
415
425
  # Connection failed
416
426
  # WSAEINVAL == 10022, but errno.WSAEINVAL is not available on non-win systems
417
427
  elif ret not in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK, 10022):
418
- log.error('Connect attempt to %s returned error %s.'
428
+ log.error('%s: Connect attempt returned error %s.'
419
429
  ' Disconnecting.', self, ret)
420
430
  errstr = errno.errorcode.get(ret, 'UNKNOWN')
421
431
  self.close(Errors.KafkaConnectionError('{} {}'.format(ret, errstr)))
@@ -428,8 +438,8 @@ class BrokerConnection(object):
428
438
  if self.state is ConnectionStates.HANDSHAKE:
429
439
  if self._try_handshake():
430
440
  log.debug('%s: completed SSL handshake.', self)
431
- log.debug('%s: checking broker Api Versions', self)
432
441
  self.state = ConnectionStates.API_VERSIONS_SEND
442
+ log.debug('%s: checking broker Api Versions', self)
433
443
  self.config['state_change_callback'](self.node_id, self._sock, self)
434
444
 
435
445
  if self.state in (ConnectionStates.API_VERSIONS_SEND, ConnectionStates.API_VERSIONS_RECV):
@@ -437,13 +447,13 @@ class BrokerConnection(object):
437
447
  # _try_api_versions_check has side-effects: possibly disconnected on socket errors
438
448
  if self.state in (ConnectionStates.API_VERSIONS_SEND, ConnectionStates.API_VERSIONS_RECV):
439
449
  if self.config['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'):
440
- log.debug('%s: initiating SASL authentication', self)
441
450
  self.state = ConnectionStates.AUTHENTICATING
451
+ log.debug('%s: initiating SASL authentication', self)
442
452
  self.config['state_change_callback'](self.node_id, self._sock, self)
443
453
  else:
444
454
  # security_protocol PLAINTEXT
445
- log.info('%s: Connection complete.', self)
446
455
  self.state = ConnectionStates.CONNECTED
456
+ log.info('%s: Connection complete.', self)
447
457
  self._reset_reconnect_backoff()
448
458
  self.config['state_change_callback'](self.node_id, self._sock, self)
449
459
 
@@ -452,8 +462,8 @@ class BrokerConnection(object):
452
462
  if self._try_authenticate():
453
463
  # _try_authenticate has side-effects: possibly disconnected on socket errors
454
464
  if self.state is ConnectionStates.AUTHENTICATING:
455
- log.info('%s: Connection complete.', self)
456
465
  self.state = ConnectionStates.CONNECTED
466
+ log.info('%s: Connection complete.', self)
457
467
  self._reset_reconnect_backoff()
458
468
  self.config['state_change_callback'](self.node_id, self._sock, self)
459
469
 
@@ -462,7 +472,7 @@ class BrokerConnection(object):
462
472
  # Connection timed out
463
473
  request_timeout = self.config['request_timeout_ms'] / 1000.0
464
474
  if time.time() > request_timeout + self.last_attempt:
465
- log.error('Connection attempt to %s timed out', self)
475
+ log.error('%s: Connection attempt timed out', self)
466
476
  self.close(Errors.KafkaConnectionError('timeout'))
467
477
  return self.state
468
478
 
@@ -521,7 +531,7 @@ class BrokerConnection(object):
521
531
  except (SSLWantReadError, SSLWantWriteError):
522
532
  pass
523
533
  except (SSLZeroReturnError, ConnectionError, TimeoutError, SSLEOFError):
524
- log.warning('SSL connection closed by server during handshake.')
534
+ log.warning('%s: SSL connection closed by server during handshake.', self)
525
535
  self.close(Errors.KafkaConnectionError('SSL connection closed by server during handshake'))
526
536
  # Other SSLErrors will be raised to user
527
537
 
@@ -538,7 +548,14 @@ class BrokerConnection(object):
538
548
  log.debug('%s: Using pre-configured api_version %s for ApiVersions', self, self._api_version)
539
549
  return True
540
550
  elif self._check_version_idx is None:
541
- request = ApiVersionsRequest[self._api_versions_idx]()
551
+ version = self._api_versions_idx
552
+ if version >= 3:
553
+ request = ApiVersionsRequest[version](
554
+ client_software_name=self.config['client_software_name'],
555
+ client_software_version=self.config['client_software_version'],
556
+ _tagged_fields={})
557
+ else:
558
+ request = ApiVersionsRequest[version]()
542
559
  future = Future()
543
560
  response = self._send(request, blocking=True, request_timeout_ms=(self.config['api_version_auto_timeout_ms'] * 0.8))
544
561
  response.add_callback(self._handle_api_versions_response, future)
@@ -573,11 +590,15 @@ class BrokerConnection(object):
573
590
 
574
591
  def _handle_api_versions_response(self, future, response):
575
592
  error_type = Errors.for_code(response.error_code)
576
- # if error_type i UNSUPPORTED_VERSION: retry w/ latest version from response
577
593
  if error_type is not Errors.NoError:
578
594
  future.failure(error_type())
579
595
  if error_type is Errors.UnsupportedVersionError:
580
596
  self._api_versions_idx -= 1
597
+ for api_key, min_version, max_version, *rest in response.api_versions:
598
+ # If broker provides a lower max_version, skip to that
599
+ if api_key == response.API_KEY:
600
+ self._api_versions_idx = min(self._api_versions_idx, max_version)
601
+ break
581
602
  if self._api_versions_idx >= 0:
582
603
  self._api_versions_future = None
583
604
  self.state = ConnectionStates.API_VERSIONS_SEND
@@ -587,10 +608,10 @@ class BrokerConnection(object):
587
608
  return
588
609
  self._api_versions = dict([
589
610
  (api_key, (min_version, max_version))
590
- for api_key, min_version, max_version in response.api_versions
611
+ for api_key, min_version, max_version, *rest in response.api_versions
591
612
  ])
592
613
  self._api_version = self._infer_broker_version_from_api_versions(self._api_versions)
593
- log.info('Broker version identified as %s', '.'.join(map(str, self._api_version)))
614
+ log.info('%s: Broker version identified as %s', self, '.'.join(map(str, self._api_version)))
594
615
  future.success(self._api_version)
595
616
  self.connect()
596
617
 
@@ -600,7 +621,7 @@ class BrokerConnection(object):
600
621
  # after failure connection is closed, so state should already be DISCONNECTED
601
622
 
602
623
  def _handle_check_version_response(self, future, version, _response):
603
- log.info('Broker version identified as %s', '.'.join(map(str, version)))
624
+ log.info('%s: Broker version identified as %s', self, '.'.join(map(str, version)))
604
625
  log.info('Set configuration api_version=%s to skip auto'
605
626
  ' check_version requests on startup', version)
606
627
  self._api_versions = BROKER_API_VERSIONS[version]
@@ -730,6 +751,7 @@ class BrokerConnection(object):
730
751
  request = SaslAuthenticateRequest[0](sasl_auth_bytes)
731
752
  self._send(request, blocking=True)
732
753
  else:
754
+ log.debug('%s: Sending %d raw sasl auth bytes to server', self, len(sasl_auth_bytes))
733
755
  try:
734
756
  self._send_bytes_blocking(Int32.encode(len(sasl_auth_bytes)) + sasl_auth_bytes)
735
757
  except (ConnectionError, TimeoutError) as e:
@@ -759,7 +781,7 @@ class BrokerConnection(object):
759
781
  latency_ms = (time.time() - timestamp) * 1000
760
782
  if self._sensors:
761
783
  self._sensors.request_time.record(latency_ms)
762
- log.debug('%s Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
784
+ log.debug('%s: Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
763
785
 
764
786
  error_type = Errors.for_code(response.error_code)
765
787
  if error_type is not Errors.NoError:
@@ -770,6 +792,7 @@ class BrokerConnection(object):
770
792
  return response.auth_bytes
771
793
  else:
772
794
  # unframed bytes w/ SaslHandhake v0
795
+ log.debug('%s: Received %d raw sasl auth bytes from server', self, nbytes)
773
796
  return data[4:]
774
797
 
775
798
  def _sasl_authenticate(self, future):
@@ -913,6 +936,7 @@ class BrokerConnection(object):
913
936
  self._update_reconnect_backoff()
914
937
  self._api_versions_future = None
915
938
  self._sasl_auth_future = None
939
+ self._init_sasl_mechanism()
916
940
  self._protocol = KafkaProtocol(
917
941
  client_id=self.config['client_id'],
918
942
  api_version=self.config['api_version'])
@@ -932,7 +956,8 @@ class BrokerConnection(object):
932
956
 
933
957
  # drop lock before state change callback and processing futures
934
958
  self.config['state_change_callback'](self.node_id, sock, self)
935
- sock.close()
959
+ if sock:
960
+ sock.close()
936
961
  for (_correlation_id, (future, _timestamp, _timeout)) in ifrs:
937
962
  future.failure(error)
938
963
 
@@ -978,7 +1003,7 @@ class BrokerConnection(object):
978
1003
 
979
1004
  correlation_id = self._protocol.send_request(request)
980
1005
 
981
- log.debug('%s Request %d (timeout_ms %s): %s', self, correlation_id, request_timeout_ms, request)
1006
+ log.debug('%s: Request %d (timeout_ms %s): %s', self, correlation_id, request_timeout_ms, request)
982
1007
  if request.expect_response():
983
1008
  assert correlation_id not in self.in_flight_requests, 'Correlation ID already in-flight!'
984
1009
  sent_time = time.time()
@@ -1012,7 +1037,7 @@ class BrokerConnection(object):
1012
1037
  return True
1013
1038
 
1014
1039
  except (ConnectionError, TimeoutError) as e:
1015
- log.exception("Error sending request data to %s", self)
1040
+ log.exception("%s: Error sending request data", self)
1016
1041
  error = Errors.KafkaConnectionError("%s: %s" % (self, e))
1017
1042
  self.close(error=error)
1018
1043
  return False
@@ -1045,7 +1070,7 @@ class BrokerConnection(object):
1045
1070
  return len(self._send_buffer) == 0
1046
1071
 
1047
1072
  except (ConnectionError, TimeoutError, Exception) as e:
1048
- log.exception("Error sending request data to %s", self)
1073
+ log.exception("%s: Error sending request data", self)
1049
1074
  error = Errors.KafkaConnectionError("%s: %s" % (self, e))
1050
1075
  self.close(error=error)
1051
1076
  return False
@@ -1082,7 +1107,7 @@ class BrokerConnection(object):
1082
1107
  if not responses and self.requests_timed_out():
1083
1108
  timed_out = self.timed_out_ifrs()
1084
1109
  timeout_ms = (timed_out[0][2] - timed_out[0][1]) * 1000
1085
- log.warning('%s timed out after %s ms. Closing connection.',
1110
+ log.warning('%s: timed out after %s ms. Closing connection.',
1086
1111
  self, timeout_ms)
1087
1112
  self.close(error=Errors.RequestTimedOutError(
1088
1113
  'Request timed out after %s ms' %
@@ -1101,7 +1126,7 @@ class BrokerConnection(object):
1101
1126
  if self._sensors:
1102
1127
  self._sensors.request_time.record(latency_ms)
1103
1128
 
1104
- log.debug('%s Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
1129
+ log.debug('%s: Response %d (%s ms): %s', self, correlation_id, latency_ms, response)
1105
1130
  self._maybe_throttle(response)
1106
1131
  responses[i] = (response, future)
1107
1132
 
@@ -1113,7 +1138,7 @@ class BrokerConnection(object):
1113
1138
  err = None
1114
1139
  with self._lock:
1115
1140
  if not self._can_send_recv():
1116
- log.warning('%s cannot recv: socket not connected', self)
1141
+ log.warning('%s: cannot recv: socket not connected', self)
1117
1142
  return ()
1118
1143
 
1119
1144
  while len(recvd) < self.config['sock_chunk_buffer_count']: