kafka-python 3.0.0__py3-none-any.whl

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 (373) hide show
  1. kafka/__init__.py +34 -0
  2. kafka/__main__.py +5 -0
  3. kafka/admin/__init__.py +29 -0
  4. kafka/admin/__main__.py +5 -0
  5. kafka/admin/_acls.py +355 -0
  6. kafka/admin/_cluster.py +359 -0
  7. kafka/admin/_configs.py +479 -0
  8. kafka/admin/_groups.py +754 -0
  9. kafka/admin/_partitions.py +595 -0
  10. kafka/admin/_topics.py +281 -0
  11. kafka/admin/_transactions.py +450 -0
  12. kafka/admin/_users.py +194 -0
  13. kafka/admin/client.py +373 -0
  14. kafka/benchmarks/__init__.py +0 -0
  15. kafka/benchmarks/consumer_performance.py +138 -0
  16. kafka/benchmarks/load_example.py +109 -0
  17. kafka/benchmarks/producer_encode_path.py +201 -0
  18. kafka/benchmarks/producer_performance.py +161 -0
  19. kafka/benchmarks/profile_protocol.py +138 -0
  20. kafka/benchmarks/protocol_old_vs_new.py +447 -0
  21. kafka/benchmarks/record_batch_compose.py +77 -0
  22. kafka/benchmarks/record_batch_read.py +82 -0
  23. kafka/benchmarks/varint_speed.py +426 -0
  24. kafka/cli/__init__.py +36 -0
  25. kafka/cli/admin/__init__.py +117 -0
  26. kafka/cli/admin/acls/__init__.py +9 -0
  27. kafka/cli/admin/acls/common.py +76 -0
  28. kafka/cli/admin/acls/create.py +19 -0
  29. kafka/cli/admin/acls/delete.py +23 -0
  30. kafka/cli/admin/acls/describe.py +16 -0
  31. kafka/cli/admin/cluster/__init__.py +14 -0
  32. kafka/cli/admin/cluster/describe.py +11 -0
  33. kafka/cli/admin/cluster/describe_quorum.py +11 -0
  34. kafka/cli/admin/cluster/features.py +52 -0
  35. kafka/cli/admin/cluster/log_dirs.py +43 -0
  36. kafka/cli/admin/cluster/versions.py +33 -0
  37. kafka/cli/admin/configs/__init__.py +10 -0
  38. kafka/cli/admin/configs/alter.py +43 -0
  39. kafka/cli/admin/configs/common.py +17 -0
  40. kafka/cli/admin/configs/describe.py +30 -0
  41. kafka/cli/admin/configs/list.py +16 -0
  42. kafka/cli/admin/configs/reset.py +20 -0
  43. kafka/cli/admin/groups/__init__.py +16 -0
  44. kafka/cli/admin/groups/alter_offsets.py +30 -0
  45. kafka/cli/admin/groups/delete.py +11 -0
  46. kafka/cli/admin/groups/delete_offsets.py +29 -0
  47. kafka/cli/admin/groups/describe.py +11 -0
  48. kafka/cli/admin/groups/list.py +28 -0
  49. kafka/cli/admin/groups/list_offsets.py +29 -0
  50. kafka/cli/admin/groups/remove_members.py +40 -0
  51. kafka/cli/admin/groups/reset_offsets.py +139 -0
  52. kafka/cli/admin/partitions/__init__.py +21 -0
  53. kafka/cli/admin/partitions/alter_reassignments.py +37 -0
  54. kafka/cli/admin/partitions/create.py +27 -0
  55. kafka/cli/admin/partitions/delete_records.py +31 -0
  56. kafka/cli/admin/partitions/describe.py +36 -0
  57. kafka/cli/admin/partitions/elect_leaders.py +53 -0
  58. kafka/cli/admin/partitions/list_offsets.py +88 -0
  59. kafka/cli/admin/partitions/list_reassignments.py +35 -0
  60. kafka/cli/admin/topics/__init__.py +10 -0
  61. kafka/cli/admin/topics/create.py +13 -0
  62. kafka/cli/admin/topics/delete.py +19 -0
  63. kafka/cli/admin/topics/describe.py +18 -0
  64. kafka/cli/admin/topics/list.py +11 -0
  65. kafka/cli/admin/transactions/__init__.py +17 -0
  66. kafka/cli/admin/transactions/abort.py +38 -0
  67. kafka/cli/admin/transactions/describe.py +24 -0
  68. kafka/cli/admin/transactions/describe_producers.py +29 -0
  69. kafka/cli/admin/transactions/find_hanging.py +26 -0
  70. kafka/cli/admin/transactions/list.py +37 -0
  71. kafka/cli/admin/users/__init__.py +8 -0
  72. kafka/cli/admin/users/alter_user_scram_credentials.py +34 -0
  73. kafka/cli/admin/users/describe_user_scram_credentials.py +15 -0
  74. kafka/cli/common.py +95 -0
  75. kafka/cli/consumer/__init__.py +63 -0
  76. kafka/cli/producer/__init__.py +57 -0
  77. kafka/cluster.py +824 -0
  78. kafka/codec.py +325 -0
  79. kafka/consumer/__init__.py +5 -0
  80. kafka/consumer/__main__.py +5 -0
  81. kafka/consumer/fetcher.py +2012 -0
  82. kafka/consumer/group.py +1347 -0
  83. kafka/consumer/subscription_state.py +897 -0
  84. kafka/coordinator/__init__.py +0 -0
  85. kafka/coordinator/assignors/__init__.py +0 -0
  86. kafka/coordinator/assignors/abstract.py +90 -0
  87. kafka/coordinator/assignors/cooperative_sticky.py +167 -0
  88. kafka/coordinator/assignors/range.py +81 -0
  89. kafka/coordinator/assignors/roundrobin.py +101 -0
  90. kafka/coordinator/assignors/sticky/StickyAssignorUserData.json +37 -0
  91. kafka/coordinator/assignors/sticky/__init__.py +0 -0
  92. kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
  93. kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
  94. kafka/coordinator/assignors/sticky/sticky_assignor.py +665 -0
  95. kafka/coordinator/assignors/sticky/user_data.py +8 -0
  96. kafka/coordinator/base.py +1215 -0
  97. kafka/coordinator/consumer.py +1224 -0
  98. kafka/coordinator/heartbeat.py +82 -0
  99. kafka/coordinator/subscription.py +34 -0
  100. kafka/errors.py +1004 -0
  101. kafka/future.py +166 -0
  102. kafka/metrics/__init__.py +13 -0
  103. kafka/metrics/compound_stat.py +33 -0
  104. kafka/metrics/dict_reporter.py +81 -0
  105. kafka/metrics/kafka_metric.py +36 -0
  106. kafka/metrics/measurable.py +27 -0
  107. kafka/metrics/measurable_stat.py +13 -0
  108. kafka/metrics/metric_config.py +33 -0
  109. kafka/metrics/metric_name.py +105 -0
  110. kafka/metrics/metrics.py +261 -0
  111. kafka/metrics/metrics_reporter.py +53 -0
  112. kafka/metrics/quota.py +41 -0
  113. kafka/metrics/stat.py +19 -0
  114. kafka/metrics/stats/__init__.py +15 -0
  115. kafka/metrics/stats/avg.py +24 -0
  116. kafka/metrics/stats/count.py +17 -0
  117. kafka/metrics/stats/histogram.py +99 -0
  118. kafka/metrics/stats/max_stat.py +17 -0
  119. kafka/metrics/stats/min_stat.py +19 -0
  120. kafka/metrics/stats/percentile.py +14 -0
  121. kafka/metrics/stats/percentiles.py +75 -0
  122. kafka/metrics/stats/rate.py +118 -0
  123. kafka/metrics/stats/sampled_stat.py +99 -0
  124. kafka/metrics/stats/sensor.py +136 -0
  125. kafka/metrics/stats/total.py +15 -0
  126. kafka/net/__init__.py +19 -0
  127. kafka/net/compat.py +165 -0
  128. kafka/net/connection.py +593 -0
  129. kafka/net/http_connect.py +144 -0
  130. kafka/net/inet.py +122 -0
  131. kafka/net/manager.py +451 -0
  132. kafka/net/metrics.py +149 -0
  133. kafka/net/sasl/__init__.py +32 -0
  134. kafka/net/sasl/abc.py +28 -0
  135. kafka/net/sasl/gssapi.py +95 -0
  136. kafka/net/sasl/msk.py +245 -0
  137. kafka/net/sasl/oauth.py +98 -0
  138. kafka/net/sasl/plain.py +42 -0
  139. kafka/net/sasl/scram.py +135 -0
  140. kafka/net/sasl/sspi.py +111 -0
  141. kafka/net/selector.py +644 -0
  142. kafka/net/socks5.py +262 -0
  143. kafka/net/transport.py +415 -0
  144. kafka/net/wakeup_notifier.py +72 -0
  145. kafka/partitioner/__init__.py +8 -0
  146. kafka/partitioner/abc.py +8 -0
  147. kafka/partitioner/default.py +89 -0
  148. kafka/partitioner/sticky.py +109 -0
  149. kafka/producer/__init__.py +5 -0
  150. kafka/producer/__main__.py +5 -0
  151. kafka/producer/future.py +101 -0
  152. kafka/producer/kafka.py +1123 -0
  153. kafka/producer/producer_batch.py +192 -0
  154. kafka/producer/record_accumulator.py +647 -0
  155. kafka/producer/sender.py +884 -0
  156. kafka/producer/transaction_manager.py +1326 -0
  157. kafka/protocol/__init__.py +0 -0
  158. kafka/protocol/admin/__init__.py +29 -0
  159. kafka/protocol/admin/acl.py +83 -0
  160. kafka/protocol/admin/acl.pyi +375 -0
  161. kafka/protocol/admin/client_quotas.py +14 -0
  162. kafka/protocol/admin/client_quotas.pyi +265 -0
  163. kafka/protocol/admin/cluster.py +31 -0
  164. kafka/protocol/admin/cluster.pyi +620 -0
  165. kafka/protocol/admin/configs.py +22 -0
  166. kafka/protocol/admin/configs.pyi +437 -0
  167. kafka/protocol/admin/groups.py +24 -0
  168. kafka/protocol/admin/groups.pyi +261 -0
  169. kafka/protocol/admin/topics.py +53 -0
  170. kafka/protocol/admin/topics.pyi +982 -0
  171. kafka/protocol/admin/transactions.py +18 -0
  172. kafka/protocol/admin/transactions.pyi +311 -0
  173. kafka/protocol/admin/users.py +14 -0
  174. kafka/protocol/admin/users.pyi +223 -0
  175. kafka/protocol/api_data.py +125 -0
  176. kafka/protocol/api_header.py +55 -0
  177. kafka/protocol/api_key.py +97 -0
  178. kafka/protocol/api_message.py +277 -0
  179. kafka/protocol/broker_version_data.py +246 -0
  180. kafka/protocol/consumer/__init__.py +13 -0
  181. kafka/protocol/consumer/fetch.py +16 -0
  182. kafka/protocol/consumer/fetch.pyi +298 -0
  183. kafka/protocol/consumer/group.py +38 -0
  184. kafka/protocol/consumer/group.pyi +824 -0
  185. kafka/protocol/consumer/metadata.py +30 -0
  186. kafka/protocol/consumer/metadata.pyi +89 -0
  187. kafka/protocol/consumer/offsets.py +75 -0
  188. kafka/protocol/consumer/offsets.pyi +288 -0
  189. kafka/protocol/data_container.py +166 -0
  190. kafka/protocol/frame.py +30 -0
  191. kafka/protocol/generate_stubs.py +468 -0
  192. kafka/protocol/metadata/__init__.py +10 -0
  193. kafka/protocol/metadata/api_versions.py +41 -0
  194. kafka/protocol/metadata/api_versions.pyi +128 -0
  195. kafka/protocol/metadata/find_coordinator.py +19 -0
  196. kafka/protocol/metadata/find_coordinator.pyi +105 -0
  197. kafka/protocol/metadata/metadata.py +34 -0
  198. kafka/protocol/metadata/metadata.pyi +160 -0
  199. kafka/protocol/old/__init__.py +0 -0
  200. kafka/protocol/old/abstract.py +17 -0
  201. kafka/protocol/old/add_offsets_to_txn.py +54 -0
  202. kafka/protocol/old/add_partitions_to_txn.py +71 -0
  203. kafka/protocol/old/admin.py +1086 -0
  204. kafka/protocol/old/api.py +205 -0
  205. kafka/protocol/old/api_versions.py +133 -0
  206. kafka/protocol/old/commit.py +355 -0
  207. kafka/protocol/old/consumer_protocol.py +36 -0
  208. kafka/protocol/old/end_txn.py +53 -0
  209. kafka/protocol/old/fetch.py +408 -0
  210. kafka/protocol/old/find_coordinator.py +72 -0
  211. kafka/protocol/old/group.py +451 -0
  212. kafka/protocol/old/init_producer_id.py +42 -0
  213. kafka/protocol/old/list_offsets.py +186 -0
  214. kafka/protocol/old/metadata.py +290 -0
  215. kafka/protocol/old/offset_for_leader_epoch.py +133 -0
  216. kafka/protocol/old/produce.py +247 -0
  217. kafka/protocol/old/sasl_authenticate.py +38 -0
  218. kafka/protocol/old/sasl_handshake.py +39 -0
  219. kafka/protocol/old/struct.py +87 -0
  220. kafka/protocol/old/txn_offset_commit.py +73 -0
  221. kafka/protocol/old/types.py +440 -0
  222. kafka/protocol/parser.py +191 -0
  223. kafka/protocol/producer/__init__.py +7 -0
  224. kafka/protocol/producer/produce.py +17 -0
  225. kafka/protocol/producer/produce.pyi +197 -0
  226. kafka/protocol/producer/transaction.py +30 -0
  227. kafka/protocol/producer/transaction.pyi +663 -0
  228. kafka/protocol/sasl.py +52 -0
  229. kafka/protocol/sasl.pyi +126 -0
  230. kafka/protocol/schemas/__init__.py +7 -0
  231. kafka/protocol/schemas/fields/__init__.py +7 -0
  232. kafka/protocol/schemas/fields/array.py +127 -0
  233. kafka/protocol/schemas/fields/base.py +156 -0
  234. kafka/protocol/schemas/fields/codecs/__init__.py +12 -0
  235. kafka/protocol/schemas/fields/codecs/encode_buffer.py +82 -0
  236. kafka/protocol/schemas/fields/codecs/tagged_fields.py +109 -0
  237. kafka/protocol/schemas/fields/codecs/types.py +505 -0
  238. kafka/protocol/schemas/fields/codegen.py +40 -0
  239. kafka/protocol/schemas/fields/simple.py +127 -0
  240. kafka/protocol/schemas/fields/struct.py +357 -0
  241. kafka/protocol/schemas/fields/struct_array.py +142 -0
  242. kafka/protocol/schemas/load_json.py +42 -0
  243. kafka/protocol/schemas/resources/AddOffsetsToTxnRequest.json +40 -0
  244. kafka/protocol/schemas/resources/AddOffsetsToTxnResponse.json +35 -0
  245. kafka/protocol/schemas/resources/AddPartitionsToTxnRequest.json +65 -0
  246. kafka/protocol/schemas/resources/AddPartitionsToTxnResponse.json +60 -0
  247. kafka/protocol/schemas/resources/AlterClientQuotasRequest.json +47 -0
  248. kafka/protocol/schemas/resources/AlterClientQuotasResponse.json +41 -0
  249. kafka/protocol/schemas/resources/AlterConfigsRequest.json +43 -0
  250. kafka/protocol/schemas/resources/AlterConfigsResponse.json +39 -0
  251. kafka/protocol/schemas/resources/AlterPartitionReassignmentsRequest.json +42 -0
  252. kafka/protocol/schemas/resources/AlterPartitionReassignmentsResponse.json +47 -0
  253. kafka/protocol/schemas/resources/AlterReplicaLogDirsRequest.json +41 -0
  254. kafka/protocol/schemas/resources/AlterReplicaLogDirsResponse.json +41 -0
  255. kafka/protocol/schemas/resources/AlterUserScramCredentialsRequest.json +45 -0
  256. kafka/protocol/schemas/resources/AlterUserScramCredentialsResponse.json +35 -0
  257. kafka/protocol/schemas/resources/ApiVersionsRequest.json +34 -0
  258. kafka/protocol/schemas/resources/ApiVersionsResponse.json +79 -0
  259. kafka/protocol/schemas/resources/ConsumerProtocolAssignment.json +42 -0
  260. kafka/protocol/schemas/resources/ConsumerProtocolSubscription.json +49 -0
  261. kafka/protocol/schemas/resources/CreateAclsRequest.json +46 -0
  262. kafka/protocol/schemas/resources/CreateAclsResponse.json +37 -0
  263. kafka/protocol/schemas/resources/CreatePartitionsRequest.json +47 -0
  264. kafka/protocol/schemas/resources/CreatePartitionsResponse.json +41 -0
  265. kafka/protocol/schemas/resources/CreateTopicsRequest.json +65 -0
  266. kafka/protocol/schemas/resources/CreateTopicsResponse.json +72 -0
  267. kafka/protocol/schemas/resources/DeleteAclsRequest.json +46 -0
  268. kafka/protocol/schemas/resources/DeleteAclsResponse.json +59 -0
  269. kafka/protocol/schemas/resources/DeleteGroupsRequest.json +30 -0
  270. kafka/protocol/schemas/resources/DeleteGroupsResponse.json +36 -0
  271. kafka/protocol/schemas/resources/DeleteRecordsRequest.json +42 -0
  272. kafka/protocol/schemas/resources/DeleteRecordsResponse.json +43 -0
  273. kafka/protocol/schemas/resources/DeleteTopicsRequest.json +43 -0
  274. kafka/protocol/schemas/resources/DeleteTopicsResponse.json +52 -0
  275. kafka/protocol/schemas/resources/DescribeAclsRequest.json +43 -0
  276. kafka/protocol/schemas/resources/DescribeAclsResponse.json +55 -0
  277. kafka/protocol/schemas/resources/DescribeClientQuotasRequest.json +37 -0
  278. kafka/protocol/schemas/resources/DescribeClientQuotasResponse.json +47 -0
  279. kafka/protocol/schemas/resources/DescribeClusterRequest.json +35 -0
  280. kafka/protocol/schemas/resources/DescribeClusterResponse.json +56 -0
  281. kafka/protocol/schemas/resources/DescribeConfigsRequest.json +42 -0
  282. kafka/protocol/schemas/resources/DescribeConfigsResponse.json +69 -0
  283. kafka/protocol/schemas/resources/DescribeGroupsRequest.json +38 -0
  284. kafka/protocol/schemas/resources/DescribeGroupsResponse.json +74 -0
  285. kafka/protocol/schemas/resources/DescribeLogDirsRequest.json +38 -0
  286. kafka/protocol/schemas/resources/DescribeLogDirsResponse.json +65 -0
  287. kafka/protocol/schemas/resources/DescribeProducersRequest.json +32 -0
  288. kafka/protocol/schemas/resources/DescribeProducersResponse.json +55 -0
  289. kafka/protocol/schemas/resources/DescribeQuorumRequest.json +39 -0
  290. kafka/protocol/schemas/resources/DescribeQuorumResponse.json +82 -0
  291. kafka/protocol/schemas/resources/DescribeTopicPartitionsRequest.json +40 -0
  292. kafka/protocol/schemas/resources/DescribeTopicPartitionsResponse.json +66 -0
  293. kafka/protocol/schemas/resources/DescribeTransactionsRequest.json +27 -0
  294. kafka/protocol/schemas/resources/DescribeTransactionsResponse.json +52 -0
  295. kafka/protocol/schemas/resources/DescribeUserScramCredentialsRequest.json +30 -0
  296. kafka/protocol/schemas/resources/DescribeUserScramCredentialsResponse.json +45 -0
  297. kafka/protocol/schemas/resources/ElectLeadersRequest.json +41 -0
  298. kafka/protocol/schemas/resources/ElectLeadersResponse.json +45 -0
  299. kafka/protocol/schemas/resources/EndTxnRequest.json +43 -0
  300. kafka/protocol/schemas/resources/EndTxnResponse.json +41 -0
  301. kafka/protocol/schemas/resources/FetchRequest.json +125 -0
  302. kafka/protocol/schemas/resources/FetchResponse.json +124 -0
  303. kafka/protocol/schemas/resources/FindCoordinatorRequest.json +43 -0
  304. kafka/protocol/schemas/resources/FindCoordinatorResponse.json +58 -0
  305. kafka/protocol/schemas/resources/HeartbeatRequest.json +39 -0
  306. kafka/protocol/schemas/resources/HeartbeatResponse.json +35 -0
  307. kafka/protocol/schemas/resources/IncrementalAlterConfigsRequest.json +44 -0
  308. kafka/protocol/schemas/resources/IncrementalAlterConfigsResponse.json +38 -0
  309. kafka/protocol/schemas/resources/InitProducerIdRequest.json +50 -0
  310. kafka/protocol/schemas/resources/InitProducerIdResponse.json +47 -0
  311. kafka/protocol/schemas/resources/JoinGroupRequest.json +63 -0
  312. kafka/protocol/schemas/resources/JoinGroupResponse.json +69 -0
  313. kafka/protocol/schemas/resources/LeaveGroupRequest.json +47 -0
  314. kafka/protocol/schemas/resources/LeaveGroupResponse.json +47 -0
  315. kafka/protocol/schemas/resources/ListConfigResourcesRequest.json +31 -0
  316. kafka/protocol/schemas/resources/ListConfigResourcesResponse.json +37 -0
  317. kafka/protocol/schemas/resources/ListGroupsRequest.json +36 -0
  318. kafka/protocol/schemas/resources/ListGroupsResponse.json +49 -0
  319. kafka/protocol/schemas/resources/ListOffsetsRequest.json +72 -0
  320. kafka/protocol/schemas/resources/ListOffsetsResponse.json +71 -0
  321. kafka/protocol/schemas/resources/ListPartitionReassignmentsRequest.json +34 -0
  322. kafka/protocol/schemas/resources/ListPartitionReassignmentsResponse.json +46 -0
  323. kafka/protocol/schemas/resources/ListTransactionsRequest.json +40 -0
  324. kafka/protocol/schemas/resources/ListTransactionsResponse.json +42 -0
  325. kafka/protocol/schemas/resources/MetadataRequest.json +56 -0
  326. kafka/protocol/schemas/resources/MetadataResponse.json +101 -0
  327. kafka/protocol/schemas/resources/OffsetCommitRequest.json +76 -0
  328. kafka/protocol/schemas/resources/OffsetCommitResponse.json +71 -0
  329. kafka/protocol/schemas/resources/OffsetDeleteRequest.json +39 -0
  330. kafka/protocol/schemas/resources/OffsetDeleteResponse.json +42 -0
  331. kafka/protocol/schemas/resources/OffsetFetchRequest.json +76 -0
  332. kafka/protocol/schemas/resources/OffsetFetchResponse.json +107 -0
  333. kafka/protocol/schemas/resources/OffsetForLeaderEpochRequest.json +52 -0
  334. kafka/protocol/schemas/resources/OffsetForLeaderEpochResponse.json +51 -0
  335. kafka/protocol/schemas/resources/ProduceRequest.json +73 -0
  336. kafka/protocol/schemas/resources/ProduceResponse.json +96 -0
  337. kafka/protocol/schemas/resources/RequestHeader.json +44 -0
  338. kafka/protocol/schemas/resources/ResponseHeader.json +26 -0
  339. kafka/protocol/schemas/resources/SaslAuthenticateRequest.json +29 -0
  340. kafka/protocol/schemas/resources/SaslAuthenticateResponse.json +34 -0
  341. kafka/protocol/schemas/resources/SaslHandshakeRequest.json +31 -0
  342. kafka/protocol/schemas/resources/SaslHandshakeResponse.json +32 -0
  343. kafka/protocol/schemas/resources/SyncGroupRequest.json +56 -0
  344. kafka/protocol/schemas/resources/SyncGroupResponse.json +46 -0
  345. kafka/protocol/schemas/resources/TxnOffsetCommitRequest.json +68 -0
  346. kafka/protocol/schemas/resources/TxnOffsetCommitResponse.json +47 -0
  347. kafka/protocol/schemas/resources/UpdateFeaturesRequest.json +43 -0
  348. kafka/protocol/schemas/resources/UpdateFeaturesResponse.json +39 -0
  349. kafka/protocol/schemas/resources/WriteTxnMarkersRequest.json +49 -0
  350. kafka/protocol/schemas/resources/WriteTxnMarkersResponse.json +45 -0
  351. kafka/protocol/schemas/resources/__init__.py +0 -0
  352. kafka/record/__init__.py +3 -0
  353. kafka/record/_crc32c.py +161 -0
  354. kafka/record/abc.py +144 -0
  355. kafka/record/default_records.py +782 -0
  356. kafka/record/legacy_records.py +587 -0
  357. kafka/record/memory_records.py +255 -0
  358. kafka/record/util.py +135 -0
  359. kafka/serializer/__init__.py +4 -0
  360. kafka/serializer/abstract.py +20 -0
  361. kafka/serializer/default.py +16 -0
  362. kafka/serializer/json.py +17 -0
  363. kafka/serializer/wrapper.py +21 -0
  364. kafka/structs.py +69 -0
  365. kafka/util.py +159 -0
  366. kafka/vendor/__init__.py +0 -0
  367. kafka/version.py +1 -0
  368. kafka_python-3.0.0.dist-info/METADATA +319 -0
  369. kafka_python-3.0.0.dist-info/RECORD +373 -0
  370. kafka_python-3.0.0.dist-info/WHEEL +5 -0
  371. kafka_python-3.0.0.dist-info/entry_points.txt +2 -0
  372. kafka_python-3.0.0.dist-info/licenses/LICENSE +202 -0
  373. kafka_python-3.0.0.dist-info/top_level.txt +1 -0
