kafka-python 2.0.6__tar.gz → 2.1.0__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 (155) hide show
  1. {kafka_python-2.0.6 → kafka_python-2.1.0}/CHANGES.md +87 -0
  2. {kafka_python-2.0.6 → kafka_python-2.1.0}/PKG-INFO +2 -2
  3. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/client.py +246 -95
  4. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/client_async.py +207 -84
  5. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/cluster.py +31 -10
  6. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/conn.py +401 -441
  7. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/consumer/fetcher.py +437 -175
  8. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/consumer/group.py +85 -129
  9. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/consumer/subscription_state.py +11 -10
  10. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/sticky/sticky_assignor.py +0 -1
  11. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/base.py +172 -134
  12. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/consumer.py +149 -79
  13. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/errors.py +1 -0
  14. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/metric_name.py +1 -1
  15. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/metrics.py +3 -1
  16. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/quota.py +1 -1
  17. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/kafka.py +84 -44
  18. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/record_accumulator.py +9 -7
  19. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/sender.py +13 -48
  20. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/admin.py +115 -97
  21. kafka_python-2.1.0/kafka/protocol/api_versions.py +90 -0
  22. kafka_python-2.1.0/kafka/protocol/broker_api_versions.py +66 -0
  23. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/commit.py +110 -52
  24. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/fetch.py +12 -10
  25. kafka_python-2.1.0/kafka/protocol/find_coordinator.py +64 -0
  26. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/group.py +64 -10
  27. kafka_python-2.0.6/kafka/protocol/offset.py → kafka_python-2.1.0/kafka/protocol/list_offsets.py +29 -29
  28. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/metadata.py +69 -13
  29. kafka_python-2.1.0/kafka/protocol/offset_for_leader_epoch.py +140 -0
  30. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/parser.py +2 -2
  31. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/produce.py +4 -2
  32. kafka_python-2.1.0/kafka/protocol/sasl_authenticate.py +42 -0
  33. kafka_python-2.1.0/kafka/protocol/sasl_handshake.py +39 -0
  34. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/default_records.py +4 -0
  35. kafka_python-2.1.0/kafka/sasl/__init__.py +34 -0
  36. kafka_python-2.1.0/kafka/sasl/abc.py +32 -0
  37. kafka_python-2.1.0/kafka/sasl/gssapi.py +87 -0
  38. kafka_python-2.1.0/kafka/sasl/msk.py +233 -0
  39. kafka_python-2.1.0/kafka/sasl/oauth.py +87 -0
  40. kafka_python-2.1.0/kafka/sasl/plain.py +41 -0
  41. kafka_python-2.1.0/kafka/sasl/scram.py +133 -0
  42. kafka_python-2.1.0/kafka/sasl/sspi.py +111 -0
  43. kafka_python-2.1.0/kafka/socks5_wrapper.py +248 -0
  44. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/structs.py +5 -4
  45. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/util.py +22 -1
  46. kafka_python-2.1.0/kafka/version.py +1 -0
  47. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka_python.egg-info/PKG-INFO +2 -2
  48. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka_python.egg-info/SOURCES.txt +16 -4
  49. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka_python.egg-info/requires.txt +3 -1
  50. {kafka_python-2.0.6 → kafka_python-2.1.0}/pyproject.toml +1 -1
  51. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_admin.py +4 -4
  52. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_admin_integration.py +74 -3
  53. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_client_async.py +11 -9
  54. kafka_python-2.1.0/test/test_cluster.py +134 -0
  55. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_conn.py +48 -7
  56. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_consumer.py +1 -1
  57. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_consumer_group.py +39 -36
  58. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_consumer_integration.py +7 -3
  59. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_coordinator.py +60 -20
  60. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_fetcher.py +120 -71
  61. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_object_conversion.py +2 -2
  62. kafka_python-2.1.0/test/test_producer.py +158 -0
  63. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_protocol.py +4 -6
  64. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_sender.py +7 -9
  65. kafka_python-2.0.6/kafka/oauth/__init__.py +0 -3
  66. kafka_python-2.0.6/kafka/oauth/abstract.py +0 -42
  67. kafka_python-2.0.6/kafka/scram.py +0 -81
  68. kafka_python-2.0.6/kafka/version.py +0 -1
  69. kafka_python-2.0.6/test/test_cluster.py +0 -22
  70. kafka_python-2.0.6/test/test_producer.py +0 -137
  71. {kafka_python-2.0.6 → kafka_python-2.1.0}/AUTHORS.md +0 -0
  72. {kafka_python-2.0.6 → kafka_python-2.1.0}/LICENSE +0 -0
  73. {kafka_python-2.0.6 → kafka_python-2.1.0}/MANIFEST.in +0 -0
  74. {kafka_python-2.0.6 → kafka_python-2.1.0}/README.rst +0 -0
  75. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/__init__.py +0 -0
  76. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/__init__.py +0 -0
  77. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/acl_resource.py +0 -0
  78. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/config_resource.py +0 -0
  79. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/new_partitions.py +0 -0
  80. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/admin/new_topic.py +0 -0
  81. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/codec.py +0 -0
  82. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/consumer/__init__.py +0 -0
  83. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/__init__.py +0 -0
  84. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/__init__.py +0 -0
  85. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/abstract.py +0 -0
  86. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/range.py +0 -0
  87. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/roundrobin.py +0 -0
  88. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/sticky/__init__.py +0 -0
  89. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/sticky/partition_movements.py +0 -0
  90. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/assignors/sticky/sorted_set.py +0 -0
  91. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/heartbeat.py +0 -0
  92. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/coordinator/protocol.py +0 -0
  93. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/future.py +0 -0
  94. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/__init__.py +0 -0
  95. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/compound_stat.py +0 -0
  96. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/dict_reporter.py +0 -0
  97. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/kafka_metric.py +0 -0
  98. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/measurable.py +0 -0
  99. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/measurable_stat.py +0 -0
  100. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/metric_config.py +0 -0
  101. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/metrics_reporter.py +0 -0
  102. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stat.py +0 -0
  103. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/__init__.py +0 -0
  104. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/avg.py +0 -0
  105. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/count.py +0 -0
  106. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/histogram.py +0 -0
  107. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/max_stat.py +0 -0
  108. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/min_stat.py +0 -0
  109. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/percentile.py +0 -0
  110. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/percentiles.py +0 -0
  111. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/rate.py +0 -0
  112. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/sampled_stat.py +0 -0
  113. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/sensor.py +0 -0
  114. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/metrics/stats/total.py +0 -0
  115. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/partitioner/__init__.py +0 -0
  116. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/partitioner/default.py +0 -0
  117. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/__init__.py +0 -0
  118. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/buffer.py +0 -0
  119. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/producer/future.py +0 -0
  120. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/__init__.py +0 -0
  121. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/abstract.py +0 -0
  122. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/api.py +0 -0
  123. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/frame.py +0 -0
  124. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/message.py +0 -0
  125. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/pickle.py +0 -0
  126. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/struct.py +0 -0
  127. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/protocol/types.py +0 -0
  128. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/__init__.py +0 -0
  129. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/_crc32c.py +0 -0
  130. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/abc.py +0 -0
  131. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/legacy_records.py +0 -0
  132. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/memory_records.py +0 -0
  133. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/record/util.py +0 -0
  134. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/serializer/__init__.py +0 -0
  135. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/serializer/abstract.py +0 -0
  136. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/vendor/__init__.py +0 -0
  137. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/vendor/enum34.py +0 -0
  138. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/vendor/selectors34.py +0 -0
  139. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/vendor/six.py +0 -0
  140. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka/vendor/socketpair.py +0 -0
  141. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka_python.egg-info/dependency_links.txt +0 -0
  142. {kafka_python-2.0.6 → kafka_python-2.1.0}/kafka_python.egg-info/top_level.txt +0 -0
  143. {kafka_python-2.0.6 → kafka_python-2.1.0}/setup.cfg +0 -0
  144. {kafka_python-2.0.6 → kafka_python-2.1.0}/setup.py +0 -0
  145. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_acl_comparisons.py +0 -0
  146. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_api_object_implementation.py +0 -0
  147. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_assignors.py +0 -0
  148. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_codec.py +0 -0
  149. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_metrics.py +0 -0
  150. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_package.py +0 -0
  151. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_partition_movements.py +0 -0
  152. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_partitioner.py +0 -0
  153. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_sasl_integration.py +0 -0
  154. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/test_subscription_state.py +0 -0
  155. {kafka_python-2.0.6 → kafka_python-2.1.0}/test/testutil.py +0 -0
