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
@@ -0,0 +1,897 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections import OrderedDict
3
+ from collections.abc import Sequence
4
+ from enum import IntEnum
5
+ import logging
6
+ import random
7
+ import re
8
+ import threading
9
+ import time
10
+
11
+ import kafka.errors as Errors
12
+ from kafka.protocol.consumer import OffsetResetStrategy
13
+ from kafka.structs import OffsetAndMetadata
14
+ from kafka.util import ensure_valid_topic_name, synchronized
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ class SubscriptionType(IntEnum):
20
+ NONE = 0
21
+ AUTO_TOPICS = 1
22
+ AUTO_PATTERN = 2
23
+ USER_ASSIGNED = 3
24
+
25
+
26
+ class SubscriptionState:
27
+ """
28
+ A class for tracking the topics, partitions, and offsets for the consumer.
29
+ A partition is "assigned" either directly with assign_from_user() (manual
30
+ assignment) or with assign_from_subscribed() (automatic assignment from
31
+ subscription).
32
+
33
+ Once assigned, the partition is not considered "fetchable" until its initial
34
+ position has been set with seek(). Fetchable partitions track a fetch
35
+ position which is used to set the offset of the next fetch, and a consumed
36
+ position which is the last offset that has been returned to the user. You
37
+ can suspend fetching from a partition through pause() without affecting the
38
+ fetched/consumed offsets. The partition will remain unfetchable until the
39
+ resume() is used. You can also query the pause state independently with
40
+ is_paused().
41
+
42
+ Note that pause state as well as fetch/consumed positions are not preserved
43
+ when partition assignment is changed whether directly by the user or
44
+ through a group rebalance.
45
+ """
46
+ _SUBSCRIPTION_EXCEPTION_MESSAGE = (
47
+ "You must choose only one way to configure your consumer:"
48
+ " (1) subscribe to specific topics by name,"
49
+ " (2) subscribe to topics matching a regex pattern,"
50
+ " (3) assign itself specific topic-partitions.")
51
+
52
+ def __init__(self, offset_reset_strategy='earliest'):
53
+ """Initialize a SubscriptionState instance
54
+
55
+ Keyword Arguments:
56
+ offset_reset_strategy: 'earliest' or 'latest', otherwise
57
+ exception will be raised when fetching an offset that is no
58
+ longer available. Default: 'earliest'
59
+ """
60
+ try:
61
+ offset_reset_strategy = getattr(OffsetResetStrategy,
62
+ offset_reset_strategy.upper())
63
+ except AttributeError:
64
+ log.warning('Unrecognized offset_reset_strategy, using NONE')
65
+ offset_reset_strategy = OffsetResetStrategy.NONE
66
+ self._default_offset_reset_strategy = offset_reset_strategy
67
+
68
+ self.subscription = None # set() or None
69
+ self.subscription_type = SubscriptionType.NONE
70
+ self.subscribed_pattern = None # regex str or None
71
+ self._group_subscription = set()
72
+ self._user_assignment = set()
73
+ self.assignment = OrderedDict()
74
+ self.rebalance_listener = None
75
+ self.listeners = []
76
+ self._lock = threading.RLock()
77
+
78
+ def _set_subscription_type(self, subscription_type):
79
+ if not isinstance(subscription_type, SubscriptionType):
80
+ raise ValueError('SubscriptionType enum required')
81
+ if self.subscription_type == SubscriptionType.NONE:
82
+ self.subscription_type = subscription_type
83
+ elif self.subscription_type != subscription_type:
84
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
85
+
86
+ @synchronized
87
+ def subscribe(self, topics=(), pattern=None, listener=None):
88
+ """Subscribe to a list of topics, or a topic regex pattern.
89
+
90
+ Partitions will be dynamically assigned via a group coordinator.
91
+ Topic subscriptions are not incremental: this list will replace the
92
+ current assignment (if there is one).
93
+
94
+ This method is incompatible with assign_from_user()
95
+
96
+ Arguments:
97
+ topics (list): List of topics for subscription.
98
+ pattern (str): Pattern to match available topics. You must provide
99
+ either topics or pattern, but not both.
100
+ listener (ConsumerRebalanceListener): Optionally include listener
101
+ callback, which will be called before and after each rebalance
102
+ operation.
103
+
104
+ As part of group management, the consumer will keep track of the
105
+ list of consumers that belong to a particular group and will
106
+ trigger a rebalance operation if one of the following events
107
+ trigger:
108
+
109
+ * Number of partitions change for any of the subscribed topics
110
+ * Topic is created or deleted
111
+ * An existing member of the consumer group dies
112
+ * A new member is added to the consumer group
113
+
114
+ When any of these events are triggered, the provided listener
115
+ will be invoked first to indicate that the consumer's assignment
116
+ has been revoked, and then again when the new assignment has
117
+ been received. Note that this listener will immediately override
118
+ any listener set in a previous call to subscribe. It is
119
+ guaranteed, however, that the partitions revoked/assigned
120
+ through this interface are from topics subscribed in this call.
121
+
122
+ Raises:
123
+ ValueError: if neither topics nor pattern provided.
124
+ IllegalStateError: if both topics and pattern provided.
125
+ TypeError: if topics is not a list/sequence, or listener is not
126
+ a AsyncConsumerRebalanceListener or ConsumerRebalanceListener.
127
+ """
128
+ if not topics and not pattern:
129
+ raise ValueError('Must provide topics or pattern')
130
+ if (topics and pattern):
131
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
132
+
133
+ elif pattern:
134
+ self._set_subscription_type(SubscriptionType.AUTO_PATTERN)
135
+ log.info('Subscribing to pattern: /%s/', pattern)
136
+ self.subscription = set()
137
+ self.subscribed_pattern = re.compile(pattern)
138
+ else:
139
+ if isinstance(topics, str) or not isinstance(topics, Sequence):
140
+ raise TypeError('Topics must be a list (or non-str sequence)')
141
+ self._set_subscription_type(SubscriptionType.AUTO_TOPICS)
142
+ self.change_subscription(topics)
143
+
144
+ if listener and not isinstance(
145
+ listener, (ConsumerRebalanceListener, AsyncConsumerRebalanceListener)):
146
+ raise TypeError(
147
+ 'listener must be a ConsumerRebalanceListener or AsyncConsumerRebalanceListener')
148
+ self.rebalance_listener = listener
149
+
150
+ @synchronized
151
+ def change_subscription(self, topics):
152
+ """Change the topic subscription.
153
+
154
+ Arguments:
155
+ topics (list of str): topics for subscription
156
+
157
+ Raises:
158
+ IllegalStateError: if assign_from_user has been used already
159
+ TypeError: if a topic is None or a non-str
160
+ ValueError: if a topic is an empty string or
161
+ - a topic name is '.' or '..' or
162
+ - a topic name does not consist of ASCII-characters/'-'/'_'/'.'
163
+ """
164
+ if not self.partitions_auto_assigned():
165
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
166
+
167
+ if isinstance(topics, str):
168
+ topics = [topics]
169
+
170
+ if self.subscription == set(topics):
171
+ log.warning("subscription unchanged by change_subscription(%s)",
172
+ topics)
173
+ return
174
+
175
+ for t in topics:
176
+ ensure_valid_topic_name(t)
177
+
178
+ log.info('Updating subscribed topics to: %s', topics)
179
+ self.subscription = set(topics)
180
+ self._group_subscription.update(topics)
181
+
182
+ @synchronized
183
+ def group_subscribe(self, topics):
184
+ """Add topics to the current group subscription.
185
+
186
+ This is used by the group leader to ensure that it receives metadata
187
+ updates for all topics that any member of the group is subscribed to.
188
+
189
+ Arguments:
190
+ topics (list of str): topics to add to the group subscription
191
+ """
192
+ if not self.partitions_auto_assigned():
193
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
194
+ self._group_subscription.update(topics)
195
+
196
+ @synchronized
197
+ def reset_group_subscription(self):
198
+ """Reset the group's subscription to only contain topics subscribed by this consumer."""
199
+ if not self.partitions_auto_assigned():
200
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
201
+ if self.subscription is None:
202
+ raise Errors.IllegalStateError('Subscription required')
203
+ self._group_subscription.intersection_update(self.subscription)
204
+
205
+ @synchronized
206
+ def assign_from_user(self, partitions):
207
+ """Manually assign a list of TopicPartitions to this consumer.
208
+
209
+ The new assignment replaces the previous one (this is not an
210
+ incremental-add API), but ``TopicPartitionState`` is preserved
211
+ for any partition that's present in both the old and new
212
+ assignment. Manual topic assignment through this method does
213
+ not use the consumer's group management functionality. As
214
+ such, there will be no rebalance operation triggered when
215
+ group membership or cluster and topic metadata change. Note
216
+ that it is not possible to use both manual partition
217
+ assignment with assign() and group assignment with subscribe().
218
+
219
+ Arguments:
220
+ partitions (list of TopicPartition): assignment for this instance.
221
+
222
+ Raises:
223
+ IllegalStateError: if consumer has already called subscribe()
224
+ """
225
+ self._set_subscription_type(SubscriptionType.USER_ASSIGNED)
226
+ if self._user_assignment == set(partitions):
227
+ return
228
+ self._user_assignment = set(partitions)
229
+ self._apply_assignment(partitions)
230
+
231
+ @synchronized
232
+ def assign_from_subscribed(self, assignments):
233
+ """Update the assignment to the specified partitions.
234
+
235
+ This method is called by the coordinator to dynamically assign
236
+ partitions based on the consumer's topic subscription. Differs
237
+ from :meth:`assign_from_user` which directly sets the assignment
238
+ from a user-supplied TopicPartition list.
239
+
240
+ Preserves ``TopicPartitionState`` (position, paused flag,
241
+ preferred read replica, fetch buffers tied to the partition)
242
+ for any partition present in both the prior and new assignments.
243
+
244
+ Validation raises ``ValueError`` BEFORE any mutation if a
245
+ partition's topic isn't subscribed.
246
+
247
+ Arguments:
248
+ assignments (list of TopicPartition): the *full* new
249
+ assignment (not a diff). Partitions present in both
250
+ the old and new assignment retain their state;
251
+ revoked partitions are dropped; new partitions get
252
+ fresh state.
253
+ """
254
+ if not self.partitions_auto_assigned():
255
+ raise Errors.IllegalStateError(self._SUBSCRIPTION_EXCEPTION_MESSAGE)
256
+
257
+ for tp in assignments:
258
+ if tp.topic not in self.subscription:
259
+ raise ValueError("Assigned partition %s for non-subscribed topic." % (tp,))
260
+
261
+ # randomize new-partition insertion order so short-lived
262
+ # consumers (CLI tools, one-shot jobs) that re-run from scratch
263
+ # don't keep starting on the same partition. The Fetcher
264
+ # iterates fetchable_partitions() in self.assignment insertion
265
+ # order; without the shuffle, partition 0 of the
266
+ # alphabetically-first topic always wins the first fetch and
267
+ # short-lived runs that bail before exhausting it never see
268
+ # later partitions.
269
+ self._apply_assignment(assignments, randomize=True)
270
+ log.info("Updated partition assignment: %s", assignments)
271
+
272
+ def _apply_assignment(self, partitions, randomize=False):
273
+ """Mutate ``self.assignment`` in place to contain exactly
274
+ ``partitions``, preserving the existing ``TopicPartitionState``
275
+ for any partition present in both the old and new sets.
276
+
277
+ Shared between :meth:`assign_from_user` and
278
+ :meth:`assign_from_subscribed` - the algorithm is identical;
279
+ only the validation around it differs.
280
+
281
+ When ``randomize=True``, the *newly-added* partitions are
282
+ inserted in random order. Kept partitions retain their
283
+ existing position regardless. This is intended for the
284
+ coordinator-driven path (assign_from_subscribed) - see the
285
+ comment at that call site for rationale.
286
+ """
287
+ new_set = set(partitions)
288
+ # Drop revoked partitions (we mutate self.assignment, so list()
289
+ # the keys first to avoid "dict changed size during iteration").
290
+ for tp in list(self.assignment.keys()):
291
+ if tp not in new_set:
292
+ del self.assignment[tp]
293
+ # Add new partitions; kept partitions retain their existing
294
+ # TopicPartitionState (positions, paused flag, KIP-392 cache,
295
+ # etc.).
296
+ new_partitions = [tp for tp in partitions if tp not in self.assignment]
297
+ if randomize:
298
+ random.shuffle(new_partitions)
299
+ for tp in new_partitions:
300
+ self.assignment[tp] = TopicPartitionState()
301
+
302
+ @synchronized
303
+ def unsubscribe(self):
304
+ """Clear all topic subscriptions and partition assignments"""
305
+ self.subscription = None
306
+ self._user_assignment.clear()
307
+ self.assignment.clear()
308
+ self.subscribed_pattern = None
309
+ self.subscription_type = SubscriptionType.NONE
310
+
311
+ @synchronized
312
+ def group_subscription(self):
313
+ """Get the topic subscription for the group.
314
+
315
+ For the leader, this will include the union of all member subscriptions.
316
+ For followers, it is the member's subscription only.
317
+
318
+ This is used when querying topic metadata to detect metadata changes
319
+ that would require rebalancing (the leader fetches metadata for all
320
+ topics in the group so that it can do partition assignment).
321
+
322
+ Returns:
323
+ set: topics
324
+ """
325
+ return self._group_subscription
326
+
327
+ @synchronized
328
+ def seek(self, partition, offset):
329
+ """Manually specify the fetch offset for a TopicPartition.
330
+
331
+ Overrides the fetch offsets that the consumer will use on the next
332
+ poll(). If this API is invoked for the same partition more than once,
333
+ the latest offset will be used on the next poll(). Note that you may
334
+ lose data if this API is arbitrarily used in the middle of consumption,
335
+ to reset the fetch offsets.
336
+
337
+ Arguments:
338
+ partition (TopicPartition): partition for seek operation
339
+ offset (int or OffsetAndMetadata): message offset in partition
340
+ """
341
+ if not isinstance(offset, (int, OffsetAndMetadata)):
342
+ raise TypeError("offset must be type in or OffsetAndMetadata")
343
+ self.assignment[partition].seek(offset)
344
+
345
+ @synchronized
346
+ def assigned_partitions(self):
347
+ """Return set of TopicPartitions in current assignment."""
348
+ return set(self.assignment.keys())
349
+
350
+ @synchronized
351
+ def paused_partitions(self):
352
+ """Return current set of paused TopicPartitions."""
353
+ return set(partition for partition in self.assignment
354
+ if self.is_paused(partition))
355
+
356
+ @synchronized
357
+ def fetchable_partitions(self):
358
+ """Return ordered list of TopicPartitions that should be Fetched."""
359
+ fetchable = list()
360
+ for partition, state in self.assignment.items():
361
+ if state.is_fetchable():
362
+ fetchable.append(partition)
363
+ return fetchable
364
+
365
+ @synchronized
366
+ def partitions_auto_assigned(self):
367
+ """Return True unless user supplied partitions manually."""
368
+ return self.subscription_type in (SubscriptionType.AUTO_TOPICS, SubscriptionType.AUTO_PATTERN)
369
+
370
+ @synchronized
371
+ def all_consumed_offsets(self):
372
+ """Returns consumed offsets as {TopicPartition: OffsetAndMetadata}"""
373
+ all_consumed = {}
374
+ for partition, state in self.assignment.items():
375
+ if state.has_valid_position:
376
+ all_consumed[partition] = state.position
377
+ return all_consumed
378
+
379
+ @synchronized
380
+ def request_offset_reset(self, partition, offset_reset_strategy=None):
381
+ """Mark partition for offset reset using specified or default strategy.
382
+
383
+ Arguments:
384
+ partition (TopicPartition): partition to mark
385
+ offset_reset_strategy (OffsetResetStrategy, optional)
386
+ """
387
+ if offset_reset_strategy is None:
388
+ offset_reset_strategy = self._default_offset_reset_strategy
389
+ self.assignment[partition].reset(offset_reset_strategy)
390
+
391
+ @synchronized
392
+ def set_reset_pending(self, partitions, next_allowed_reset_time):
393
+ for partition in partitions:
394
+ self.assignment[partition].set_reset_pending(next_allowed_reset_time)
395
+
396
+ @synchronized
397
+ def has_default_offset_reset_policy(self):
398
+ """Return True if default offset reset policy is Earliest or Latest"""
399
+ return self._default_offset_reset_strategy != OffsetResetStrategy.NONE
400
+
401
+ @synchronized
402
+ def is_offset_reset_needed(self, partition):
403
+ return self.assignment[partition].awaiting_reset
404
+
405
+ @synchronized
406
+ def has_all_fetch_positions(self):
407
+ for state in self.assignment.values():
408
+ if not state.has_valid_position:
409
+ return False
410
+ return True
411
+
412
+ @synchronized
413
+ def missing_fetch_positions(self):
414
+ missing = set()
415
+ for partition, state in self.assignment.items():
416
+ if state.is_missing_position():
417
+ missing.add(partition)
418
+ return missing
419
+
420
+ @synchronized
421
+ def has_valid_position(self, partition):
422
+ return partition in self.assignment and self.assignment[partition].has_valid_position
423
+
424
+ @synchronized
425
+ def reset_missing_positions(self):
426
+ partitions_with_no_offsets = set()
427
+ for tp, state in self.assignment.items():
428
+ if state.is_missing_position():
429
+ if self._default_offset_reset_strategy == OffsetResetStrategy.NONE:
430
+ partitions_with_no_offsets.add(tp)
431
+ else:
432
+ state.reset(self._default_offset_reset_strategy)
433
+
434
+ if partitions_with_no_offsets:
435
+ raise Errors.NoOffsetForPartitionError(partitions_with_no_offsets)
436
+
437
+ @synchronized
438
+ def partitions_needing_reset(self):
439
+ partitions = set()
440
+ for tp, state in self.assignment.items():
441
+ if state.awaiting_reset and state.is_reset_allowed():
442
+ partitions.add(tp)
443
+ return partitions
444
+
445
+ @synchronized
446
+ def next_offset_reset_retry_time(self):
447
+ times = [state.next_allowed_retry_time
448
+ for state in self.assignment.values()
449
+ if state.awaiting_reset and state.next_allowed_retry_time is not None]
450
+ return min(times) if times else None
451
+
452
+ @synchronized
453
+ def maybe_validate_position_for_current_leader(self, partition, current_leader_epoch):
454
+ if partition not in self.assignment:
455
+ return False
456
+ return self.assignment[partition].maybe_validate_position(current_leader_epoch)
457
+
458
+ @synchronized
459
+ def request_position_validation(self, partition):
460
+ if partition not in self.assignment:
461
+ return False
462
+ return self.assignment[partition].request_position_validation()
463
+
464
+ @synchronized
465
+ def partitions_needing_validation(self):
466
+ partitions = set()
467
+ for tp, state in self.assignment.items():
468
+ if state.awaiting_validation and state.is_validation_allowed():
469
+ partitions.add(tp)
470
+ return partitions
471
+
472
+ @synchronized
473
+ def next_offset_validation_retry_time(self):
474
+ times = [state.next_allowed_retry_time
475
+ for state in self.assignment.values()
476
+ if state.awaiting_validation and state.next_allowed_retry_time is not None]
477
+ return min(times) if times else None
478
+
479
+ @synchronized
480
+ def set_validation_pending(self, partitions, next_allowed_retry_time):
481
+ for partition in partitions:
482
+ self.assignment[partition].set_validation_pending(next_allowed_retry_time)
483
+
484
+ @synchronized
485
+ def validation_failed(self, partitions, next_allowed_retry_time):
486
+ for partition in partitions:
487
+ self.assignment[partition].validation_failed(next_allowed_retry_time)
488
+
489
+ @synchronized
490
+ def complete_validation(self, partition, validated_position=None):
491
+ if partition in self.assignment:
492
+ self.assignment[partition].complete_validation(validated_position)
493
+
494
+ @synchronized
495
+ def is_offset_validation_needed(self, partition):
496
+ return partition in self.assignment and self.assignment[partition].awaiting_validation
497
+
498
+ @synchronized
499
+ def is_assigned(self, partition):
500
+ return partition in self.assignment
501
+
502
+ @synchronized
503
+ def is_paused(self, partition):
504
+ return partition in self.assignment and self.assignment[partition].paused
505
+
506
+ @synchronized
507
+ def is_fetchable(self, partition):
508
+ return partition in self.assignment and self.assignment[partition].is_fetchable()
509
+
510
+ @synchronized
511
+ def pause(self, partition):
512
+ self.assignment[partition].pause()
513
+
514
+ @synchronized
515
+ def resume(self, partition):
516
+ self.assignment[partition].resume()
517
+
518
+ @synchronized
519
+ def mark_pending_revocation(self, partitions):
520
+ """KIP-429: gate ``is_fetchable()`` for each partition's state
521
+ so the fetcher would skip them while an on_partitions_revoked /
522
+ on_partitions_lost listener runs. Called immediately before
523
+ invoking the listener. The flag is single-shot - the
524
+ surrounding ``assign_from_subscribed`` drops the
525
+ ``TopicPartitionState`` for revoked partitions when the
526
+ listener returns.
527
+
528
+ Currently a no-op while the user thread is blocked in ``_net.run``
529
+ during rebalance and so the only path that calls ``send_fetches``
530
+ cannot fire. Kept as a defensive gate in case this changes in
531
+ the future."""
532
+ for tp in partitions:
533
+ if tp in self.assignment:
534
+ self.assignment[tp].mark_pending_revocation()
535
+
536
+ @synchronized
537
+ def reset_failed(self, partitions, next_retry_time):
538
+ for partition in partitions:
539
+ self.assignment[partition].reset_failed(next_retry_time)
540
+
541
+ @synchronized
542
+ def move_partition_to_end(self, partition):
543
+ if partition in self.assignment:
544
+ try:
545
+ self.assignment.move_to_end(partition)
546
+ except AttributeError:
547
+ state = self.assignment.pop(partition)
548
+ self.assignment[partition] = state
549
+
550
+ @synchronized
551
+ def position(self, partition):
552
+ return self.assignment[partition].position
553
+
554
+
555
+ class TopicPartitionState:
556
+ def __init__(self):
557
+ self.paused = False # whether this partition has been paused by the user
558
+ self.reset_strategy = None # the reset strategy if awaiting_reset is set
559
+ self._position = None # OffsetAndMetadata exposed to the user
560
+ self.highwater = None
561
+ self.drop_pending_record_batch = False
562
+ self.next_allowed_retry_time = None
563
+ # KIP-320: offset validation state. _awaiting_validation gates fetches
564
+ # until OffsetForLeaderEpoch confirms the position is consistent with
565
+ # the current leader's log; mutually exclusive with awaiting_reset.
566
+ self._awaiting_validation = False
567
+ # KIP-392: preferred read replica chosen by the broker (rack-aware).
568
+ # ``_preferred_read_replica_expiration`` is a monotonic deadline; after
569
+ # it passes we fall back to the leader and re-learn. Cleared on
570
+ # replica-related errors so the next fetch goes to the leader.
571
+ self._preferred_read_replica = None
572
+ self._preferred_read_replica_expiration = None
573
+ # KIP-429 (Java parity): set while an on_partitions_revoked /
574
+ # on_partitions_lost listener is running for this partition.
575
+ # Gates fetches so records aren't pulled for a partition the
576
+ # user is in the middle of releasing. The TopicPartitionState
577
+ # is dropped from the assignment when the listener returns,
578
+ # so the flag only matters for the listener-call window.
579
+ self._pending_revocation = False
580
+
581
+ def _set_position(self, offset):
582
+ if not self.has_valid_position:
583
+ raise Errors.IllegalStateError('Valid position required')
584
+ if not isinstance(offset, OffsetAndMetadata):
585
+ raise TypeError('offset must be OffsetAndMetadata')
586
+ self._position = offset
587
+
588
+ def _get_position(self):
589
+ return self._position
590
+
591
+ position = property(_get_position, _set_position, None, "last position")
592
+
593
+ def reset(self, strategy):
594
+ if strategy is None:
595
+ raise ValueError('strategy cannot be None')
596
+ self.reset_strategy = strategy
597
+ self._position = None
598
+ self.next_allowed_retry_time = None
599
+ self._awaiting_validation = False
600
+ self.clear_preferred_read_replica()
601
+
602
+ def is_reset_allowed(self):
603
+ return self.next_allowed_retry_time is None or self.next_allowed_retry_time < time.monotonic()
604
+
605
+ @property
606
+ def awaiting_reset(self):
607
+ return self.reset_strategy is not None
608
+
609
+ def set_reset_pending(self, next_allowed_retry_time):
610
+ self.next_allowed_retry_time = next_allowed_retry_time
611
+
612
+ def reset_failed(self, next_allowed_retry_time):
613
+ self.next_allowed_retry_time = next_allowed_retry_time
614
+
615
+ @property
616
+ def has_valid_position(self):
617
+ return self._position is not None
618
+
619
+ def is_missing_position(self):
620
+ return not self.has_valid_position and not self.awaiting_reset
621
+
622
+ def seek(self, offset):
623
+ self._position = offset if isinstance(offset, OffsetAndMetadata) else OffsetAndMetadata(offset, '', -1)
624
+ self.reset_strategy = None
625
+ self.drop_pending_record_batch = True
626
+ self.next_allowed_retry_time = None
627
+ self._awaiting_validation = False
628
+ self.clear_preferred_read_replica()
629
+
630
+ def pause(self):
631
+ self.paused = True
632
+
633
+ def resume(self):
634
+ self.paused = False
635
+
636
+ def mark_pending_revocation(self):
637
+ """KIP-429: gate fetches while an on_partitions_revoked /
638
+ on_partitions_lost listener is in progress for this partition.
639
+ Single-shot: the surrounding ``assign_from_subscribed`` drops
640
+ the state object once the listener returns."""
641
+ self._pending_revocation = True
642
+
643
+ def is_fetchable(self):
644
+ return (not self.paused
645
+ and not self._pending_revocation
646
+ and self.has_valid_position
647
+ and not self._awaiting_validation)
648
+
649
+ def preferred_read_replica(self):
650
+ """Return the currently-cached preferred read replica (KIP-392),
651
+ or None if unset/expired. Lazily clears the cache on expiry."""
652
+ if self._preferred_read_replica is None:
653
+ return None
654
+ if (self._preferred_read_replica_expiration is not None
655
+ and time.monotonic() >= self._preferred_read_replica_expiration):
656
+ self.clear_preferred_read_replica()
657
+ return None
658
+ return self._preferred_read_replica
659
+
660
+ def update_preferred_read_replica(self, node_id, expiration_time):
661
+ """Cache the broker's chosen preferred read replica until ``expiration_time``
662
+ (monotonic). ``node_id == -1`` (or None) clears the cache.
663
+
664
+ Returns True if the cached replica actually changed (caller can log).
665
+ """
666
+ if node_id is None or node_id < 0:
667
+ changed = self._preferred_read_replica is not None
668
+ self.clear_preferred_read_replica()
669
+ return changed
670
+ if node_id == self._preferred_read_replica:
671
+ return False
672
+ self._preferred_read_replica = node_id
673
+ self._preferred_read_replica_expiration = expiration_time
674
+ return True
675
+
676
+ def clear_preferred_read_replica(self):
677
+ """Clear the cached preferred read replica. Returns the previously-
678
+ cached node_id (or None) so the caller can log the eviction."""
679
+ previous = self._preferred_read_replica
680
+ self._preferred_read_replica = None
681
+ self._preferred_read_replica_expiration = None
682
+ return previous
683
+
684
+ @property
685
+ def awaiting_validation(self):
686
+ return self._awaiting_validation
687
+
688
+ def maybe_validate_position(self, current_leader_epoch):
689
+ """Mark for validation if current leader has advanced beyond our position's epoch.
690
+
691
+ Returns True if the partition is now awaiting validation.
692
+ """
693
+ if self.awaiting_reset:
694
+ return False
695
+ if self._position is None:
696
+ return False
697
+ if current_leader_epoch is None or current_leader_epoch < 0:
698
+ return False
699
+ # Positions without a known epoch (legacy data, post-seek to bare offset)
700
+ # can't be validated; treat as already-fetchable.
701
+ if self._position.leader_epoch < 0:
702
+ return False
703
+ if self._position.leader_epoch >= current_leader_epoch:
704
+ return False
705
+ self.clear_preferred_read_replica()
706
+ self._awaiting_validation = True
707
+ self.next_allowed_retry_time = None
708
+ return True
709
+
710
+ def request_position_validation(self):
711
+ """Force validation (e.g., after FENCED/UNKNOWN epoch errors from the broker)."""
712
+ if self._position is None or self._position.leader_epoch < 0:
713
+ return False
714
+ self._awaiting_validation = True
715
+ self.next_allowed_retry_time = None
716
+ return True
717
+
718
+ def is_validation_allowed(self):
719
+ return self.next_allowed_retry_time is None or self.next_allowed_retry_time < time.monotonic()
720
+
721
+ def set_validation_pending(self, next_allowed_retry_time):
722
+ self.next_allowed_retry_time = next_allowed_retry_time
723
+
724
+ def validation_failed(self, next_allowed_retry_time):
725
+ self.next_allowed_retry_time = next_allowed_retry_time
726
+
727
+ def complete_validation(self, validated_position=None):
728
+ self._awaiting_validation = False
729
+ self.next_allowed_retry_time = None
730
+ if validated_position is not None:
731
+ self._position = validated_position
732
+
733
+
734
+ class ConsumerRebalanceListener(ABC):
735
+ """
736
+ A callback interface that the user can implement to trigger custom actions
737
+ when the set of partitions assigned to the consumer changes.
738
+
739
+ This is applicable when the consumer is having Kafka auto-manage group
740
+ membership. If the consumer's directly assign partitions, those
741
+ partitions will never be reassigned and this callback is not applicable.
742
+
743
+ When Kafka is managing the group membership, a partition re-assignment will
744
+ be triggered any time the members of the group changes or the subscription
745
+ of the members changes. This can occur when processes die, new process
746
+ instances are added or old instances come back to life after failure.
747
+ Rebalances can also be triggered by changes affecting the subscribed
748
+ topics (e.g. when then number of partitions is administratively adjusted).
749
+
750
+ There are many uses for this functionality. One common use is saving offsets
751
+ in a custom store. By saving offsets in the on_partitions_revoked(), call we
752
+ can ensure that any time partition assignment changes the offset gets saved.
753
+
754
+ Another use is flushing out any kind of cache of intermediate results the
755
+ consumer may be keeping. For example, consider a case where the consumer is
756
+ subscribed to a topic containing user page views, and the goal is to count
757
+ the number of page views per users for each five minute window. Let's say
758
+ the topic is partitioned by the user id so that all events for a particular
759
+ user will go to a single consumer instance. The consumer can keep in memory
760
+ a running tally of actions per user and only flush these out to a remote
761
+ data store when its cache gets too big. However if a partition is reassigned
762
+ it may want to automatically trigger a flush of this cache, before the new
763
+ owner takes over consumption.
764
+
765
+ Threading: callbacks run on the consumer's IO event loop, the same loop
766
+ that drives heartbeats. Sync listener methods must return promptly --
767
+ blocking IO inside a sync listener will block heartbeats for the duration
768
+ and can cause the consumer to be kicked from the group if the listener
769
+ runs longer than ``session_timeout_ms``. For listeners that need to do
770
+ blocking work (e.g. flushing state to a database), prefer
771
+ :class:`AsyncConsumerRebalanceListener`, which lets you ``await`` while
772
+ keeping the loop responsive, or wrap the blocking call in your own
773
+ worker thread.
774
+
775
+ It is guaranteed that all consumer processes will invoke
776
+ on_partitions_revoked() prior to any process invoking
777
+ on_partitions_assigned(). So if offsets or other state is saved in the
778
+ on_partitions_revoked() call, it should be saved by the time the process
779
+ taking over that partition has their on_partitions_assigned() callback
780
+ called to load the state.
781
+ """
782
+ @abstractmethod
783
+ def on_partitions_revoked(self, revoked):
784
+ """
785
+ A callback method the user can implement to provide handling of offset
786
+ commits to a customized store on the start of a rebalance operation.
787
+ This method will be called before a rebalance operation starts and
788
+ after the consumer stops fetching data. It is recommended that offsets
789
+ should be committed in this callback to either Kafka or a custom offset
790
+ store to prevent duplicate data.
791
+
792
+ NOTE: This method is called before each rebalance and also when the
793
+ consumer is closing (KafkaConsumer.close()), so that offsets / state
794
+ can be committed before the partitions are given up. If the group
795
+ membership has already been lost (forced eviction),
796
+ on_partitions_lost() is called instead.
797
+
798
+ Arguments:
799
+ revoked (list of TopicPartition): the partitions that were assigned
800
+ to the consumer on the last rebalance
801
+ """
802
+ pass
803
+
804
+ @abstractmethod
805
+ def on_partitions_assigned(self, assigned):
806
+ """
807
+ A callback method the user can implement to provide handling of
808
+ customized offsets on completion of a successful partition
809
+ re-assignment. This method will be called after an offset re-assignment
810
+ completes and before the consumer starts fetching data.
811
+
812
+ It is guaranteed that all the processes in a consumer group will execute
813
+ their on_partitions_revoked() callback before any instance executes its
814
+ on_partitions_assigned() callback.
815
+
816
+ Arguments:
817
+ assigned (list of TopicPartition): the partitions assigned to the
818
+ consumer (may include partitions that were previously assigned)
819
+ """
820
+ pass
821
+
822
+ def on_partitions_lost(self, lost):
823
+ """KIP-429: called when the consumer has been forcibly removed
824
+ from the group (heartbeat session expiry, ``UnknownMemberIdError``,
825
+ ``IllegalGenerationError``, ``FencedInstanceIdError``) and the
826
+ partitions cannot be cleanly committed. ``on_partitions_revoked``
827
+ implies the user *can* still commit; ``on_partitions_lost`` makes
828
+ explicit that the member has been booted and any in-flight state
829
+ for these partitions should be discarded.
830
+
831
+ Default behaviour is to delegate to ``on_partitions_revoked`` so
832
+ listeners written before KIP-429 keep working unchanged. Override
833
+ for cleanup that is specific to the forced-eviction case (e.g.
834
+ skipping a commit attempt that will fail anyway).
835
+
836
+ Arguments:
837
+ lost (set of TopicPartition): the partitions that were
838
+ assigned but have been lost due to forced eviction.
839
+ """
840
+ return self.on_partitions_revoked(lost)
841
+
842
+
843
+ class AsyncConsumerRebalanceListener(ABC):
844
+ """
845
+ Async variant of :class:`ConsumerRebalanceListener`.
846
+
847
+ Implement this when your rebalance hooks need to perform IO that would
848
+ otherwise block the consumer's event loop -- e.g. flushing state to a
849
+ database, calling an external service, or coordinating with other async
850
+ code. The coordinator detects coroutine functions and ``await`` s them
851
+ instead of calling inline, so other tasks on the loop (notably the
852
+ heartbeat coroutine) continue to run while your listener is suspended.
853
+
854
+ Same lifecycle and ordering guarantees as the sync listener: all
855
+ consumers in the group invoke ``on_partitions_revoked`` before any
856
+ invokes ``on_partitions_assigned``. Both methods must be defined as
857
+ ``async def``; otherwise use :class:`ConsumerRebalanceListener`.
858
+ """
859
+ @abstractmethod
860
+ async def on_partitions_revoked(self, revoked):
861
+ """Async-callback for the start of a rebalance operation.
862
+
863
+ See :meth:`ConsumerRebalanceListener.on_partitions_revoked` for
864
+ semantics. The coordinator awaits this method, so non-blocking IO
865
+ via ``await`` keeps the heartbeat loop responsive during the call.
866
+
867
+ Arguments:
868
+ revoked (set of TopicPartition): the partitions that were
869
+ assigned to the consumer on the last rebalance.
870
+ """
871
+ pass
872
+
873
+ @abstractmethod
874
+ async def on_partitions_assigned(self, assigned):
875
+ """Async-callback for the completion of a partition re-assignment.
876
+
877
+ See :meth:`ConsumerRebalanceListener.on_partitions_assigned` for
878
+ semantics.
879
+
880
+ Arguments:
881
+ assigned (set of TopicPartition): the partitions assigned to
882
+ the consumer (may include partitions that were previously
883
+ assigned).
884
+ """
885
+ pass
886
+
887
+ async def on_partitions_lost(self, lost):
888
+ """Async variant of
889
+ :meth:`ConsumerRebalanceListener.on_partitions_lost`. Default
890
+ implementation awaits ``on_partitions_revoked`` for backward
891
+ compatibility with listeners written before KIP-429.
892
+
893
+ Arguments:
894
+ lost (set of TopicPartition): the partitions that were
895
+ assigned but have been lost due to forced eviction.
896
+ """
897
+ await self.on_partitions_revoked(lost)