kafka/cluster.py ADDED
@@ -0,0 +1,824 @@
1
+ import collections
2
+ import copy
3
+ import logging
4
+ import random
5
+ import re
6
+ import socket
7
+ import threading
8
+ import time
9
+ import uuid
10
+ import weakref
11
+
12
+ from kafka import errors as Errors
13
+ from kafka.future import Future
14
+ from kafka.net.wakeup_notifier import WakeupNotifier
15
+ from kafka.protocol.metadata import MetadataRequest, MetadataResponse, CoordinatorType
16
+ from kafka.structs import TopicPartition
17
+ from kafka.util import ensure_valid_topic_name
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+
22
+ class ClusterMetadata:
23
+ """
24
+ A class to manage kafka cluster metadata.
25
+
26
+ Keyword Arguments:
27
+ retry_backoff_ms (int): Milliseconds to backoff when retrying on
28
+ errors. Default: 100.
29
+ metadata_max_age_ms (int): The period of time in milliseconds after
30
+ which we force a refresh of metadata even if we haven't seen any
31
+ partition leadership changes to proactively discover any new
32
+ brokers or partitions. Default: 300000
33
+ bootstrap_servers: 'host[:port]' string (or list of 'host[:port]'
34
+ strings) that the client should contact to bootstrap initial
35
+ cluster metadata. This does not have to be the full node list.
36
+ It just needs to have at least one broker that will respond to a
37
+ Metadata API Request. Default port is 9092. If no servers are
38
+ specified, will default to localhost:9092.
39
+ allow_auto_create_topics (bool): Enable/disable auto topic creation
40
+ on metadata request. Only available with api_version >= (0, 11).
41
+ Default: True
42
+ """
43
+ DEFAULT_CONFIG = {
44
+ 'retry_backoff_ms': 100,
45
+ 'metadata_max_age_ms': 300000,
46
+ 'bootstrap_servers': [],
47
+ 'allow_auto_create_topics': True,
48
+ 'client_dns_lookup': 'use_all_dns_ips',
49
+ }
50
+
51
+ def __init__(self, **configs):
52
+ self._manager = None
53
+ self._topics = set()
54
+ self._brokers = {} # node_id -> MetadataResponseBroker
55
+ self._partitions = {} # topic -> partition -> PartitionMetadata
56
+ self._broker_partitions = collections.defaultdict(set) # node_id -> {TopicPartition...}
57
+ self._topic_ids = {} # topic name -> uuid.UUID
58
+ self._topic_names_by_id = {} # uuid.UUID -> topic name
59
+ self._coordinators = {} # (key_type, key) -> node_id
60
+ self._last_refresh_ms = 0
61
+ self._last_successful_refresh_ms = 0
62
+ self._need_update = True
63
+ self._future = None
64
+ self._listeners = set()
65
+ self._lock = threading.Lock()
66
+ self.need_all_topic_metadata = False
67
+ self.unauthorized_topics = set()
68
+ self.internal_topics = set()
69
+ self.controller = None
70
+ self.cluster_id = None
71
+
72
+ self._refresh_loop_future = None
73
+ self._refresh_future = None
74
+ self._wakeup = None
75
+ self.closed = False
76
+
77
+ self.config = copy.copy(self.DEFAULT_CONFIG)
78
+ for key in self.config:
79
+ if key in configs:
80
+ self.config[key] = configs[key]
81
+
82
+ self._bootstrap_brokers = self._generate_bootstrap_brokers()
83
+ self._coordinator_brokers = {}
84
+
85
+ @property
86
+ def metadata_refresh_in_progress(self):
87
+ """True if a refresh is mid-flight."""
88
+ return self._refresh_future is not None and not self._refresh_future.is_done
89
+
90
+ def attach(self, manager):
91
+ """Wire this cluster to its connection manager.
92
+
93
+ Construction is split from attach so ClusterMetadata can be built
94
+ standalone (tests, snapshots) without a live manager. The reference is
95
+ held via weakref.proxy so that manager <-> cluster does not form a GC
96
+ cycle; manager.close() still calls cluster.close() to clear eagerly.
97
+ """
98
+ self._manager = weakref.proxy(manager)
99
+ self._wakeup = WakeupNotifier(self._manager._net)
100
+
101
+ def close(self):
102
+ self.closed = True
103
+ self._wakeup.notify()
104
+
105
+ def start_refresh_loop(self):
106
+ """Spawn the periodic refresh coroutine. Idempotent. Triggers bootstrap if needed."""
107
+ if self._manager is None:
108
+ raise RuntimeError('start_refresh_loop requires prior attach()')
109
+ if self._refresh_loop_future is not None:
110
+ return
111
+ self._refresh_loop_future = self._manager.call_soon(self._refresh_loop)
112
+
113
+ async def _refresh_loop(self):
114
+ """Awaits ttl() then triggers refresh_metadata(); request_update() wakes early."""
115
+ if self._manager is None:
116
+ raise RuntimeError('start_refresh_loop requires prior attach()')
117
+ if not self._manager.bootstrapped:
118
+ log.debug('Metadata refresh loop needs bootstrap...')
119
+ await self._manager.bootstrap_async()
120
+ log.info('Starting metadata refresh loop')
121
+ while not self.closed:
122
+ if self.metadata_refresh_in_progress:
123
+ await self._refresh_future
124
+ ttl_ms = self.ttl()
125
+ if ttl_ms == 0:
126
+ try:
127
+ await self.refresh_metadata()
128
+ except Errors.KafkaError as exc:
129
+ log.debug('Metadata refresh failed: %s', exc)
130
+ log.exception(exc)
131
+ continue
132
+ try:
133
+ log.debug('Sleeping %s for next Metadata refresh', ttl_ms / 1000)
134
+ await self._wakeup(ttl_ms / 1000)
135
+ except Exception as exc:
136
+ log.error('Metadata refresh loop error: %s', exc)
137
+ log.info('Stopping metadata refresh loop')
138
+
139
+ async def refresh_metadata(self, node_id=None):
140
+ """Send one MetadataRequest and apply the response.
141
+
142
+ Concurrent callers share a single in-flight request: if a refresh is
143
+ already underway, additional callers await the same Future and see the
144
+ same outcome (success or exception). This avoids duplicate broker
145
+ requests when bootstrap and the refresh loop race, or when external
146
+ callers invoke refresh while the loop is mid-flight.
147
+ """
148
+ if self._manager is None:
149
+ raise RuntimeError('refresh_metadata requires prior attach()')
150
+ if self.metadata_refresh_in_progress:
151
+ log.debug('Metadata refresh already in flight; awaiting existing')
152
+ await self._refresh_future
153
+ return
154
+ self._refresh_future = Future()
155
+ try:
156
+ await self._do_refresh_metadata(node_id)
157
+ except Exception as exc:
158
+ self._refresh_future.failure(exc)
159
+ raise
160
+ else:
161
+ self._refresh_future.success(None)
162
+
163
+ async def _do_refresh_metadata(self, node_id):
164
+ node_id = self._manager.least_loaded_node() if node_id is None else node_id
165
+ if node_id is None:
166
+ log.warning('No node available for metadata refresh - backoff/retry')
167
+ self._manager.update_backoff('metadata')
168
+ raise Errors.NodeNotReadyError('metadata')
169
+ else:
170
+ self._manager.reset_backoff('metadata')
171
+ log.info(f'Metadata refresh (node_id={node_id})')
172
+ try:
173
+ request = self.metadata_request()
174
+ log.debug("Sending metadata request %s to node %s", request, node_id)
175
+ response = await self._manager.send(request, node_id)
176
+ except Exception as exc:
177
+ log.error('Metadata refresh: failed %s', exc)
178
+ self.failed_update(exc)
179
+ raise
180
+ log.debug('Metadata refresh: success')
181
+ self.update_metadata(response)
182
+
183
+ def _generate_bootstrap_brokers(self):
184
+ # collect_hosts does not perform DNS, so we should be fine to re-use
185
+ bootstrap_hosts = collect_hosts(self.config['bootstrap_servers'])
186
+
187
+ if self.config['client_dns_lookup'] == 'resolve_canonical_bootstrap_servers_only':
188
+ bootstrap_hosts = expand_to_canonical_bootstrap_hosts(bootstrap_hosts)
189
+
190
+ brokers = {}
191
+ for i, (host, port, _) in enumerate(bootstrap_hosts):
192
+ node_id = 'bootstrap-%s' % i
193
+ brokers[node_id] = MetadataResponse.MetadataResponseBroker(node_id, host, port, None)
194
+ return brokers
195
+
196
+ def is_bootstrap(self, node_id):
197
+ return node_id in self._bootstrap_brokers
198
+
199
+ def set_topics(self, topics):
200
+ """Set specific topics to track for metadata.
201
+
202
+ Arguments:
203
+ topics (list of str): topics to check for metadata
204
+
205
+ Returns:
206
+ Future: resolves after metadata request/response
207
+ """
208
+ for topic in topics:
209
+ ensure_valid_topic_name(topic)
210
+ if not set(topics).difference(self._topics):
211
+ return Future().success(self)
212
+ # TODO: handle future when old metadata request is currently in-flight
213
+ # TODO: handle future when set_topics called multiple times before new request
214
+ self._topics = set(topics)
215
+ return self.request_update()
216
+
217
+ def add_topic(self, topic):
218
+ """Add a topic to the list of topics tracked via metadata.
219
+
220
+ Arguments:
221
+ topic (str): topic to track
222
+
223
+ Returns:
224
+ Future: resolves after metadata request/response
225
+
226
+ Raises:
227
+ TypeError: if topic is not a string
228
+ ValueError: if topic is invalid: must be chars (a-zA-Z0-9._-), and less than 250 length
229
+ """
230
+ ensure_valid_topic_name(topic)
231
+ if topic in self._topics:
232
+ return Future().success(self)
233
+ # TODO: handle future when old metadata request is currently in-flight
234
+ self._topics.add(topic)
235
+ return self.request_update()
236
+
237
+ def brokers(self):
238
+ """Get all MetadataResponseBroker
239
+
240
+ Returns:
241
+ list: [MetadataResponseBroker, ...]
242
+ """
243
+ return list(self._brokers.values())
244
+
245
+ def bootstrap_brokers(self):
246
+ """Get bootstrap brokers only, extracted from the
247
+ bootstrap_servers config option. Node ids are synthesized
248
+ as 'bootstrap-0' etc.
249
+
250
+ Returns:
251
+ list: [MetadataResponseBroker, ...]
252
+ """
253
+ return list(self._bootstrap_brokers.values())
254
+
255
+ def broker_metadata(self, broker_id):
256
+ """Get MetadataResponseBroker
257
+
258
+ Arguments:
259
+ broker_id (int or str): node_id for a broker to check
260
+
261
+ Returns:
262
+ MetadataResponseBroker or None if not found
263
+ """
264
+ return (
265
+ self._brokers.get(broker_id) or
266
+ self._bootstrap_brokers.get(broker_id) or
267
+ self._coordinator_brokers.get(broker_id)
268
+ )
269
+
270
+ def partitions_for_topic(self, topic):
271
+ """Return set of all partitions for topic (whether available or not)
272
+
273
+ Arguments:
274
+ topic (str): topic to check for partitions
275
+
276
+ Returns:
277
+ set: {partition (int), ...}
278
+ None if topic not found.
279
+ """
280
+ if topic not in self._partitions:
281
+ return None
282
+ return set(self._partitions[topic].keys())
283
+
284
+ def available_partitions_for_topic(self, topic):
285
+ """Return set of partitions with known leaders
286
+
287
+ Arguments:
288
+ topic (str): topic to check for partitions
289
+
290
+ Returns:
291
+ set: {partition (int), ...}
292
+ None if topic not found.
293
+ """
294
+ if topic not in self._partitions:
295
+ return None
296
+ return set([partition for partition, metadata
297
+ in self._partitions[topic].items()
298
+ if metadata.leader_id != -1])
299
+
300
+ def leader_for_partition(self, partition):
301
+ """Return node_id of leader, -1 unavailable, None if unknown."""
302
+ if partition.topic not in self._partitions:
303
+ return None
304
+ elif partition.partition not in self._partitions[partition.topic]:
305
+ return None
306
+ return self._partitions[partition.topic][partition.partition].leader_id
307
+
308
+ def is_replica_node(self, partition, node_id):
309
+ """Return MetadataResponseBroker for ``node_id`` only when it is
310
+ known AND still listed as a replica of ``partition`` (KIP-392).
311
+
312
+ Used by the consumer's preferred-read-replica routing to avoid
313
+ sending fetches to a broker that has been demoted out of the
314
+ partition's replica set even though it still exists as a node.
315
+
316
+ Arguments:
317
+ partition (TopicPartition): topic / partition to look up.
318
+ node_id (int): broker id to validate.
319
+
320
+ Returns:
321
+ MetadataResponseBroker if the node exists in cluster metadata
322
+ and is currently listed as a replica of ``partition``;
323
+ otherwise None.
324
+ """
325
+ broker = self.broker_metadata(node_id)
326
+ if broker is None:
327
+ return None
328
+ if partition.topic not in self._partitions:
329
+ return None
330
+ partition_data = self._partitions[partition.topic].get(partition.partition)
331
+ if partition_data is None:
332
+ return None
333
+ if node_id not in partition_data.replica_nodes:
334
+ return None
335
+ return broker
336
+
337
+ def leader_epoch_for_partition(self, partition):
338
+ """Return leader_epoch for partition, or None if topic/partition is unknown."""
339
+ if partition.topic not in self._partitions:
340
+ return None
341
+ elif partition.partition not in self._partitions[partition.topic]:
342
+ return None
343
+ return self._partitions[partition.topic][partition.partition].leader_epoch
344
+
345
+ def update_partition_leader(self, partition, leader_id, leader_epoch):
346
+ """Apply a KIP-951 current-leader hint from a Fetch/Produce response.
347
+
348
+ The cached leader id and epoch for ``partition`` are replaced only when
349
+ ``leader_epoch`` is strictly newer than the cached value (and
350
+ non-negative). When the leader id moves, ``_broker_partitions`` is
351
+ rewired so leader-based routing follows immediately.
352
+
353
+ Arguments:
354
+ partition (TopicPartition): topic / partition the hint is about.
355
+ leader_id (int): broker id named as the new leader.
356
+ leader_epoch (int): epoch of that new leader.
357
+
358
+ Returns:
359
+ bool: True iff cached state was changed.
360
+ """
361
+ with self._lock:
362
+ p_data = self._partitions.get(partition.topic, {}).get(partition.partition)
363
+ if p_data is None:
364
+ return False
365
+ if leader_epoch < 0 or leader_epoch <= p_data.leader_epoch:
366
+ return False
367
+ old_leader = p_data.leader_id
368
+ p_data.leader_id = leader_id
369
+ p_data.leader_epoch = leader_epoch
370
+ if old_leader != leader_id:
371
+ if old_leader in self._broker_partitions:
372
+ self._broker_partitions[old_leader].discard(partition)
373
+ if leader_id != -1:
374
+ self._broker_partitions[leader_id].add(partition)
375
+ return True
376
+
377
+ def partitions_for_broker(self, broker_id):
378
+ """Return TopicPartitions for which the broker is a leader.
379
+
380
+ Arguments:
381
+ broker_id (int or str): node id for a broker
382
+
383
+ Returns:
384
+ set: {TopicPartition, ...}
385
+ None if the broker either has no partitions or does not exist.
386
+ """
387
+ return self._broker_partitions.get(broker_id)
388
+
389
+ def get_coordinator(self, key, key_type=CoordinatorType.GROUP):
390
+ """Return node_id of group coordinator from cache.
391
+
392
+ Arguments:
393
+ key (str): name of consumer group or transaction_id
394
+ key_type (CoordinatorType, optional): Default GROUP
395
+
396
+ Returns:
397
+ node_id (int or str) for coordinator, -1 if coordinator unknown
398
+ None if the group does not exist.
399
+ """
400
+ return self._coordinators.get((key_type, key))
401
+
402
+ def ttl(self):
403
+ """Milliseconds until metadata should be refreshed"""
404
+ now = time.monotonic() * 1000
405
+ if self._manager is not None and self._manager.connection_delay('metadata'):
406
+ # Exponential backoff - KIP-580
407
+ return self._manager.connection_delay('metadata') * 1000
408
+ elif self._need_update:
409
+ ttl = 0
410
+ else:
411
+ metadata_age = now - self._last_successful_refresh_ms
412
+ ttl = self.config['metadata_max_age_ms'] - metadata_age
413
+
414
+ retry_age = now - self._last_refresh_ms
415
+ next_retry = self.config['retry_backoff_ms'] - retry_age
416
+
417
+ return max(ttl, next_retry, 0)
418
+
419
+ def refresh_backoff(self):
420
+ """Return milliseconds to wait before attempting to retry after failure"""
421
+ return self.config['retry_backoff_ms']
422
+
423
+ def request_update(self):
424
+ """Flags metadata for update, return Future()
425
+
426
+ Actual update must be handled separately. This method will only
427
+ change the reported ttl()
428
+
429
+ Returns:
430
+ kafka.future.Future (value will be the cluster object after update)
431
+ """
432
+ with self._lock:
433
+ self._need_update = True
434
+ if not self._future or self._future.is_done:
435
+ self._future = Future()
436
+ ret = self._future
437
+ if self._manager:
438
+ self.start_refresh_loop()
439
+ self._wakeup.notify()
440
+ return ret
441
+
442
+ @property
443
+ def need_update(self):
444
+ return self._need_update
445
+
446
+ def topics(self, exclude_internal_topics=True):
447
+ """Get set of known topics.
448
+
449
+ Arguments:
450
+ exclude_internal_topics (bool): Whether records from internal topics
451
+ (such as offsets) should be exposed to the consumer. If set to
452
+ True the only way to receive records from an internal topic is
453
+ subscribing to it. Default True
454
+
455
+ Returns:
456
+ set: {topic (str), ...}
457
+ """
458
+ topics = set(self._partitions.keys())
459
+ if exclude_internal_topics:
460
+ return topics - self.internal_topics
461
+ else:
462
+ return topics
463
+
464
+ def metadata_request(self):
465
+ if self.need_all_topic_metadata:
466
+ topics = MetadataRequest.ALL_TOPICS
467
+ elif not self._topics:
468
+ topics = MetadataRequest.NO_TOPICS
469
+ else:
470
+ topics = [MetadataRequest.MetadataRequestTopic(name=topic)
471
+ for topic in self._topics]
472
+ return MetadataRequest(
473
+ topics=topics,
474
+ allow_auto_topic_creation=self.config['allow_auto_create_topics'],
475
+ include_cluster_authorized_operations=False,
476
+ include_topic_authorized_operations=False,
477
+ )
478
+
479
+ def topic_id(self, topic_name):
480
+ """Return the topic UUID for ``topic_name``, or None if unknown.
481
+
482
+ Populated from MetadataResponse v10+ (Kafka 2.8+, KIP-516). Older
483
+ responses leave this empty.
484
+ """
485
+ return self._topic_ids.get(topic_name)
486
+
487
+ def topic_name_for_id(self, topic_id):
488
+ """Return the topic name for ``topic_id`` (uuid.UUID), or None.
489
+
490
+ Reverse lookup of :meth:`topic_id`. Populated from MetadataResponse
491
+ v10+ (KIP-516).
492
+ """
493
+ return self._topic_names_by_id.get(topic_id)
494
+
495
+ def failed_update(self, exception):
496
+ """Update cluster state given a failed MetadataRequest."""
497
+ f = None
498
+ with self._lock:
499
+ if self._future:
500
+ f = self._future
501
+ self._future = None
502
+ self._last_refresh_ms = time.monotonic() * 1000
503
+ if f:
504
+ f.failure(exception)
505
+
506
+ def update_metadata(self, metadata):
507
+ """Update cluster state given a MetadataResponse.
508
+
509
+ Arguments:
510
+ metadata (MetadataResponse): broker response to a metadata request
511
+
512
+ Returns: None
513
+ """
514
+ if not metadata.brokers:
515
+ log.warning("No broker metadata found in MetadataResponse -- ignoring.")
516
+ return self.failed_update(Errors.MetadataEmptyBrokerList(metadata))
517
+
518
+ _new_brokers = {}
519
+ for broker in metadata.brokers:
520
+ if metadata.API_VERSION == 0:
521
+ node_id, host, port = broker
522
+ rack = None
523
+ else:
524
+ node_id, host, port, rack = broker
525
+ _new_brokers.update({
526
+ node_id: MetadataResponse.MetadataResponseBroker(node_id, host, port, rack)
527
+ })
528
+
529
+ if metadata.API_VERSION == 0:
530
+ _new_controller = None
531
+ else:
532
+ _new_controller = _new_brokers.get(metadata.controller_id)
533
+
534
+ if metadata.API_VERSION < 2:
535
+ _new_cluster_id = None
536
+ else:
537
+ _new_cluster_id = metadata.cluster_id
538
+
539
+ _new_partitions = {}
540
+ _new_broker_partitions = collections.defaultdict(set)
541
+ _new_unauthorized_topics = set()
542
+ _new_internal_topics = set()
543
+ _new_topic_ids = {}
544
+ _new_topic_names_by_id = {}
545
+ _retry_topics = set()
546
+
547
+ # KAFKA-9212: pre-2.4 brokers may emit stale leader_epoch values
548
+ # during partition reassignment (the controller failed to update
549
+ # its own cached epoch before sending UpdateMetadata, so the
550
+ # propagated epoch lags behind the actual leader's). Caching that
551
+ # stale value would loop us on FENCED_LEADER_EPOCH for every
552
+ # subsequent ListOffsets / Fetch / OffsetCommit. The Java client's
553
+ # fix gates on response version >= 9 (Kafka 2.4+, where the
554
+ # controller bug is fixed); we do the same.
555
+ epoch_reliable = metadata.API_VERSION >= 9
556
+
557
+ for t in metadata.topics:
558
+ topic = t.name
559
+ if t.is_internal:
560
+ _new_internal_topics.add(topic)
561
+ error_type = Errors.for_code(t.error_code)
562
+ new_topic_id = t.topic_id
563
+ recreated = False
564
+ if new_topic_id is not None and topic is not None:
565
+ prior = self._topic_ids.get(topic)
566
+ if prior is not None and prior != new_topic_id:
567
+ log.warning(
568
+ "Topic %s topic_id changed from %s to %s -- likely"
569
+ " recreated; resetting cached leader epochs.",
570
+ topic, prior, new_topic_id)
571
+ recreated = True
572
+ _new_topic_ids[topic] = new_topic_id
573
+ _new_topic_names_by_id[new_topic_id] = topic
574
+ if error_type is Errors.NoError:
575
+ _new_partitions[topic] = {}
576
+ for p_data in t.partitions:
577
+ partition = p_data.partition_index
578
+ if not epoch_reliable or recreated:
579
+ p_data.leader_epoch = -1
580
+ _new_partitions[topic][partition] = p_data
581
+ if p_data.leader_id != -1:
582
+ _new_broker_partitions[p_data.leader_id].add(
583
+ TopicPartition(topic, partition))
584
+
585
+ # Only log errors for topics we are specifically tracking
586
+ elif topic in self._topics:
587
+ if issubclass(error_type, Errors.RetriableError):
588
+ _retry_topics.add(topic)
589
+ if error_type is Errors.LeaderNotAvailableError:
590
+ log.warning("Topic %s is not available during auto-create"
591
+ " initialization", topic)
592
+ elif error_type is Errors.UnknownTopicOrPartitionError:
593
+ log.error("Topic %s not found in cluster metadata", topic)
594
+ elif error_type is Errors.TopicAuthorizationFailedError:
595
+ log.error("Topic %s is not authorized for this client", topic)
596
+ _new_unauthorized_topics.add(topic)
597
+ elif error_type is Errors.InvalidTopicError:
598
+ log.error("'%s' is not a valid topic name", topic)
599
+ if topic in self._topics:
600
+ self._topics.remove(topic)
601
+ else:
602
+ log.error("Error fetching metadata for topic %s: %s",
603
+ topic, error_type)
604
+
605
+ with self._lock:
606
+ self._brokers = _new_brokers
607
+ self.controller = _new_controller
608
+ self.cluster_id = _new_cluster_id
609
+ self._partitions = _new_partitions
610
+ self._broker_partitions = _new_broker_partitions
611
+ self.unauthorized_topics = _new_unauthorized_topics
612
+ self.internal_topics = _new_internal_topics
613
+ # Pre-v10 responses don't carry topic_id, so the wholesale swap
614
+ # would clobber known ids during a rolling downgrade (or any
615
+ # cross-broker version skew). Only replace the index when the
616
+ # response actually had a chance to populate it.
617
+ if metadata.API_VERSION >= 10:
618
+ self._topic_ids = _new_topic_ids
619
+ self._topic_names_by_id = _new_topic_names_by_id
620
+ self._need_update = len(_retry_topics) > 0
621
+ f = None
622
+ if self._future:
623
+ f = self._future
624
+ self._future = None
625
+
626
+ now = time.monotonic() * 1000
627
+ self._last_refresh_ms = now
628
+ self._last_successful_refresh_ms = now
629
+
630
+ if f:
631
+ # In the common case where we ask for a single topic and get back an
632
+ # error, we should fail the future
633
+ if len(metadata.topics) == 1 and metadata.topics[0][0] != Errors.NoError.errno:
634
+ error_code, topic = metadata.topics[0][:2]
635
+ error = Errors.for_code(error_code)(topic)
636
+ f.failure(error)
637
+ else:
638
+ f.success(self)
639
+
640
+ log.info("Updated metadata: %s", self)
641
+
642
+ for listener in self._listeners:
643
+ listener(self)
644
+
645
+ def add_listener(self, listener):
646
+ """Add a callback function to be called on each metadata update"""
647
+ self._listeners.add(listener)
648
+
649
+ def remove_listener(self, listener):
650
+ """Remove a previously added listener callback."""
651
+ try:
652
+ self._listeners.remove(listener)
653
+ except KeyError:
654
+ pass
655
+
656
+ def add_coordinator(self, response, key_type, key, synthesize_node_id=True):
657
+ """Update with metadata for a group or txn coordinator
658
+
659
+ Arguments:
660
+ response (FindCoordinatorResponse): broker response
661
+ key_type (CoordinatorType): GROUP / TRANSACTION / SHARE
662
+ key (str): consumer_group or transactional_id
663
+ synthesize_node_id (bool): If True synthesizes a unique
664
+ node_id to generate a dedicated network connection for
665
+ coordinator requests. Default: True.
666
+
667
+ Returns:
668
+ string: coordinator node_id.
669
+
670
+ Raises:
671
+ BrokerResponseError: if ``response.error_code`` is non-zero.
672
+ """
673
+ key_type = CoordinatorType.build_from(key_type)
674
+ log.debug("Updating coordinator for %s/%s: %s", key_type.name, key, response)
675
+ error_type = Errors.for_code(response.error_code)
676
+ if error_type is not Errors.NoError:
677
+ raise error_type(
678
+ "FindCoordinatorResponse error for %s/%s: %s"
679
+ % (key_type.name, key, getattr(response, 'error_message', '')))
680
+
681
+ # Use a coordinator-specific node id so that requests
682
+ # get a dedicated connection
683
+ if synthesize_node_id:
684
+ node_id = 'coordinator-{}'.format(response.node_id)
685
+ else:
686
+ node_id = response.node_id
687
+ coordinator = MetadataResponse.MetadataResponseBroker(
688
+ node_id,
689
+ response.host,
690
+ response.port,
691
+ None)
692
+
693
+ log.info("Coordinator for %s/%s is %s", key_type.name, key, coordinator)
694
+ self._coordinator_brokers[node_id] = coordinator
695
+ self._coordinators[(key_type, key)] = node_id
696
+ return node_id
697
+
698
+ def __str__(self):
699
+ return 'ClusterMetadata(brokers: %d, topics: %d, coordinators: %d)' % \
700
+ (len(self._brokers), len(self._partitions), len(self._coordinators))
701
+
702
+
703
+ def collect_hosts(hosts, randomize=True):
704
+ """
705
+ Processes a list (or comma-separated string) of hosts strings (host:port)
706
+ and returns a list of (host, port, family) tuples.
707
+ Optionally randomizes the returned list.
708
+ """
709
+
710
+ if isinstance(hosts, str):
711
+ hosts = hosts.strip().split(',')
712
+
713
+ result = []
714
+ for host_port in hosts:
715
+ # ignore leading SECURITY_PROTOCOL:// to mimic java client
716
+ host_port = re.sub('^.*://', '', host_port)
717
+ host, port, afi = get_ip_port_afi(host_port)
718
+ result.append((host, port, afi))
719
+
720
+ if randomize:
721
+ random.shuffle(result)
722
+ return result
723
+
724
+
725
+ def expand_to_canonical_bootstrap_hosts(hosts):
726
+ """Expand each bootstrap entry to one entry per canonical FQDN.
727
+
728
+ Mirrors Java's ``client.dns.lookup=resolve_canonical_bootstrap_servers_only``:
729
+ forward-resolve each host, take the ``canonname`` reported by the resolver,
730
+ and emit one bootstrap entry per unique canonical name. Useful for
731
+ Kerberos round-robin DNS deployments where the principal must match each
732
+ individual broker FQDN.
733
+
734
+ If a host fails to resolve, the original entry is preserved verbatim --
735
+ matching Java's best-effort behaviour so bootstrap doesn't fail outright.
736
+ """
737
+ expanded = []
738
+ for host, port, afi in hosts:
739
+ try:
740
+ addrinfos = socket.getaddrinfo(
741
+ host, port, afi, socket.SOCK_STREAM, 0, socket.AI_CANONNAME)
742
+ except socket.gaierror as exc:
743
+ log.warning('Canonical bootstrap resolution failed for %s:%s: %s; '
744
+ 'keeping original entry', host, port, exc)
745
+ expanded.append((host, port, afi))
746
+ continue
747
+ seen = set()
748
+ for family, _socktype, _proto, canonname, _sockaddr in addrinfos:
749
+ name = canonname or host
750
+ if name in seen:
751
+ continue
752
+ seen.add(name)
753
+ expanded.append((name, port, family))
754
+ return expanded
755
+
756
+
757
+ def _address_family(address):
758
+ """
759
+ Attempt to determine the family of an address (or hostname)
760
+
761
+ :return: either socket.AF_INET or socket.AF_INET6 or socket.AF_UNSPEC if the address family
762
+ could not be determined
763
+ """
764
+ if address.startswith('[') and address.endswith(']'):
765
+ return socket.AF_INET6
766
+ for af in (socket.AF_INET, socket.AF_INET6):
767
+ try:
768
+ socket.inet_pton(af, address)
769
+ return af
770
+ except (ValueError, AttributeError, socket.error):
771
+ continue
772
+ return socket.AF_UNSPEC
773
+
774
+
775
+ DEFAULT_KAFKA_PORT = 9092
776
+
777
+
778
+ def get_ip_port_afi(host_and_port_str):
779
+ """
780
+ Parse the IP and port from a string in the format of:
781
+
782
+ * host_or_ip <- Can be either IPv4 address literal or hostname/fqdn
783
+ * host_or_ipv4:port <- Can be either IPv4 address literal or hostname/fqdn
784
+ * [host_or_ip] <- IPv6 address literal
785
+ * [host_or_ip]:port. <- IPv6 address literal
786
+
787
+ .. note:: IPv6 address literals with ports *must* be enclosed in brackets
788
+
789
+ .. note:: If the port is not specified, default will be returned.
790
+
791
+ :return: tuple (host, port, afi), afi will be socket.AF_INET or socket.AF_INET6 or socket.AF_UNSPEC
792
+ """
793
+ host_and_port_str = host_and_port_str.strip()
794
+ if host_and_port_str.startswith('['):
795
+ af = socket.AF_INET6
796
+ host, rest = host_and_port_str[1:].split(']')
797
+ if rest:
798
+ port = int(rest[1:])
799
+ else:
800
+ port = DEFAULT_KAFKA_PORT
801
+ return host, port, af
802
+ else:
803
+ if ':' not in host_and_port_str:
804
+ af = _address_family(host_and_port_str)
805
+ return host_and_port_str, DEFAULT_KAFKA_PORT, af
806
+ else:
807
+ # now we have something with a colon in it and no square brackets. It could be
808
+ # either an IPv6 address literal (e.g., "::1") or an IP:port pair or a host:port pair
809
+ try:
810
+ # if it decodes as an IPv6 address, use that
811
+ socket.inet_pton(socket.AF_INET6, host_and_port_str)
812
+ return host_and_port_str, DEFAULT_KAFKA_PORT, socket.AF_INET6
813
+ except AttributeError:
814
+ log.warning('socket.inet_pton not available on this platform.'
815
+ ' consider `pip install win_inet_pton`')
816
+ pass
817
+ except (ValueError, socket.error):
818
+ # it's a host:port pair
819
+ pass
820
+ host, port = host_and_port_str.rsplit(':', 1)
821
+ port = int(port)
822
+
823
+ af = _address_family(host)
824
+ return host, port, af