@@ -1,3 +1,90 @@
1
+ # 2.1.0 (Mar 14, 2025)
2
+
3
+ Support Kafka Broker 2.1 API Baseline
4
+ * Add baseline leader_epoch support for ListOffsets v4 / FetchRequest v10 (#2511)
5
+ * Support OffsetFetch v5 / OffsetCommit v6 (2.1 baseline) (#2505)
6
+ * Support 2.1 baseline consumer group apis (#2503)
7
+ * Support FindCoordinatorRequest v2 in consumer and admin client (#2502)
8
+ * Support ListOffsets v3 in consumer (#2501)
9
+ * Support Fetch Request/Response v6 in consumer (#2500)
10
+ * Add support for Metadata Request/Response v7 (#2497)
11
+ * Implement Incremental Fetch Sessions / KIP-227 (#2508)
12
+ * Implement client-side connection throttling / KIP-219 (#2510)
13
+ * Add KafkaClient.api_version(operation) for best available from api_versions (#2495)
14
+
15
+ Consumer
16
+ * Timeout coordinator poll / ensure_coordinator_ready / ensure_active_group (#2526)
17
+ * Add optional timeout_ms kwarg to remaining consumer/coordinator methods (#2544)
18
+ * Check for coordinator.poll failure in KafkaConsumer
19
+ * Only mark coordinator dead if connection_delay > 0 (#2530)
20
+ * Delay group coordinator until after bootstrap (#2539)
21
+ * KAFKA-4160: Ensure rebalance listener not called with coordinator lock (#1438)
22
+ * Call default_offset_commit_callback after `_maybe_auto_commit_offsets_async` (#2546)
23
+ * Remove legacy/v1 consumer message iterator (#2543)
24
+ * Log warning when attempting to list offsets for unknown topic/partition (#2540)
25
+ * Add heartbeat thread id to debug logs on start
26
+ * Add inner_timeout_ms handler to fetcher; add fallback (#2529)
27
+
28
+ Producer
29
+ * KafkaProducer: Flush pending records before close() (#2537)
30
+ * Raise immediate error on producer.send after close (#2542)
31
+ * Limit producer close timeout to 1sec in __del__; use context managers to close in test_producer
32
+ * Use NullLogger in producer atexit cleanup
33
+ * Attempt to fix metadata race condition when partitioning in producer.send (#2523)
34
+ * Remove unused partial KIP-467 implementation (ProduceResponse batch error details) (#2524)
35
+
36
+ AdminClient
37
+ * Implement perform leader election (#2536)
38
+ * Support delete_records (#2535)
39
+
40
+ Networking
41
+ * Call ApiVersionsRequest during connection, prior to Sasl Handshake (#2493)
42
+ * Fake api_versions for old brokers, rename to ApiVersionsRequest, and handle error decoding (#2494)
43
+ * Debug log when skipping api_versions request with pre-configured api_version
44
+ * Only refresh metadata if connection fails all dns records (#2532)
45
+ * Support connections through SOCKS5 proxies (#2531)
46
+ * Fix OverflowError when connection_max_idle_ms is 0 or inf (#2538)
47
+ * socket.setblocking for eventlet/gevent compatibility
48
+ * Support custom per-request timeouts (#2498)
49
+ * Include request_timeout_ms in request debug log
50
+ * Support client.poll with future and timeout_ms
51
+ * mask unused afi var
52
+ * Debug log if check_version connection attempt fails
53
+
54
+ SASL Modules
55
+ * Refactor Sasl authentication with SaslMechanism abstract base class; support SaslAuthenticate (#2515)
56
+ * Add SSPI (Kerberos for Windows) authentication mechanism (#2521)
57
+ * Support AWS_MSK_IAM authentication (#2519)
58
+ * Cleanup sasl mechanism configuration checks; fix gssapi bugs; add sasl_kerberos_name config (#2520)
59
+ * Move kafka.oauth.AbstractTokenProvider -> kafka.sasl.oauth.AbstractTokenProvider (#2525)
60
+
61
+ Testing
62
+ * Bump default python to 3.13 in CI tests (#2541)
63
+ * Update pytest log_format: use logger instead of filename; add thread id
64
+ * Improve test_consumer_group::test_group logging before group stabilized (#2534)
65
+ * Limit test duration to 5mins w/ pytest-timeout
66
+ * Fix external kafka/zk fixtures for testing (#2533)
67
+ * Disable zookeeper admin server to avoid port conflicts
68
+ * Set default pytest log level to debug
69
+ * test_group: shorter timeout, more logging, more sleep
70
+ * Cache servers/dist in github actions workflow (#2527)
71
+ * Remove tox.ini; update testing docs
72
+ * Use thread-specific client_id in test_group
73
+ * Fix subprocess log warning; specify timeout_ms kwarg in consumer.poll tests
74
+ * Only set KAFKA_JVM_PERFORMANCE_OPTS in makefile if unset; add note re: 2.0-2.3 broker testing
75
+ * Add kafka command to test.fixtures; raise FileNotFoundError if version not installed
76
+
77
+ Documentation
78
+ * Improve ClusterMetadata docs re: node_id/broker_id str/int types
79
+ * Document api_version_auto_timeout_ms default; override in group tests
80
+
81
+ Fixes
82
+ * Signal close to metrics expire_loop
83
+ * Add kafka.util timeout_ms_fn
84
+ * fixup TopicAuthorizationFailedError construction
85
+ * Fix lint issues via ruff check (#2522)
86
+ * Make the "mock" dependency optional (only used in Python < 3.3). (#2518)
87
+
1
88
  # 2.0.6 (Mar 4, 2025)
2
89
 
3
90
  Networking
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kafka-python
3
- Version: 2.0.6
3
+ Version: 2.1.0
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
@@ -35,7 +35,7 @@ Provides-Extra: zstd
35
35
  Requires-Dist: zstandard; extra == "zstd"
36
36
  Provides-Extra: testing
37
37
  Requires-Dist: pytest; extra == "testing"
38
- Requires-Dist: mock; extra == "testing"
38
+ Requires-Dist: mock; python_version < "3.3" and extra == "testing"
39
39
  Requires-Dist: pytest-mock; extra == "testing"
40
40
 
41
41
  Kafka Python client
@@ -15,15 +15,15 @@ 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,
18
+ IncompatibleBrokerVersion, KafkaConfigurationError, NotControllerError, UnknownTopicOrPartitionError,
19
19
  UnrecognizedBrokerVersion, IllegalArgumentError)
20
20
  from kafka.metrics import MetricConfig, Metrics
21
21
  from kafka.protocol.admin import (
22
22
  CreateTopicsRequest, DeleteTopicsRequest, DescribeConfigsRequest, AlterConfigsRequest, CreatePartitionsRequest,
23
23
  ListGroupsRequest, DescribeGroupsRequest, DescribeAclsRequest, CreateAclsRequest, DeleteAclsRequest,
24
- DeleteGroupsRequest, DescribeLogDirsRequest
25
- )
26
- from kafka.protocol.commit import GroupCoordinatorRequest, OffsetFetchRequest
24
+ DeleteGroupsRequest, DeleteRecordsRequest, DescribeLogDirsRequest, ElectLeadersRequest, ElectionType)
25
+ from kafka.protocol.commit import OffsetFetchRequest
26
+ from kafka.protocol.find_coordinator import FindCoordinatorRequest
27
27
  from kafka.protocol.metadata import MetadataRequest
28
28
  from kafka.protocol.types import Array
29
29
  from kafka.structs import TopicPartition, OffsetAndMetadata, MemberInformation, GroupInformation
@@ -141,14 +141,17 @@ class KafkaAdminClient(object):
141
141
  Required if sasl_mechanism is PLAIN or one of the SCRAM mechanisms.
142
142
  sasl_plain_password (str): password for sasl PLAIN and SCRAM authentication.
143
143
  Required if sasl_mechanism is PLAIN or one of the SCRAM mechanisms.
144
+ sasl_kerberos_name (str or gssapi.Name): Constructed gssapi.Name for use with
145
+ sasl mechanism handshake. If provided, sasl_kerberos_service_name and
146
+ sasl_kerberos_domain name are ignored. Default: None.
144
147
  sasl_kerberos_service_name (str): Service name to include in GSSAPI
145
148
  sasl mechanism handshake. Default: 'kafka'
146
149
  sasl_kerberos_domain_name (str): kerberos domain name to use in GSSAPI
147
150
  sasl mechanism handshake. Default: one of bootstrap servers
148
- sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
149
- instance. (See kafka.oauth.abstract). Default: None
151
+ sasl_oauth_token_provider (kafka.sasl.oauth.AbstractTokenProvider): OAuthBearer
152
+ token provider instance. Default: None
153
+ socks5_proxy (str): Socks5 proxy url. Default: None
150
154
  kafka_client (callable): Custom class / callable for creating KafkaClient instances
151
-
152
155
  """
153
156
  DEFAULT_CONFIG = {
154
157
  # client configs
@@ -180,9 +183,11 @@ class KafkaAdminClient(object):
180
183
  'sasl_mechanism': None,
181
184
  'sasl_plain_username': None,
182
185
  'sasl_plain_password': None,
186
+ 'sasl_kerberos_name': None,
183
187
  'sasl_kerberos_service_name': 'kafka',
184
188
  'sasl_kerberos_domain_name': None,
185
189
  'sasl_oauth_token_provider': None,
190
+ 'socks5_proxy': None,
186
191
 
187
192
  # metrics configs
188
193
  'metric_reporters': [],
@@ -215,11 +220,7 @@ class KafkaAdminClient(object):
215
220
  )
216
221
 
217
222
  # Get auto-discovered version from client if necessary
218
- if self.config['api_version'] is None:
219
- self.config['api_version'] = self._client.config['api_version']
220
- else:
221
- # need to run check_version for get_api_versions()
222
- self._client.check_version(timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
223
+ self.config['api_version'] = self._client.config['api_version']
223
224
 
224
225
  self._closed = False
225
226
  self._refresh_controller_id()
@@ -236,35 +237,6 @@ class KafkaAdminClient(object):
236
237
  self._closed = True
237
238
  log.debug("KafkaAdminClient is now closed.")
238
239
 
239
- def _matching_api_version(self, operation):
240
- """Find the latest version of the protocol operation supported by both
241
- this library and the broker.
242
-
243
- This resolves to the lesser of either the latest api version this
244
- library supports, or the max version supported by the broker.
245
-
246
- Arguments:
247
- operation: A list of protocol operation versions from kafka.protocol.
248
-
249
- Returns:
250
- int: The max matching version number between client and broker.
251
- """
252
- broker_api_versions = self._client.get_api_versions()
253
- api_key = operation[0].API_KEY
254
- if broker_api_versions is None or api_key not in broker_api_versions:
255
- raise IncompatibleBrokerVersion(
256
- "Kafka broker does not support the '{}' Kafka protocol."
257
- .format(operation[0].__name__))
258
- min_version, max_version = broker_api_versions[api_key]
259
- version = min(len(operation) - 1, max_version)
260
- if version < min_version:
261
- # max library version is less than min broker version. Currently,
262
- # no Kafka versions specify a min msg version. Maybe in the future?
263
- raise IncompatibleBrokerVersion(
264
- "No version of the '{}' Kafka protocol is supported by both the client and broker."
265
- .format(operation[0].__name__))
266
- return version
267
-
268
240
  def _validate_timeout(self, timeout_ms):
269
241
  """Validate the timeout is set or use the configuration default.
270
242
 
@@ -278,7 +250,7 @@ class KafkaAdminClient(object):
278
250
 
279
251
  def _refresh_controller_id(self, timeout_ms=30000):
280
252
  """Determine the Kafka cluster controller."""
281
- version = self._matching_api_version(MetadataRequest)
253
+ version = self._client.api_version(MetadataRequest, max_version=6)
282
254
  if 1 <= version <= 6:
283
255
  timeout_at = time.time() + timeout_ms / 1000
284
256
  while time.time() < timeout_at:
@@ -318,18 +290,14 @@ class KafkaAdminClient(object):
318
290
  Returns:
319
291
  A message future
320
292
  """
321
- # TODO add support for dynamically picking version of
322
- # GroupCoordinatorRequest which was renamed to FindCoordinatorRequest.
323
- # When I experimented with this, the coordinator value returned in
324
- # GroupCoordinatorResponse_v1 didn't match the value returned by
325
- # GroupCoordinatorResponse_v0 and I couldn't figure out why.
326
- version = 0
327
- # version = self._matching_api_version(GroupCoordinatorRequest)
293
+ version = self._client.api_version(FindCoordinatorRequest, max_version=2)
328
294
  if version <= 0:
329
- request = GroupCoordinatorRequest[version](group_id)
295
+ request = FindCoordinatorRequest[version](group_id)
296
+ elif version <= 2:
297
+ request = FindCoordinatorRequest[version](group_id, 0)
330
298
  else:
331
299
  raise NotImplementedError(
332
- "Support for GroupCoordinatorRequest_v{} has not yet been added to KafkaAdminClient."
300
+ "Support for FindCoordinatorRequest_v{} has not yet been added to KafkaAdminClient."
333
301
  .format(version))
334
302
  return self._send_request_to_node(self._client.least_loaded_node(), request)
335
303
 
@@ -342,18 +310,13 @@ class KafkaAdminClient(object):
342
310
  Returns:
343
311
  The node_id of the broker that is the coordinator.
344
312
  """
345
- if response.API_VERSION <= 0:
346
- error_type = Errors.for_code(response.error_code)
347
- if error_type is not Errors.NoError:
348
- # Note: When error_type.retriable, Java will retry... see
349
- # KafkaAdminClient's handleFindCoordinatorError method
350
- raise error_type(
351
- "FindCoordinatorRequest failed with response '{}'."
352
- .format(response))
353
- else:
354
- raise NotImplementedError(
355
- "Support for FindCoordinatorRequest_v{} has not yet been added to KafkaAdminClient."
356
- .format(response.API_VERSION))
313
+ error_type = Errors.for_code(response.error_code)
314
+ if error_type is not Errors.NoError:
315
+ # Note: When error_type.retriable, Java will retry... see
316
+ # KafkaAdminClient's handleFindCoordinatorError method
317
+ raise error_type(
318
+ "FindCoordinatorRequest failed with response '{}'."
319
+ .format(response))
357
320
  return response.coordinator_id
358
321
 
359
322
  def _find_coordinator_ids(self, group_ids):
@@ -430,27 +393,55 @@ class KafkaAdminClient(object):
430
393
  # So this is a little brittle in that it assumes all responses have
431
394
  # one of these attributes and that they always unpack into
432
395
  # (topic, error_code) tuples.
433
- topic_error_tuples = (response.topic_errors if hasattr(response, 'topic_errors')
434
- else response.topic_error_codes)
435
- # Also small py2/py3 compatibility -- py3 can ignore extra values
436
- # during unpack via: for x, y, *rest in list_of_values. py2 cannot.
437
- # So for now we have to map across the list and explicitly drop any
438
- # extra values (usually the error_message)
439
- for topic, error_code in map(lambda e: e[:2], topic_error_tuples):
396
+ topic_error_tuples = getattr(response, 'topic_errors', getattr(response, 'topic_error_codes', None))
397
+ if topic_error_tuples is not None:
398
+ success = self._parse_topic_request_response(topic_error_tuples, request, response, tries)
399
+ else:
400
+ # Leader Election request has a two layer error response (topic and partition)
401
+ success = self._parse_topic_partition_request_response(request, response, tries)
402
+
403
+ if success:
404
+ return response
405
+ raise RuntimeError("This should never happen, please file a bug with full stacktrace if encountered")
406
+
407
+ def _parse_topic_request_response(self, topic_error_tuples, request, response, tries):
408
+ # Also small py2/py3 compatibility -- py3 can ignore extra values
409
+ # during unpack via: for x, y, *rest in list_of_values. py2 cannot.
410
+ # So for now we have to map across the list and explicitly drop any
411
+ # extra values (usually the error_message)
412
+ for topic, error_code in map(lambda e: e[:2], topic_error_tuples):
413
+ error_type = Errors.for_code(error_code)
414
+ if tries and error_type is NotControllerError:
415
+ # No need to inspect the rest of the errors for
416
+ # non-retriable errors because NotControllerError should
417
+ # either be thrown for all errors or no errors.
418
+ self._refresh_controller_id()
419
+ return False
420
+ elif error_type is not Errors.NoError:
421
+ raise error_type(
422
+ "Request '{}' failed with response '{}'."
423
+ .format(request, response))
424
+ return True
425
+
426
+ def _parse_topic_partition_request_response(self, request, response, tries):
427
+ # Also small py2/py3 compatibility -- py3 can ignore extra values
428
+ # during unpack via: for x, y, *rest in list_of_values. py2 cannot.
429
+ # So for now we have to map across the list and explicitly drop any
430
+ # extra values (usually the error_message)
431
+ for topic, partition_results in response.replication_election_results:
432
+ for partition_id, error_code in map(lambda e: e[:2], partition_results):
440
433
  error_type = Errors.for_code(error_code)
441
434
  if tries and error_type is NotControllerError:
442
435
  # No need to inspect the rest of the errors for
443
436
  # non-retriable errors because NotControllerError should
444
437
  # either be thrown for all errors or no errors.
445
438
  self._refresh_controller_id()
446
- break
447
- elif error_type is not Errors.NoError:
439
+ return False
440
+ elif error_type not in [Errors.NoError, Errors.ElectionNotNeeded]:
448
441
  raise error_type(
449
442
  "Request '{}' failed with response '{}'."
450
443
  .format(request, response))
451
- else:
452
- return response
453
- raise RuntimeError("This should never happen, please file a bug with full stacktrace if encountered")
444
+ return True
454
445
 
455
446
  @staticmethod
456
447
  def _convert_new_topic_request(new_topic):
@@ -493,7 +484,7 @@ class KafkaAdminClient(object):
493
484
  Returns:
494
485
  Appropriate version of CreateTopicResponse class.
495
486
  """
496
- version = self._matching_api_version(CreateTopicsRequest)
487
+ version = self._client.api_version(CreateTopicsRequest, max_version=3)
497
488
  timeout_ms = self._validate_timeout(timeout_ms)
498
489
  if version == 0:
499
490
  if validate_only:
@@ -531,7 +522,7 @@ class KafkaAdminClient(object):
531
522
  Returns:
532
523
  Appropriate version of DeleteTopicsResponse class.
533
524
  """
534
- version = self._matching_api_version(DeleteTopicsRequest)
525
+ version = self._client.api_version(DeleteTopicsRequest, max_version=3)
535
526
  timeout_ms = self._validate_timeout(timeout_ms)
536
527
  if version <= 3:
537
528
  request = DeleteTopicsRequest[version](
@@ -550,7 +541,7 @@ class KafkaAdminClient(object):
550
541
  """
551
542
  topics == None means "get all topics"
552
543
  """
553
- version = self._matching_api_version(MetadataRequest)
544
+ version = self._client.api_version(MetadataRequest, max_version=5)
554
545
  if version <= 3:
555
546
  if auto_topic_creation:
556
547
  raise IncompatibleBrokerVersion(
@@ -667,7 +658,7 @@ class KafkaAdminClient(object):
667
658
  tuple of a list of matching ACL objects and a KafkaError (NoError if successful)
668
659
  """
669
660
 
670
- version = self._matching_api_version(DescribeAclsRequest)
661
+ version = self._client.api_version(DescribeAclsRequest, max_version=1)
671
662
  if version == 0:
672
663
  request = DescribeAclsRequest[version](
673
664
  resource_type=acl_filter.resource_pattern.resource_type,
@@ -801,7 +792,7 @@ class KafkaAdminClient(object):
801
792
  if not isinstance(acl, ACL):
802
793
  raise IllegalArgumentError("acls must contain ACL objects")
803
794
 
804
- version = self._matching_api_version(CreateAclsRequest)
795
+ version = self._client.api_version(CreateAclsRequest, max_version=1)
805
796
  if version == 0:
806
797
  request = CreateAclsRequest[version](
807
798
  creations=[self._convert_create_acls_resource_request_v0(acl) for acl in acls]
@@ -923,7 +914,7 @@ class KafkaAdminClient(object):
923
914
  if not isinstance(acl, ACLFilter):
924
915
  raise IllegalArgumentError("acl_filters must contain ACLFilter type objects")
925
916
 
926
- version = self._matching_api_version(DeleteAclsRequest)
917
+ version = self._client.api_version(DeleteAclsRequest, max_version=1)
927
918
 
928
919
  if version == 0:
929
920
  request = DeleteAclsRequest[version](
@@ -992,7 +983,7 @@ class KafkaAdminClient(object):
992
983
  topic_resources.append(self._convert_describe_config_resource_request(config_resource))
993
984
 
994
985
  futures = []
995
- version = self._matching_api_version(DescribeConfigsRequest)
986
+ version = self._client.api_version(DescribeConfigsRequest, max_version=2)
996
987
  if version == 0:
997
988
  if include_synonyms:
998
989
  raise IncompatibleBrokerVersion(
@@ -1077,7 +1068,7 @@ class KafkaAdminClient(object):
1077
1068
  Returns:
1078
1069
  Appropriate version of AlterConfigsResponse class.
1079
1070
  """
1080
- version = self._matching_api_version(AlterConfigsRequest)
1071
+ version = self._client.api_version(AlterConfigsRequest, max_version=1)
1081
1072
  if version <= 1:
1082
1073
  request = AlterConfigsRequest[version](
1083
1074
  resources=[self._convert_alter_config_resource_request(config_resource) for config_resource in config_resources]
@@ -1138,7 +1129,7 @@ class KafkaAdminClient(object):
1138
1129
  Returns:
1139
1130
  Appropriate version of CreatePartitionsResponse class.
1140
1131
  """
1141
- version = self._matching_api_version(CreatePartitionsRequest)
1132
+ version = self._client.api_version(CreatePartitionsRequest, max_version=1)
1142
1133
  timeout_ms = self._validate_timeout(timeout_ms)
1143
1134
  if version <= 1:
1144
1135
  request = CreatePartitionsRequest[version](
@@ -1152,8 +1143,118 @@ class KafkaAdminClient(object):
1152
1143
  .format(version))
1153
1144
  return self._send_request_to_controller(request)
1154
1145
 
1155
- # delete records protocol not yet implemented
1156
- # Note: send the request to the partition leaders
1146
+ def _get_leader_for_partitions(self, partitions, timeout_ms=None):
1147
+ """Finds ID of the leader node for every given topic partition.
1148
+
1149
+ Will raise UnknownTopicOrPartitionError if for some partition no leader can be found.
1150
+
1151
+ :param partitions: ``[TopicPartition]``: partitions for which to find leaders.
1152
+ :param timeout_ms: ``float``: Timeout in milliseconds, if None (default), will be read from
1153
+ config.
1154
+
1155
+ :return: Dictionary with ``{leader_id -> {partitions}}``
1156
+ """
1157
+ timeout_ms = self._validate_timeout(timeout_ms)
1158
+
1159
+ partitions = set(partitions)
1160
+ topics = set(tp.topic for tp in partitions)
1161
+
1162
+ response = self._get_cluster_metadata(topics=topics).to_object()
1163
+
1164
+ leader2partitions = defaultdict(list)
1165
+ valid_partitions = set()
1166
+ for topic in response.get("topics", ()):
1167
+ for partition in topic.get("partitions", ()):
1168
+ t2p = TopicPartition(topic=topic["topic"], partition=partition["partition"])
1169
+ if t2p in partitions:
1170
+ leader2partitions[partition["leader"]].append(t2p)
1171
+ valid_partitions.add(t2p)
1172
+
1173
+ if len(partitions) != len(valid_partitions):
1174
+ unknown = set(partitions) - valid_partitions
1175
+ raise UnknownTopicOrPartitionError(
1176
+ "The following partitions are not known: %s"
1177
+ % ", ".join(str(x) for x in unknown)
1178
+ )
1179
+
1180
+ return leader2partitions
1181
+
1182
+ def delete_records(self, records_to_delete, timeout_ms=None, partition_leader_id=None):
1183
+ """Delete records whose offset is smaller than the given offset of the corresponding partition.
1184
+
1185
+ :param records_to_delete: ``{TopicPartition: int}``: The earliest available offsets for the
1186
+ given partitions.
1187
+ :param timeout_ms: ``float``: Timeout in milliseconds, if None (default), will be read from
1188
+ config.
1189
+ :param partition_leader_id: ``str``: If specified, all deletion requests will be sent to
1190
+ this node. No check is performed verifying that this is indeed the leader for all
1191
+ listed partitions: use with caution.
1192
+
1193
+ :return: Dictionary {topicPartition -> metadata}, where metadata is returned by the broker.
1194
+ See DeleteRecordsResponse for possible fields. error_code for all partitions is
1195
+ guaranteed to be zero, otherwise an exception is raised.
1196
+ """
1197
+ timeout_ms = self._validate_timeout(timeout_ms)
1198
+ responses = []
1199
+ version = self._client.api_version(DeleteRecordsRequest, max_version=0)
1200
+ if version is None:
1201
+ raise IncompatibleBrokerVersion("Broker does not support DeleteGroupsRequest")
1202
+
1203
+ # We want to make as few requests as possible
1204
+ # If a single node serves as a partition leader for multiple partitions (and/or
1205
+ # topics), we can send all of those in a single request.
1206
+ # For that we store {leader -> {partitions for leader}}, and do 1 request per leader
1207
+ if partition_leader_id is None:
1208
+ leader2partitions = self._get_leader_for_partitions(
1209
+ set(records_to_delete), timeout_ms
1210
+ )
1211
+ else:
1212
+ leader2partitions = {partition_leader_id: set(records_to_delete)}
1213
+
1214
+ for leader, partitions in leader2partitions.items():
1215
+ topic2partitions = defaultdict(list)
1216
+ for partition in partitions:
1217
+ topic2partitions[partition.topic].append(partition)
1218
+
1219
+ request = DeleteRecordsRequest[version](
1220
+ topics=[
1221
+ (topic, [(tp.partition, records_to_delete[tp]) for tp in partitions])
1222
+ for topic, partitions in topic2partitions.items()
1223
+ ],
1224
+ timeout_ms=timeout_ms
1225
+ )
1226
+ future = self._send_request_to_node(leader, request)
1227
+ self._wait_for_futures([future])
1228
+
1229
+ responses.append(future.value.to_object())
1230
+
1231
+ partition2result = {}
1232
+ partition2error = {}
1233
+ for response in responses:
1234
+ for topic in response["topics"]:
1235
+ for partition in topic["partitions"]:
1236
+ tp = TopicPartition(topic["name"], partition["partition_index"])
1237
+ partition2result[tp] = partition
1238
+ if partition["error_code"] != 0:
1239
+ partition2error[tp] = partition["error_code"]
1240
+
1241
+ if partition2error:
1242
+ if len(partition2error) == 1:
1243
+ key, error = next(iter(partition2error.items()))
1244
+ raise Errors.for_code(error)(
1245
+ "Error deleting records from topic %s partition %s" % (key.topic, key.partition)
1246
+ )
1247
+ else:
1248
+ raise Errors.BrokerResponseError(
1249
+ "The following errors occured when trying to delete records: " +
1250
+ ", ".join(
1251
+ "%s(partition=%d): %s" %
1252
+ (partition.topic, partition.partition, Errors.for_code(error).__name__)
1253
+ for partition, error in partition2error.items()
1254
+ )
1255
+ )
1256
+
1257
+ return partition2result
1157
1258
 
1158
1259
  # create delegation token protocol not yet implemented
1159
1260
  # Note: send the request to the least_loaded_node()
@@ -1177,7 +1278,7 @@ class KafkaAdminClient(object):
1177
1278
  Returns:
1178
1279
  A message future.
1179
1280
  """
1180
- version = self._matching_api_version(DescribeGroupsRequest)
1281
+ version = self._client.api_version(DescribeGroupsRequest, max_version=3)
1181
1282
  if version <= 2:
1182
1283
  if include_authorized_operations:
1183
1284
  raise IncompatibleBrokerVersion(
@@ -1311,7 +1412,7 @@ class KafkaAdminClient(object):
1311
1412
  Returns:
1312
1413
  A message future
1313
1414
  """
1314
- version = self._matching_api_version(ListGroupsRequest)
1415
+ version = self._client.api_version(ListGroupsRequest, max_version=2)
1315
1416
  if version <= 2:
1316
1417
  request = ListGroupsRequest[version]()
1317
1418
  else:
@@ -1394,7 +1495,7 @@ class KafkaAdminClient(object):
1394
1495
  Returns:
1395
1496
  A message future
1396
1497
  """
1397
- version = self._matching_api_version(OffsetFetchRequest)
1498
+ version = self._client.api_version(OffsetFetchRequest, max_version=5)
1398
1499
  if version <= 3:
1399
1500
  if partitions is None:
1400
1501
  if version <= 1:
@@ -1427,7 +1528,7 @@ class KafkaAdminClient(object):
1427
1528
  A dictionary composed of TopicPartition keys and
1428
1529
  OffsetAndMetadata values.
1429
1530
  """
1430
- if response.API_VERSION <= 3:
1531
+ if response.API_VERSION <= 5:
1431
1532
 
1432
1533
  # OffsetFetchResponse_v1 lacks a top-level error_code
1433
1534
  if response.API_VERSION > 1:
@@ -1442,13 +1543,18 @@ class KafkaAdminClient(object):
1442
1543
  # OffsetAndMetadata values--this is what the Java AdminClient returns
1443
1544
  offsets = {}
1444
1545
  for topic, partitions in response.topics:
1445
- for partition, offset, metadata, error_code in partitions:
1546
+ for partition_data in partitions:
1547
+ if response.API_VERSION <= 4:
1548
+ partition, offset, metadata, error_code = partition_data
1549
+ leader_epoch = -1
1550
+ else:
1551
+ partition, offset, leader_epoch, metadata, error_code = partition_data
1446
1552
  error_type = Errors.for_code(error_code)
1447
1553
  if error_type is not Errors.NoError:
1448
1554
  raise error_type(
1449
1555
  "Unable to fetch consumer group offsets for topic {}, partition {}"
1450
1556
  .format(topic, partition))
1451
- offsets[TopicPartition(topic, partition)] = OffsetAndMetadata(offset, metadata)
1557
+ offsets[TopicPartition(topic, partition)] = OffsetAndMetadata(offset, metadata, leader_epoch)
1452
1558
  else:
1453
1559
  raise NotImplementedError(
1454
1560
  "Support for OffsetFetchResponse_v{} has not yet been added to KafkaAdminClient."
@@ -1480,7 +1586,7 @@ class KafkaAdminClient(object):
1480
1586
 
1481
1587
  Returns:
1482
1588
  dictionary: A dictionary with TopicPartition keys and
1483
- OffsetAndMetada values. Partitions that are not specified and for
1589
+ OffsetAndMetadata values. Partitions that are not specified and for
1484
1590
  which the group_id does not have a recorded offset are omitted. An
1485
1591
  offset value of `-1` indicates the group_id has no offset for that
1486
1592
  TopicPartition. A `-1` can only happen for partitions that are
@@ -1564,7 +1670,7 @@ class KafkaAdminClient(object):
1564
1670
  Returns:
1565
1671
  A future representing the in-flight DeleteGroupsRequest.
1566
1672
  """
1567
- version = self._matching_api_version(DeleteGroupsRequest)
1673
+ version = self._client.api_version(DeleteGroupsRequest, max_version=1)
1568
1674
  if version <= 1:
1569
1675
  request = DeleteGroupsRequest[version](group_ids)
1570
1676
  else:
@@ -1573,6 +1679,51 @@ class KafkaAdminClient(object):
1573
1679
  .format(version))
1574
1680
  return self._send_request_to_node(group_coordinator_id, request)
1575
1681
 
1682
+ @staticmethod
1683
+ def _convert_topic_partitions(topic_partitions):
1684
+ return [
1685
+ (
1686
+ topic,
1687
+ partition_ids
1688
+ )
1689
+ for topic, partition_ids in topic_partitions.items()
1690
+ ]
1691
+
1692
+ def _get_all_topic_partitions(self):
1693
+ return [
1694
+ (
1695
+ topic,
1696
+ [partition_info.partition for partition_info in self._client.cluster._partitions[topic].values()]
1697
+ )
1698
+ for topic in self._client.cluster.topics()
1699
+ ]
1700
+
1701
+ def _get_topic_partitions(self, topic_partitions):
1702
+ if topic_partitions is None:
1703
+ return self._get_all_topic_partitions()
1704
+ return self._convert_topic_partitions(topic_partitions)
1705
+
1706
+ def perform_leader_election(self, election_type, topic_partitions=None, timeout_ms=None):
1707
+ """Perform leader election on the topic partitions.
1708
+
1709
+ :param election_type: Type of election to attempt. 0 for Perferred, 1 for Unclean
1710
+ :param topic_partitions: A map of topic name strings to partition ids list.
1711
+ By default, will run on all topic partitions
1712
+ :param timeout_ms: Milliseconds to wait for the leader election process to complete
1713
+ before the broker returns.
1714
+
1715
+ :return: Appropriate version of ElectLeadersResponse class.
1716
+ """
1717
+ version = self._client.api_version(ElectLeadersRequest, max_version=1)
1718
+ timeout_ms = self._validate_timeout(timeout_ms)
1719
+ request = ElectLeadersRequest[version](
1720
+ election_type=ElectionType(election_type),
1721
+ topic_partitions=self._get_topic_partitions(topic_partitions),
1722
+ timeout=timeout_ms,
1723
+ )
1724
+ # TODO convert structs to a more pythonic interface
1725
+ return self._send_request_to_controller(request)
1726
+
1576
1727
  def _wait_for_futures(self, futures):
1577
1728
  """Block until all futures complete. If any fail, raise the encountered exception.
1578
1729
 
@@ -1595,7 +1746,7 @@ class KafkaAdminClient(object):
1595
1746
  Returns:
1596
1747
  A message future
1597
1748
  """
1598
- version = self._matching_api_version(DescribeLogDirsRequest)
1749
+ version = self._client.api_version(DescribeLogDirsRequest, max_version=0)
1599
1750
  if version <= 0:
1600
1751
  request = DescribeLogDirsRequest[version]()
1601
1752
  future = self._send_request_to_node(self._client.least_loaded_node(), request)