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.
- kafka/__init__.py +34 -0
- kafka/__main__.py +5 -0
- kafka/admin/__init__.py +29 -0
- kafka/admin/__main__.py +5 -0
- kafka/admin/_acls.py +355 -0
- kafka/admin/_cluster.py +359 -0
- kafka/admin/_configs.py +479 -0
- kafka/admin/_groups.py +754 -0
- kafka/admin/_partitions.py +595 -0
- kafka/admin/_topics.py +281 -0
- kafka/admin/_transactions.py +450 -0
- kafka/admin/_users.py +194 -0
- kafka/admin/client.py +373 -0
- kafka/benchmarks/__init__.py +0 -0
- kafka/benchmarks/consumer_performance.py +138 -0
- kafka/benchmarks/load_example.py +109 -0
- kafka/benchmarks/producer_encode_path.py +201 -0
- kafka/benchmarks/producer_performance.py +161 -0
- kafka/benchmarks/profile_protocol.py +138 -0
- kafka/benchmarks/protocol_old_vs_new.py +447 -0
- kafka/benchmarks/record_batch_compose.py +77 -0
- kafka/benchmarks/record_batch_read.py +82 -0
- kafka/benchmarks/varint_speed.py +426 -0
- kafka/cli/__init__.py +36 -0
- kafka/cli/admin/__init__.py +117 -0
- kafka/cli/admin/acls/__init__.py +9 -0
- kafka/cli/admin/acls/common.py +76 -0
- kafka/cli/admin/acls/create.py +19 -0
- kafka/cli/admin/acls/delete.py +23 -0
- kafka/cli/admin/acls/describe.py +16 -0
- kafka/cli/admin/cluster/__init__.py +14 -0
- kafka/cli/admin/cluster/describe.py +11 -0
- kafka/cli/admin/cluster/describe_quorum.py +11 -0
- kafka/cli/admin/cluster/features.py +52 -0
- kafka/cli/admin/cluster/log_dirs.py +43 -0
- kafka/cli/admin/cluster/versions.py +33 -0
- kafka/cli/admin/configs/__init__.py +10 -0
- kafka/cli/admin/configs/alter.py +43 -0
- kafka/cli/admin/configs/common.py +17 -0
- kafka/cli/admin/configs/describe.py +30 -0
- kafka/cli/admin/configs/list.py +16 -0
- kafka/cli/admin/configs/reset.py +20 -0
- kafka/cli/admin/groups/__init__.py +16 -0
- kafka/cli/admin/groups/alter_offsets.py +30 -0
- kafka/cli/admin/groups/delete.py +11 -0
- kafka/cli/admin/groups/delete_offsets.py +29 -0
- kafka/cli/admin/groups/describe.py +11 -0
- kafka/cli/admin/groups/list.py +28 -0
- kafka/cli/admin/groups/list_offsets.py +29 -0
- kafka/cli/admin/groups/remove_members.py +40 -0
- kafka/cli/admin/groups/reset_offsets.py +139 -0
- kafka/cli/admin/partitions/__init__.py +21 -0
- kafka/cli/admin/partitions/alter_reassignments.py +37 -0
- kafka/cli/admin/partitions/create.py +27 -0
- kafka/cli/admin/partitions/delete_records.py +31 -0
- kafka/cli/admin/partitions/describe.py +36 -0
- kafka/cli/admin/partitions/elect_leaders.py +53 -0
- kafka/cli/admin/partitions/list_offsets.py +88 -0
- kafka/cli/admin/partitions/list_reassignments.py +35 -0
- kafka/cli/admin/topics/__init__.py +10 -0
- kafka/cli/admin/topics/create.py +13 -0
- kafka/cli/admin/topics/delete.py +19 -0
- kafka/cli/admin/topics/describe.py +18 -0
- kafka/cli/admin/topics/list.py +11 -0
- kafka/cli/admin/transactions/__init__.py +17 -0
- kafka/cli/admin/transactions/abort.py +38 -0
- kafka/cli/admin/transactions/describe.py +24 -0
- kafka/cli/admin/transactions/describe_producers.py +29 -0
- kafka/cli/admin/transactions/find_hanging.py +26 -0
- kafka/cli/admin/transactions/list.py +37 -0
- kafka/cli/admin/users/__init__.py +8 -0
- kafka/cli/admin/users/alter_user_scram_credentials.py +34 -0
- kafka/cli/admin/users/describe_user_scram_credentials.py +15 -0
- kafka/cli/common.py +95 -0
- kafka/cli/consumer/__init__.py +63 -0
- kafka/cli/producer/__init__.py +57 -0
- kafka/cluster.py +824 -0
- kafka/codec.py +325 -0
- kafka/consumer/__init__.py +5 -0
- kafka/consumer/__main__.py +5 -0
- kafka/consumer/fetcher.py +2012 -0
- kafka/consumer/group.py +1347 -0
- kafka/consumer/subscription_state.py +897 -0
- kafka/coordinator/__init__.py +0 -0
- kafka/coordinator/assignors/__init__.py +0 -0
- kafka/coordinator/assignors/abstract.py +90 -0
- kafka/coordinator/assignors/cooperative_sticky.py +167 -0
- kafka/coordinator/assignors/range.py +81 -0
- kafka/coordinator/assignors/roundrobin.py +101 -0
- kafka/coordinator/assignors/sticky/StickyAssignorUserData.json +37 -0
- kafka/coordinator/assignors/sticky/__init__.py +0 -0
- kafka/coordinator/assignors/sticky/partition_movements.py +149 -0
- kafka/coordinator/assignors/sticky/sorted_set.py +63 -0
- kafka/coordinator/assignors/sticky/sticky_assignor.py +665 -0
- kafka/coordinator/assignors/sticky/user_data.py +8 -0
- kafka/coordinator/base.py +1215 -0
- kafka/coordinator/consumer.py +1224 -0
- kafka/coordinator/heartbeat.py +82 -0
- kafka/coordinator/subscription.py +34 -0
- kafka/errors.py +1004 -0
- kafka/future.py +166 -0
- kafka/metrics/__init__.py +13 -0
- kafka/metrics/compound_stat.py +33 -0
- kafka/metrics/dict_reporter.py +81 -0
- kafka/metrics/kafka_metric.py +36 -0
- kafka/metrics/measurable.py +27 -0
- kafka/metrics/measurable_stat.py +13 -0
- kafka/metrics/metric_config.py +33 -0
- kafka/metrics/metric_name.py +105 -0
- kafka/metrics/metrics.py +261 -0
- kafka/metrics/metrics_reporter.py +53 -0
- kafka/metrics/quota.py +41 -0
- kafka/metrics/stat.py +19 -0
- kafka/metrics/stats/__init__.py +15 -0
- kafka/metrics/stats/avg.py +24 -0
- kafka/metrics/stats/count.py +17 -0
- kafka/metrics/stats/histogram.py +99 -0
- kafka/metrics/stats/max_stat.py +17 -0
- kafka/metrics/stats/min_stat.py +19 -0
- kafka/metrics/stats/percentile.py +14 -0
- kafka/metrics/stats/percentiles.py +75 -0
- kafka/metrics/stats/rate.py +118 -0
- kafka/metrics/stats/sampled_stat.py +99 -0
- kafka/metrics/stats/sensor.py +136 -0
- kafka/metrics/stats/total.py +15 -0
- kafka/net/__init__.py +19 -0
- kafka/net/compat.py +165 -0
- kafka/net/connection.py +593 -0
- kafka/net/http_connect.py +144 -0
- kafka/net/inet.py +122 -0
- kafka/net/manager.py +451 -0
- kafka/net/metrics.py +149 -0
- kafka/net/sasl/__init__.py +32 -0
- kafka/net/sasl/abc.py +28 -0
- kafka/net/sasl/gssapi.py +95 -0
- kafka/net/sasl/msk.py +245 -0
- kafka/net/sasl/oauth.py +98 -0
- kafka/net/sasl/plain.py +42 -0
- kafka/net/sasl/scram.py +135 -0
- kafka/net/sasl/sspi.py +111 -0
- kafka/net/selector.py +644 -0
- kafka/net/socks5.py +262 -0
- kafka/net/transport.py +415 -0
- kafka/net/wakeup_notifier.py +72 -0
- kafka/partitioner/__init__.py +8 -0
- kafka/partitioner/abc.py +8 -0
- kafka/partitioner/default.py +89 -0
- kafka/partitioner/sticky.py +109 -0
- kafka/producer/__init__.py +5 -0
- kafka/producer/__main__.py +5 -0
- kafka/producer/future.py +101 -0
- kafka/producer/kafka.py +1123 -0
- kafka/producer/producer_batch.py +192 -0
- kafka/producer/record_accumulator.py +647 -0
- kafka/producer/sender.py +884 -0
- kafka/producer/transaction_manager.py +1326 -0
- kafka/protocol/__init__.py +0 -0
- kafka/protocol/admin/__init__.py +29 -0
- kafka/protocol/admin/acl.py +83 -0
- kafka/protocol/admin/acl.pyi +375 -0
- kafka/protocol/admin/client_quotas.py +14 -0
- kafka/protocol/admin/client_quotas.pyi +265 -0
- kafka/protocol/admin/cluster.py +31 -0
- kafka/protocol/admin/cluster.pyi +620 -0
- kafka/protocol/admin/configs.py +22 -0
- kafka/protocol/admin/configs.pyi +437 -0
- kafka/protocol/admin/groups.py +24 -0
- kafka/protocol/admin/groups.pyi +261 -0
- kafka/protocol/admin/topics.py +53 -0
- kafka/protocol/admin/topics.pyi +982 -0
- kafka/protocol/admin/transactions.py +18 -0
- kafka/protocol/admin/transactions.pyi +311 -0
- kafka/protocol/admin/users.py +14 -0
- kafka/protocol/admin/users.pyi +223 -0
- kafka/protocol/api_data.py +125 -0
- kafka/protocol/api_header.py +55 -0
- kafka/protocol/api_key.py +97 -0
- kafka/protocol/api_message.py +277 -0
- kafka/protocol/broker_version_data.py +246 -0
- kafka/protocol/consumer/__init__.py +13 -0
- kafka/protocol/consumer/fetch.py +16 -0
- kafka/protocol/consumer/fetch.pyi +298 -0
- kafka/protocol/consumer/group.py +38 -0
- kafka/protocol/consumer/group.pyi +824 -0
- kafka/protocol/consumer/metadata.py +30 -0
- kafka/protocol/consumer/metadata.pyi +89 -0
- kafka/protocol/consumer/offsets.py +75 -0
- kafka/protocol/consumer/offsets.pyi +288 -0
- kafka/protocol/data_container.py +166 -0
- kafka/protocol/frame.py +30 -0
- kafka/protocol/generate_stubs.py +468 -0
- kafka/protocol/metadata/__init__.py +10 -0
- kafka/protocol/metadata/api_versions.py +41 -0
- kafka/protocol/metadata/api_versions.pyi +128 -0
- kafka/protocol/metadata/find_coordinator.py +19 -0
- kafka/protocol/metadata/find_coordinator.pyi +105 -0
- kafka/protocol/metadata/metadata.py +34 -0
- kafka/protocol/metadata/metadata.pyi +160 -0
- kafka/protocol/old/__init__.py +0 -0
- kafka/protocol/old/abstract.py +17 -0
- kafka/protocol/old/add_offsets_to_txn.py +54 -0
- kafka/protocol/old/add_partitions_to_txn.py +71 -0
- kafka/protocol/old/admin.py +1086 -0
- kafka/protocol/old/api.py +205 -0
- kafka/protocol/old/api_versions.py +133 -0
- kafka/protocol/old/commit.py +355 -0
- kafka/protocol/old/consumer_protocol.py +36 -0
- kafka/protocol/old/end_txn.py +53 -0
- kafka/protocol/old/fetch.py +408 -0
- kafka/protocol/old/find_coordinator.py +72 -0
- kafka/protocol/old/group.py +451 -0
- kafka/protocol/old/init_producer_id.py +42 -0
- kafka/protocol/old/list_offsets.py +186 -0
- kafka/protocol/old/metadata.py +290 -0
- kafka/protocol/old/offset_for_leader_epoch.py +133 -0
- kafka/protocol/old/produce.py +247 -0
- kafka/protocol/old/sasl_authenticate.py +38 -0
- kafka/protocol/old/sasl_handshake.py +39 -0
- kafka/protocol/old/struct.py +87 -0
- kafka/protocol/old/txn_offset_commit.py +73 -0
- kafka/protocol/old/types.py +440 -0
- kafka/protocol/parser.py +191 -0
- kafka/protocol/producer/__init__.py +7 -0
- kafka/protocol/producer/produce.py +17 -0
- kafka/protocol/producer/produce.pyi +197 -0
- kafka/protocol/producer/transaction.py +30 -0
- kafka/protocol/producer/transaction.pyi +663 -0
- kafka/protocol/sasl.py +52 -0
- kafka/protocol/sasl.pyi +126 -0
- kafka/protocol/schemas/__init__.py +7 -0
- kafka/protocol/schemas/fields/__init__.py +7 -0
- kafka/protocol/schemas/fields/array.py +127 -0
- kafka/protocol/schemas/fields/base.py +156 -0
- kafka/protocol/schemas/fields/codecs/__init__.py +12 -0
- kafka/protocol/schemas/fields/codecs/encode_buffer.py +82 -0
- kafka/protocol/schemas/fields/codecs/tagged_fields.py +109 -0
- kafka/protocol/schemas/fields/codecs/types.py +505 -0
- kafka/protocol/schemas/fields/codegen.py +40 -0
- kafka/protocol/schemas/fields/simple.py +127 -0
- kafka/protocol/schemas/fields/struct.py +357 -0
- kafka/protocol/schemas/fields/struct_array.py +142 -0
- kafka/protocol/schemas/load_json.py +42 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnRequest.json +40 -0
- kafka/protocol/schemas/resources/AddOffsetsToTxnResponse.json +35 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnRequest.json +65 -0
- kafka/protocol/schemas/resources/AddPartitionsToTxnResponse.json +60 -0
- kafka/protocol/schemas/resources/AlterClientQuotasRequest.json +47 -0
- kafka/protocol/schemas/resources/AlterClientQuotasResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterConfigsRequest.json +43 -0
- kafka/protocol/schemas/resources/AlterConfigsResponse.json +39 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsRequest.json +42 -0
- kafka/protocol/schemas/resources/AlterPartitionReassignmentsResponse.json +47 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsRequest.json +41 -0
- kafka/protocol/schemas/resources/AlterReplicaLogDirsResponse.json +41 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsRequest.json +45 -0
- kafka/protocol/schemas/resources/AlterUserScramCredentialsResponse.json +35 -0
- kafka/protocol/schemas/resources/ApiVersionsRequest.json +34 -0
- kafka/protocol/schemas/resources/ApiVersionsResponse.json +79 -0
- kafka/protocol/schemas/resources/ConsumerProtocolAssignment.json +42 -0
- kafka/protocol/schemas/resources/ConsumerProtocolSubscription.json +49 -0
- kafka/protocol/schemas/resources/CreateAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/CreateAclsResponse.json +37 -0
- kafka/protocol/schemas/resources/CreatePartitionsRequest.json +47 -0
- kafka/protocol/schemas/resources/CreatePartitionsResponse.json +41 -0
- kafka/protocol/schemas/resources/CreateTopicsRequest.json +65 -0
- kafka/protocol/schemas/resources/CreateTopicsResponse.json +72 -0
- kafka/protocol/schemas/resources/DeleteAclsRequest.json +46 -0
- kafka/protocol/schemas/resources/DeleteAclsResponse.json +59 -0
- kafka/protocol/schemas/resources/DeleteGroupsRequest.json +30 -0
- kafka/protocol/schemas/resources/DeleteGroupsResponse.json +36 -0
- kafka/protocol/schemas/resources/DeleteRecordsRequest.json +42 -0
- kafka/protocol/schemas/resources/DeleteRecordsResponse.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsRequest.json +43 -0
- kafka/protocol/schemas/resources/DeleteTopicsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeAclsRequest.json +43 -0
- kafka/protocol/schemas/resources/DescribeAclsResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasRequest.json +37 -0
- kafka/protocol/schemas/resources/DescribeClientQuotasResponse.json +47 -0
- kafka/protocol/schemas/resources/DescribeClusterRequest.json +35 -0
- kafka/protocol/schemas/resources/DescribeClusterResponse.json +56 -0
- kafka/protocol/schemas/resources/DescribeConfigsRequest.json +42 -0
- kafka/protocol/schemas/resources/DescribeConfigsResponse.json +69 -0
- kafka/protocol/schemas/resources/DescribeGroupsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeGroupsResponse.json +74 -0
- kafka/protocol/schemas/resources/DescribeLogDirsRequest.json +38 -0
- kafka/protocol/schemas/resources/DescribeLogDirsResponse.json +65 -0
- kafka/protocol/schemas/resources/DescribeProducersRequest.json +32 -0
- kafka/protocol/schemas/resources/DescribeProducersResponse.json +55 -0
- kafka/protocol/schemas/resources/DescribeQuorumRequest.json +39 -0
- kafka/protocol/schemas/resources/DescribeQuorumResponse.json +82 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsRequest.json +40 -0
- kafka/protocol/schemas/resources/DescribeTopicPartitionsResponse.json +66 -0
- kafka/protocol/schemas/resources/DescribeTransactionsRequest.json +27 -0
- kafka/protocol/schemas/resources/DescribeTransactionsResponse.json +52 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsRequest.json +30 -0
- kafka/protocol/schemas/resources/DescribeUserScramCredentialsResponse.json +45 -0
- kafka/protocol/schemas/resources/ElectLeadersRequest.json +41 -0
- kafka/protocol/schemas/resources/ElectLeadersResponse.json +45 -0
- kafka/protocol/schemas/resources/EndTxnRequest.json +43 -0
- kafka/protocol/schemas/resources/EndTxnResponse.json +41 -0
- kafka/protocol/schemas/resources/FetchRequest.json +125 -0
- kafka/protocol/schemas/resources/FetchResponse.json +124 -0
- kafka/protocol/schemas/resources/FindCoordinatorRequest.json +43 -0
- kafka/protocol/schemas/resources/FindCoordinatorResponse.json +58 -0
- kafka/protocol/schemas/resources/HeartbeatRequest.json +39 -0
- kafka/protocol/schemas/resources/HeartbeatResponse.json +35 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsRequest.json +44 -0
- kafka/protocol/schemas/resources/IncrementalAlterConfigsResponse.json +38 -0
- kafka/protocol/schemas/resources/InitProducerIdRequest.json +50 -0
- kafka/protocol/schemas/resources/InitProducerIdResponse.json +47 -0
- kafka/protocol/schemas/resources/JoinGroupRequest.json +63 -0
- kafka/protocol/schemas/resources/JoinGroupResponse.json +69 -0
- kafka/protocol/schemas/resources/LeaveGroupRequest.json +47 -0
- kafka/protocol/schemas/resources/LeaveGroupResponse.json +47 -0
- kafka/protocol/schemas/resources/ListConfigResourcesRequest.json +31 -0
- kafka/protocol/schemas/resources/ListConfigResourcesResponse.json +37 -0
- kafka/protocol/schemas/resources/ListGroupsRequest.json +36 -0
- kafka/protocol/schemas/resources/ListGroupsResponse.json +49 -0
- kafka/protocol/schemas/resources/ListOffsetsRequest.json +72 -0
- kafka/protocol/schemas/resources/ListOffsetsResponse.json +71 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsRequest.json +34 -0
- kafka/protocol/schemas/resources/ListPartitionReassignmentsResponse.json +46 -0
- kafka/protocol/schemas/resources/ListTransactionsRequest.json +40 -0
- kafka/protocol/schemas/resources/ListTransactionsResponse.json +42 -0
- kafka/protocol/schemas/resources/MetadataRequest.json +56 -0
- kafka/protocol/schemas/resources/MetadataResponse.json +101 -0
- kafka/protocol/schemas/resources/OffsetCommitRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetCommitResponse.json +71 -0
- kafka/protocol/schemas/resources/OffsetDeleteRequest.json +39 -0
- kafka/protocol/schemas/resources/OffsetDeleteResponse.json +42 -0
- kafka/protocol/schemas/resources/OffsetFetchRequest.json +76 -0
- kafka/protocol/schemas/resources/OffsetFetchResponse.json +107 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochRequest.json +52 -0
- kafka/protocol/schemas/resources/OffsetForLeaderEpochResponse.json +51 -0
- kafka/protocol/schemas/resources/ProduceRequest.json +73 -0
- kafka/protocol/schemas/resources/ProduceResponse.json +96 -0
- kafka/protocol/schemas/resources/RequestHeader.json +44 -0
- kafka/protocol/schemas/resources/ResponseHeader.json +26 -0
- kafka/protocol/schemas/resources/SaslAuthenticateRequest.json +29 -0
- kafka/protocol/schemas/resources/SaslAuthenticateResponse.json +34 -0
- kafka/protocol/schemas/resources/SaslHandshakeRequest.json +31 -0
- kafka/protocol/schemas/resources/SaslHandshakeResponse.json +32 -0
- kafka/protocol/schemas/resources/SyncGroupRequest.json +56 -0
- kafka/protocol/schemas/resources/SyncGroupResponse.json +46 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitRequest.json +68 -0
- kafka/protocol/schemas/resources/TxnOffsetCommitResponse.json +47 -0
- kafka/protocol/schemas/resources/UpdateFeaturesRequest.json +43 -0
- kafka/protocol/schemas/resources/UpdateFeaturesResponse.json +39 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersRequest.json +49 -0
- kafka/protocol/schemas/resources/WriteTxnMarkersResponse.json +45 -0
- kafka/protocol/schemas/resources/__init__.py +0 -0
- kafka/record/__init__.py +3 -0
- kafka/record/_crc32c.py +161 -0
- kafka/record/abc.py +144 -0
- kafka/record/default_records.py +782 -0
- kafka/record/legacy_records.py +587 -0
- kafka/record/memory_records.py +255 -0
- kafka/record/util.py +135 -0
- kafka/serializer/__init__.py +4 -0
- kafka/serializer/abstract.py +20 -0
- kafka/serializer/default.py +16 -0
- kafka/serializer/json.py +17 -0
- kafka/serializer/wrapper.py +21 -0
- kafka/structs.py +69 -0
- kafka/util.py +159 -0
- kafka/vendor/__init__.py +0 -0
- kafka/version.py +1 -0
- kafka_python-3.0.0.dist-info/METADATA +319 -0
- kafka_python-3.0.0.dist-info/RECORD +373 -0
- kafka_python-3.0.0.dist-info/WHEEL +5 -0
- kafka_python-3.0.0.dist-info/entry_points.txt +2 -0
- kafka_python-3.0.0.dist-info/licenses/LICENSE +202 -0
- kafka_python-3.0.0.dist-info/top_level.txt +1 -0
kafka/admin/_groups.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
"""Group management mixin for KafkaAdminClient."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import itertools
|
|
7
|
+
import logging
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
import struct
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import kafka.errors as Errors
|
|
13
|
+
from kafka.admin._acls import valid_acl_operations
|
|
14
|
+
from kafka.protocol.admin import DeleteGroupsRequest, DescribeGroupsRequest, ListGroupsRequest
|
|
15
|
+
from kafka.protocol.consumer import (
|
|
16
|
+
LeaveGroupRequest, OffsetCommitRequest, OffsetDeleteRequest, OffsetFetchRequest,
|
|
17
|
+
OffsetSpec, OffsetTimestamp,
|
|
18
|
+
)
|
|
19
|
+
from kafka.protocol.consumer.group import DEFAULT_GENERATION_ID, UNKNOWN_MEMBER_ID
|
|
20
|
+
from kafka.protocol.consumer.metadata import (
|
|
21
|
+
ConsumerProtocolAssignment, ConsumerProtocolSubscription, ConsumerProtocolType,
|
|
22
|
+
)
|
|
23
|
+
from kafka.structs import OffsetAndMetadata, TopicPartition
|
|
24
|
+
from kafka.util import EnumHelper
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from kafka.net.manager import KafkaConnectionManager
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GroupAdminMixin:
|
|
33
|
+
"""Mixin providing consumer group management methods for KafkaAdminClient."""
|
|
34
|
+
_manager: KafkaConnectionManager
|
|
35
|
+
config: dict
|
|
36
|
+
|
|
37
|
+
# -- Describe groups ----------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _describe_groups_request(self, group_ids):
|
|
40
|
+
request = DescribeGroupsRequest(
|
|
41
|
+
groups=list(group_ids),
|
|
42
|
+
include_authorized_operations=True
|
|
43
|
+
)
|
|
44
|
+
return request
|
|
45
|
+
|
|
46
|
+
def _describe_groups_process_response(self, response):
|
|
47
|
+
"""Process a DescribeGroupsResponse into a group description."""
|
|
48
|
+
for group in response.groups:
|
|
49
|
+
for member in group.members:
|
|
50
|
+
if member.member_metadata:
|
|
51
|
+
try:
|
|
52
|
+
member.member_metadata = ConsumerProtocolSubscription.decode(member.member_metadata)
|
|
53
|
+
except struct.error:
|
|
54
|
+
log.warning(f'Unable to decode member_metadata for {group}/{member.member_id}')
|
|
55
|
+
pass
|
|
56
|
+
if member.member_assignment:
|
|
57
|
+
try:
|
|
58
|
+
member.member_assignment = ConsumerProtocolAssignment.decode(member.member_assignment)
|
|
59
|
+
except struct.error:
|
|
60
|
+
log.warning(f'Unable to decode member_assignment for {group}/{member.member_id}')
|
|
61
|
+
pass
|
|
62
|
+
# Return dict (key, val) tuples
|
|
63
|
+
results = {}
|
|
64
|
+
for group in response.groups:
|
|
65
|
+
group_id = group.group_id
|
|
66
|
+
result = self._process_acl_operations(group.to_dict())
|
|
67
|
+
error_code = result.pop('error_code')
|
|
68
|
+
error_message = result.pop('error_message', '') # v6+
|
|
69
|
+
result['error'] = str(Errors.for_code(error_code)(error_message)) if error_code else None
|
|
70
|
+
results[group_id] = result
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
async def _async_describe_groups(self, group_ids, group_coordinator_id=None):
|
|
74
|
+
# Bucket groups by coordinator. One DescribeGroups per coordinator.
|
|
75
|
+
coordinators_groups = defaultdict(list)
|
|
76
|
+
if group_coordinator_id is not None:
|
|
77
|
+
coordinators_groups[group_coordinator_id] = list(group_ids)
|
|
78
|
+
else:
|
|
79
|
+
coordinator_ids = await self._find_coordinator_ids(group_ids)
|
|
80
|
+
for group_id, coordinator_id in coordinator_ids.items():
|
|
81
|
+
coordinators_groups[coordinator_id].append(group_id)
|
|
82
|
+
|
|
83
|
+
results = {}
|
|
84
|
+
for coordinator_id, coordinator_group_ids in coordinators_groups.items():
|
|
85
|
+
request = self._describe_groups_request(coordinator_group_ids)
|
|
86
|
+
response = await self._manager.send(request, node_id=coordinator_id)
|
|
87
|
+
results.update(self._describe_groups_process_response(response))
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
def describe_groups(self, group_ids, group_coordinator_id=None, include_authorized_operations=False):
|
|
91
|
+
"""Describe a set of consumer groups.
|
|
92
|
+
|
|
93
|
+
Any errors are immediately raised.
|
|
94
|
+
|
|
95
|
+
Arguments:
|
|
96
|
+
group_ids: A list of consumer group IDs. These are typically the
|
|
97
|
+
group names as strings.
|
|
98
|
+
|
|
99
|
+
Keyword Arguments:
|
|
100
|
+
group_coordinator_id (int, optional): The node_id of the groups' coordinator
|
|
101
|
+
broker. If set to None, it will query the cluster for each group to
|
|
102
|
+
find that group's coordinator. Explicitly specifying this can be
|
|
103
|
+
useful for avoiding extra network round trips if you already know
|
|
104
|
+
the group coordinator. This is only useful when all the group_ids
|
|
105
|
+
have the same coordinator, otherwise it will error. Default: None.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
A dict of {group_id: {key: val}}. key/vals are simple to_dict translations
|
|
109
|
+
of the raw results from DescribeGroupsResponse (with inline decoding
|
|
110
|
+
of ConsumerSubscription and ConsumerAssignment metadata, and conversion
|
|
111
|
+
of acl set ints to semantic enums).
|
|
112
|
+
"""
|
|
113
|
+
return self._manager.run(self._async_describe_groups, group_ids, group_coordinator_id)
|
|
114
|
+
|
|
115
|
+
# -- List groups --------------------------------------------------
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _list_groups_request(states_filter=None, types_filter=None):
|
|
119
|
+
kwargs = {'min_version': 0}
|
|
120
|
+
if states_filter:
|
|
121
|
+
kwargs['states_filter'] = [GroupState.value_for(s) for s in states_filter]
|
|
122
|
+
kwargs['min_version'] = 4
|
|
123
|
+
if types_filter:
|
|
124
|
+
kwargs['types_filter'] = [GroupType.value_for(t) for t in types_filter]
|
|
125
|
+
kwargs['min_version'] = 5
|
|
126
|
+
return ListGroupsRequest(**kwargs)
|
|
127
|
+
|
|
128
|
+
def _list_groups_process_response(self, response):
|
|
129
|
+
"""Process a ListGroupsResponse into a list of groups."""
|
|
130
|
+
error_type = Errors.for_code(response.error_code)
|
|
131
|
+
if error_type is not Errors.NoError:
|
|
132
|
+
raise error_type(
|
|
133
|
+
"ListGroupsRequest failed with response '{}'."
|
|
134
|
+
.format(response))
|
|
135
|
+
return [group.to_dict() for group in response.groups]
|
|
136
|
+
|
|
137
|
+
async def _async_list_groups(self, broker_ids=None, states_filter=None, types_filter=None):
|
|
138
|
+
if broker_ids is None:
|
|
139
|
+
broker_ids = [broker.node_id for broker in self._manager.cluster.brokers()]
|
|
140
|
+
groups = []
|
|
141
|
+
for broker_id in broker_ids:
|
|
142
|
+
request = self._list_groups_request(states_filter=states_filter,
|
|
143
|
+
types_filter=types_filter)
|
|
144
|
+
response = await self._manager.send(request, node_id=broker_id)
|
|
145
|
+
groups.extend(self._list_groups_process_response(response))
|
|
146
|
+
return groups
|
|
147
|
+
|
|
148
|
+
def list_groups(self, broker_ids=None, states_filter=None, types_filter=None):
|
|
149
|
+
"""List all consumer groups known to the cluster.
|
|
150
|
+
|
|
151
|
+
This returns a list of Group dicts. The tuples are
|
|
152
|
+
composed of the consumer group name and the consumer group protocol
|
|
153
|
+
type.
|
|
154
|
+
|
|
155
|
+
Only consumer groups that store their offsets in Kafka are returned.
|
|
156
|
+
The protocol type will be an empty string for groups created using
|
|
157
|
+
Kafka < 0.9 APIs because, although they store their offsets in Kafka,
|
|
158
|
+
they don't use Kafka for group coordination. For groups created using
|
|
159
|
+
Kafka >= 0.9, the protocol type will typically be "consumer".
|
|
160
|
+
|
|
161
|
+
As soon as any error is encountered, it is immediately raised.
|
|
162
|
+
|
|
163
|
+
Keyword Arguments:
|
|
164
|
+
broker_ids ([int], optional): A list of broker node_ids to query for consumer
|
|
165
|
+
groups. If set to None, will query all brokers in the cluster.
|
|
166
|
+
Explicitly specifying broker(s) can be useful for determining which
|
|
167
|
+
consumer groups are coordinated by those broker(s). Default: None
|
|
168
|
+
states_filter (list, optional): Filter groups by state. Values
|
|
169
|
+
may be :class:`GroupState` members, their string names
|
|
170
|
+
(case-insensitive, hyphen or underscore), or raw protocol
|
|
171
|
+
strings (e.g. ``['Stable', 'Empty']``). Requires broker
|
|
172
|
+
>= 3.0 (KIP-518). Default: None (no filter).
|
|
173
|
+
types_filter (list, optional): Filter groups by type. Values
|
|
174
|
+
may be :class:`GroupType` members, their string names
|
|
175
|
+
(case-insensitive), or raw protocol strings (e.g.
|
|
176
|
+
``['consumer', 'classic', 'share']``). Requires broker
|
|
177
|
+
>= 4.0 (KIP-848). Default: None (no filter).
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of group data dicts, with key/vals from ListGroupsRequest
|
|
181
|
+
"""
|
|
182
|
+
return self._manager.run(self._async_list_groups, broker_ids,
|
|
183
|
+
states_filter, types_filter)
|
|
184
|
+
|
|
185
|
+
# -- List group offsets -------------------------------------------
|
|
186
|
+
|
|
187
|
+
def _list_group_offsets_requests(self, group_specs):
|
|
188
|
+
_Topic = OffsetFetchRequest.OffsetFetchRequestTopic
|
|
189
|
+
_Group = OffsetFetchRequest.OffsetFetchRequestGroup
|
|
190
|
+
_GroupTopic = _Group.OffsetFetchRequestTopics
|
|
191
|
+
max_version = 8
|
|
192
|
+
|
|
193
|
+
groups = []
|
|
194
|
+
for group_id, partitions in group_specs.items():
|
|
195
|
+
if partitions is None:
|
|
196
|
+
group_topics = None
|
|
197
|
+
else:
|
|
198
|
+
topics_partitions = defaultdict(set)
|
|
199
|
+
for topic, partition in partitions:
|
|
200
|
+
topics_partitions[topic].add(partition)
|
|
201
|
+
group_topics = [
|
|
202
|
+
_GroupTopic(name=name, partition_indexes=list(parts))
|
|
203
|
+
for name, parts in topics_partitions.items()
|
|
204
|
+
]
|
|
205
|
+
groups.append(_Group(group_id=group_id, topics=group_topics))
|
|
206
|
+
|
|
207
|
+
if len(groups) == 0:
|
|
208
|
+
raise ValueError('Empty group_specs!')
|
|
209
|
+
# Return multple requests when broker does not support v8+
|
|
210
|
+
if self._manager.broker_version_data.api_version(OffsetFetchRequest) < 8:
|
|
211
|
+
for group in groups:
|
|
212
|
+
min_version = 2 if group.topics is None else 0
|
|
213
|
+
yield (group.group_id, OffsetFetchRequest(group_id=group.group_id,
|
|
214
|
+
topics=group.topics,
|
|
215
|
+
min_version=min_version,
|
|
216
|
+
max_version=max_version))
|
|
217
|
+
else:
|
|
218
|
+
yield (None, OffsetFetchRequest(groups=groups,
|
|
219
|
+
min_version=8,
|
|
220
|
+
max_version=max_version))
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _parse_group_offsets(group):
|
|
224
|
+
"""Build {TopicPartition: OffsetAndMetadata} from an OffsetFetchResponse or OffsetFetchResponseGroup."""
|
|
225
|
+
error_type = Errors.for_code(group.error_code)
|
|
226
|
+
if error_type is not Errors.NoError:
|
|
227
|
+
raise error_type(
|
|
228
|
+
"OffsetFetchResponse failed for group '{}'.".format(group.group_id))
|
|
229
|
+
results = {}
|
|
230
|
+
for topic in group.topics:
|
|
231
|
+
for partition in topic.partitions:
|
|
232
|
+
tp = TopicPartition(topic.name, partition.partition_index)
|
|
233
|
+
partition_error = Errors.for_code(partition.error_code)
|
|
234
|
+
if partition_error is not Errors.NoError:
|
|
235
|
+
raise partition_error(
|
|
236
|
+
f"OffsetFetchResponse failed for partition {tp.partition}")
|
|
237
|
+
results[tp] = OffsetAndMetadata(
|
|
238
|
+
offset=partition.committed_offset,
|
|
239
|
+
metadata=partition.metadata,
|
|
240
|
+
leader_epoch=partition.committed_leader_epoch,
|
|
241
|
+
)
|
|
242
|
+
return results
|
|
243
|
+
|
|
244
|
+
def _list_group_offsets_process_response(self, response, group_id=None):
|
|
245
|
+
"""Process an OffsetFetchResponse."""
|
|
246
|
+
error_type = Errors.for_code(response.error_code)
|
|
247
|
+
if error_type is not Errors.NoError:
|
|
248
|
+
raise error_type(
|
|
249
|
+
"OffsetFetchResponse failed with response '{}'."
|
|
250
|
+
.format(response))
|
|
251
|
+
if response.API_VERSION >= 8:
|
|
252
|
+
return {group.group_id: self._parse_group_offsets(group)
|
|
253
|
+
for group in response.groups}
|
|
254
|
+
else:
|
|
255
|
+
return {group_id: self._parse_group_offsets(response)}
|
|
256
|
+
|
|
257
|
+
async def _async_list_group_offsets(self, group_specs):
|
|
258
|
+
# Bucket groups by coordinator. One OffsetFetch per coordinator.
|
|
259
|
+
coordinators_groups = defaultdict(list)
|
|
260
|
+
coordinator_ids = await self._find_coordinator_ids(list(group_specs))
|
|
261
|
+
for group_id, coordinator_id in coordinator_ids.items():
|
|
262
|
+
coordinators_groups[coordinator_id].append(group_id)
|
|
263
|
+
|
|
264
|
+
results = {}
|
|
265
|
+
_Group = OffsetFetchRequest.OffsetFetchRequestGroup
|
|
266
|
+
_GroupTopic = _Group.OffsetFetchRequestTopics
|
|
267
|
+
for coordinator_id, group_ids in coordinators_groups.items():
|
|
268
|
+
for group_id, request in self._list_group_offsets_requests({group_id: group_specs[group_id]
|
|
269
|
+
for group_id in group_ids}):
|
|
270
|
+
response = await self._manager.send(request, node_id=coordinator_id)
|
|
271
|
+
results.update(self._list_group_offsets_process_response(response, group_id=group_id))
|
|
272
|
+
return results
|
|
273
|
+
|
|
274
|
+
def list_group_offsets(self, group_specs):
|
|
275
|
+
"""Fetch committed offsets for one or more consumer groups.
|
|
276
|
+
|
|
277
|
+
On brokers supporting OffsetFetch v8+ (Apache Kafka 3.0+, KIP-709), this
|
|
278
|
+
issues a single OffsetFetch per coordinator covering all groups
|
|
279
|
+
hosted by that coordinator. On older brokers it currently only supports
|
|
280
|
+
one consumer group (per coordinator).
|
|
281
|
+
|
|
282
|
+
Arguments:
|
|
283
|
+
group_specs (dict): Mapping of group_id (str) to either a list of
|
|
284
|
+
:class:`~kafka.TopicPartition` to fetch, or None to fetch all
|
|
285
|
+
committed offsets for that group.
|
|
286
|
+
Or, one or more group_id (str or list[str]) to fetch all offsets
|
|
287
|
+
for each group.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
A dict mapping group_id (str) to a dict mapping
|
|
291
|
+
:class:`~kafka.TopicPartition` to
|
|
292
|
+
:class:`~kafka.structs.OffsetAndMetadata`.
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
UnsupportedVersionError: if multiple groups are requested against
|
|
296
|
+
a broker that does not support OffsetFetch v8+; or if group_spec
|
|
297
|
+
with value None against a broker that does not support
|
|
298
|
+
OffsetFetch v2+.
|
|
299
|
+
BrokerResponseError: as soon as any group- or partition-level error
|
|
300
|
+
is encountered.
|
|
301
|
+
"""
|
|
302
|
+
if isinstance(group_specs, list):
|
|
303
|
+
group_specs = {group_id: None for group_id in group_specs}
|
|
304
|
+
elif isinstance(group_specs, str):
|
|
305
|
+
group_specs = {group_specs: None}
|
|
306
|
+
return self._manager.run(self._async_list_group_offsets, group_specs)
|
|
307
|
+
|
|
308
|
+
# -- Delete groups ------------------------------------------------
|
|
309
|
+
|
|
310
|
+
def _delete_groups_request(self, group_ids):
|
|
311
|
+
return DeleteGroupsRequest(groups_names=group_ids)
|
|
312
|
+
|
|
313
|
+
def _convert_delete_groups_response(self, response):
|
|
314
|
+
"""Parse a DeleteGroupsResponse."""
|
|
315
|
+
results = []
|
|
316
|
+
for group_id, error_code in response.results:
|
|
317
|
+
res = 'OK' if error_code == 0 else Errors.for_code(error_code).__name__
|
|
318
|
+
results.append((group_id, res))
|
|
319
|
+
return results
|
|
320
|
+
|
|
321
|
+
async def _async_delete_groups(self, group_ids, group_coordinator_id=None):
|
|
322
|
+
coordinators_groups = defaultdict(list)
|
|
323
|
+
if group_coordinator_id is not None:
|
|
324
|
+
coordinators_groups[group_coordinator_id] = group_ids
|
|
325
|
+
else:
|
|
326
|
+
coordinator_ids = await self._find_coordinator_ids(group_ids)
|
|
327
|
+
for group_id, coordinator_id in coordinator_ids.items():
|
|
328
|
+
coordinators_groups[coordinator_id].append(group_id)
|
|
329
|
+
|
|
330
|
+
results = []
|
|
331
|
+
for coordinator_id, coordinator_group_ids in coordinators_groups.items():
|
|
332
|
+
request = self._delete_groups_request(coordinator_group_ids)
|
|
333
|
+
response = await self._manager.send(request, node_id=coordinator_id)
|
|
334
|
+
results.extend(self._convert_delete_groups_response(response))
|
|
335
|
+
return dict(results)
|
|
336
|
+
|
|
337
|
+
def delete_groups(self, group_ids, group_coordinator_id=None):
|
|
338
|
+
"""Delete Group Offsets for given consumer groups.
|
|
339
|
+
|
|
340
|
+
Note:
|
|
341
|
+
This does not verify that the group ids actually exist and
|
|
342
|
+
group_coordinator_id is the correct coordinator for all these groups.
|
|
343
|
+
|
|
344
|
+
The result needs checking for potential errors.
|
|
345
|
+
|
|
346
|
+
Arguments:
|
|
347
|
+
group_ids ([str]): The consumer group ids of the groups which are to be deleted.
|
|
348
|
+
|
|
349
|
+
Keyword Arguments:
|
|
350
|
+
group_coordinator_id (int, optional): The node_id of the broker which is
|
|
351
|
+
the coordinator for all the groups. Default: None.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
A list of tuples (group_id, KafkaError)
|
|
355
|
+
"""
|
|
356
|
+
return self._manager.run(self._async_delete_groups, group_ids, group_coordinator_id)
|
|
357
|
+
|
|
358
|
+
# -- Alter group offsets -----------------------------------------------
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _alter_group_offsets_request(group_id, offsets):
|
|
362
|
+
_Topic = OffsetCommitRequest.OffsetCommitRequestTopic
|
|
363
|
+
_Partition = _Topic.OffsetCommitRequestPartition
|
|
364
|
+
topic2partitions = defaultdict(list)
|
|
365
|
+
for tp, oam in offsets.items():
|
|
366
|
+
topic2partitions[tp.topic].append(_Partition(
|
|
367
|
+
partition_index=tp.partition,
|
|
368
|
+
committed_offset=oam.offset,
|
|
369
|
+
committed_leader_epoch=-1 if oam.leader_epoch is None else oam.leader_epoch,
|
|
370
|
+
committed_metadata=oam.metadata,
|
|
371
|
+
))
|
|
372
|
+
return OffsetCommitRequest(
|
|
373
|
+
group_id=group_id,
|
|
374
|
+
generation_id_or_member_epoch=DEFAULT_GENERATION_ID,
|
|
375
|
+
member_id=UNKNOWN_MEMBER_ID,
|
|
376
|
+
group_instance_id=None,
|
|
377
|
+
retention_time_ms=-1,
|
|
378
|
+
topics=[_Topic(name=name, partitions=parts)
|
|
379
|
+
for name, parts in topic2partitions.items()],
|
|
380
|
+
max_version=8,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _alter_group_offsets_process_response(response):
|
|
385
|
+
results = {}
|
|
386
|
+
for topic in response.topics:
|
|
387
|
+
for partition in topic.partitions:
|
|
388
|
+
results[TopicPartition(topic.name, partition.partition_index)] = \
|
|
389
|
+
Errors.for_code(partition.error_code)
|
|
390
|
+
return results
|
|
391
|
+
|
|
392
|
+
async def _async_alter_group_offsets(self, group_id, offsets, group_coordinator_id=None):
|
|
393
|
+
if not offsets:
|
|
394
|
+
return {}
|
|
395
|
+
if group_coordinator_id is None:
|
|
396
|
+
group_coordinator_id = await self._find_coordinator_id(group_id)
|
|
397
|
+
request = self._alter_group_offsets_request(group_id, offsets)
|
|
398
|
+
response = await self._manager.send(request, node_id=group_coordinator_id)
|
|
399
|
+
return self._alter_group_offsets_process_response(response)
|
|
400
|
+
|
|
401
|
+
def alter_group_offsets(self, group_id, offsets, group_coordinator_id=None):
|
|
402
|
+
"""Alter committed offsets for a consumer group.
|
|
403
|
+
|
|
404
|
+
The group must have no active members (i.e. be empty or dead) for
|
|
405
|
+
the commit to succeed; otherwise individual partitions may return
|
|
406
|
+
``UNKNOWN_MEMBER_ID`` or similar errors.
|
|
407
|
+
|
|
408
|
+
Arguments:
|
|
409
|
+
group_id (str): The consumer group id.
|
|
410
|
+
offsets (dict): A dict mapping :class:`~kafka.TopicPartition` to
|
|
411
|
+
:class:`~kafka.structs.OffsetAndMetadata`.
|
|
412
|
+
|
|
413
|
+
Keyword Arguments:
|
|
414
|
+
group_coordinator_id (int, optional): The node_id of the group's
|
|
415
|
+
coordinator broker. If None, the cluster will be queried to
|
|
416
|
+
locate the coordinator. Default: None.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
dict: A dict mapping :class:`~kafka.TopicPartition` to the
|
|
420
|
+
partition-level :class:`~kafka.errors.KafkaError` class
|
|
421
|
+
(``NoError`` on success).
|
|
422
|
+
"""
|
|
423
|
+
return self._manager.run(
|
|
424
|
+
self._async_alter_group_offsets, group_id, offsets, group_coordinator_id)
|
|
425
|
+
|
|
426
|
+
# -- Reset group offsets ----------------------------------------------
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _reset_group_offsets_process_response(response, to_reset):
|
|
430
|
+
results = {}
|
|
431
|
+
for topic in response.topics:
|
|
432
|
+
for partition in topic.partitions:
|
|
433
|
+
tp = TopicPartition(topic.name, partition.partition_index)
|
|
434
|
+
results[tp] = {
|
|
435
|
+
'error': Errors.for_code(partition.error_code),
|
|
436
|
+
'offset': to_reset[tp].offset
|
|
437
|
+
}
|
|
438
|
+
return results
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _clamp_offset(raw, earliest, latest):
|
|
442
|
+
if raw < 0 or raw > latest:
|
|
443
|
+
return latest
|
|
444
|
+
if raw < earliest:
|
|
445
|
+
return earliest
|
|
446
|
+
return raw
|
|
447
|
+
|
|
448
|
+
async def _async_reset_group_offsets(self, group_id, offset_specs, group_coordinator_id=None):
|
|
449
|
+
if not offset_specs:
|
|
450
|
+
return {}
|
|
451
|
+
all_tps = set(offset_specs.keys())
|
|
452
|
+
|
|
453
|
+
explicit_offsets = {}
|
|
454
|
+
for tp, val in list(offset_specs.items()):
|
|
455
|
+
if isinstance(val, (OffsetSpec, OffsetTimestamp)):
|
|
456
|
+
pass
|
|
457
|
+
elif isinstance(val, int):
|
|
458
|
+
explicit_offsets[tp] = offset_specs.pop(tp)
|
|
459
|
+
else:
|
|
460
|
+
raise TypeError(
|
|
461
|
+
f'Unsupported reset target for {tp}: {val!r} '
|
|
462
|
+
'(expected OffsetSpec, OffsetTimestamp, or int offset)')
|
|
463
|
+
|
|
464
|
+
if group_coordinator_id is None:
|
|
465
|
+
group_coordinator_id = await self._find_coordinator_id(group_id)
|
|
466
|
+
|
|
467
|
+
current = (await self._async_list_group_offsets({group_id: list(all_tps)}))[group_id]
|
|
468
|
+
earliest = await self._async_list_partition_offsets({tp: OffsetSpec.EARLIEST for tp in all_tps})
|
|
469
|
+
latest = await self._async_list_partition_offsets({tp: OffsetSpec.LATEST for tp in all_tps})
|
|
470
|
+
|
|
471
|
+
offsets = {}
|
|
472
|
+
if offset_specs:
|
|
473
|
+
offsets = await self._async_list_partition_offsets(offset_specs)
|
|
474
|
+
|
|
475
|
+
to_reset = {}
|
|
476
|
+
for tp in all_tps:
|
|
477
|
+
if tp in offsets:
|
|
478
|
+
raw = offsets[tp].offset
|
|
479
|
+
else:
|
|
480
|
+
raw = explicit_offsets[tp]
|
|
481
|
+
clamped = self._clamp_offset(raw, earliest[tp].offset, latest[tp].offset)
|
|
482
|
+
if tp in current:
|
|
483
|
+
to_reset[tp] = current[tp]._replace(offset=clamped)
|
|
484
|
+
else:
|
|
485
|
+
to_reset[tp] = OffsetAndMetadata(offset=clamped, metadata='', leader_epoch=None)
|
|
486
|
+
|
|
487
|
+
request = self._alter_group_offsets_request(group_id, to_reset)
|
|
488
|
+
response = await self._manager.send(request, node_id=group_coordinator_id)
|
|
489
|
+
return self._reset_group_offsets_process_response(response, to_reset)
|
|
490
|
+
|
|
491
|
+
def reset_group_offsets(self, group_id, offset_specs, group_coordinator_id=None):
|
|
492
|
+
"""Reset committed offsets for a consumer group.
|
|
493
|
+
|
|
494
|
+
The group must have no active members (i.e. be empty or dead) for
|
|
495
|
+
the reset to succeed; otherwise individual partitions may return
|
|
496
|
+
``UNKNOWN_MEMBER_ID`` or similar errors.
|
|
497
|
+
|
|
498
|
+
Each dict value selects how the target offset is produced. All
|
|
499
|
+
resulting offsets are clamped to the partition's
|
|
500
|
+
``[earliest, latest]`` range; values that resolve to
|
|
501
|
+
``UNKNOWN_OFFSET`` (e.g. a timestamp beyond the last record) are
|
|
502
|
+
clamped to ``latest``.
|
|
503
|
+
|
|
504
|
+
Arguments:
|
|
505
|
+
group_id (str): The consumer group id.
|
|
506
|
+
offset_specs (dict): A dict mapping :class:`~kafka.TopicPartition` to
|
|
507
|
+
one of:
|
|
508
|
+
|
|
509
|
+
* :class:`~kafka.admin.OffsetSpec` (e.g. ``OffsetSpec.EARLIEST``,
|
|
510
|
+
``OffsetSpec.LATEST``, ``OffsetSpec.MAX_TIMESTAMP``):
|
|
511
|
+
resolved server-side via ListOffsets.
|
|
512
|
+
* :class:`~kafka.admin.OffsetTimestamp` (ms since epoch):
|
|
513
|
+
resolved server-side to the earliest offset whose timestamp
|
|
514
|
+
is ``>=`` the given value.
|
|
515
|
+
* Plain ``int``: an explicit committed offset (no server-side
|
|
516
|
+
resolution), which is still clamped to the valid range.
|
|
517
|
+
|
|
518
|
+
Keyword Arguments:
|
|
519
|
+
group_coordinator_id (int, optional): The node_id of the group's
|
|
520
|
+
coordinator broker. If None, the cluster will be queried to
|
|
521
|
+
locate the coordinator. Default: None.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
dict: A dict mapping :class:`~kafka.TopicPartition` to dict of
|
|
525
|
+
{'error': :class:`~kafka.errors.KafkaError` class, 'offset': int}.
|
|
526
|
+
The ``offset`` value is the post-clamp value that was committed.
|
|
527
|
+
"""
|
|
528
|
+
return self._manager.run(
|
|
529
|
+
self._async_reset_group_offsets, group_id, offset_specs, group_coordinator_id)
|
|
530
|
+
|
|
531
|
+
# -- Delete group offsets ----------------------------------------------
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def _delete_group_offsets_request(group_id, partitions):
|
|
535
|
+
_Topic = OffsetDeleteRequest.OffsetDeleteRequestTopic
|
|
536
|
+
_Partition = _Topic.OffsetDeleteRequestPartition
|
|
537
|
+
topic2partitions = defaultdict(list)
|
|
538
|
+
for tp in partitions:
|
|
539
|
+
topic2partitions[tp.topic].append(
|
|
540
|
+
_Partition(partition_index=tp.partition))
|
|
541
|
+
return OffsetDeleteRequest(
|
|
542
|
+
group_id=group_id,
|
|
543
|
+
topics=[_Topic(name=name, partitions=parts)
|
|
544
|
+
for name, parts in topic2partitions.items()],
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _delete_group_offsets_process_response(response):
|
|
549
|
+
top_level = Errors.for_code(response.error_code)
|
|
550
|
+
if top_level is not Errors.NoError:
|
|
551
|
+
raise top_level(
|
|
552
|
+
"OffsetDeleteRequest failed with response '{}'.".format(response))
|
|
553
|
+
results = {}
|
|
554
|
+
for topic in response.topics:
|
|
555
|
+
for partition in topic.partitions:
|
|
556
|
+
results[TopicPartition(topic.name, partition.partition_index)] = \
|
|
557
|
+
Errors.for_code(partition.error_code)
|
|
558
|
+
return results
|
|
559
|
+
|
|
560
|
+
async def _async_delete_group_offsets(self, group_id, partitions, group_coordinator_id=None):
|
|
561
|
+
if not partitions:
|
|
562
|
+
return {}
|
|
563
|
+
if group_coordinator_id is None:
|
|
564
|
+
group_coordinator_id = await self._find_coordinator_id(group_id)
|
|
565
|
+
request = self._delete_group_offsets_request(group_id, partitions)
|
|
566
|
+
response = await self._manager.send(request, node_id=group_coordinator_id)
|
|
567
|
+
return self._delete_group_offsets_process_response(response)
|
|
568
|
+
|
|
569
|
+
def delete_group_offsets(self, group_id, partitions, group_coordinator_id=None):
|
|
570
|
+
"""Delete committed offsets for a consumer group.
|
|
571
|
+
|
|
572
|
+
The group must have no active members subscribed to the given topics;
|
|
573
|
+
otherwise partitions may fail with ``GROUP_SUBSCRIBED_TO_TOPIC``.
|
|
574
|
+
|
|
575
|
+
Arguments:
|
|
576
|
+
group_id (str): The consumer group id.
|
|
577
|
+
partitions: An iterable of :class:`~kafka.TopicPartition` whose
|
|
578
|
+
committed offsets should be deleted.
|
|
579
|
+
|
|
580
|
+
Keyword Arguments:
|
|
581
|
+
group_coordinator_id (int, optional): The node_id of the group's
|
|
582
|
+
coordinator broker. If None, the cluster will be queried to
|
|
583
|
+
locate the coordinator. Default: None.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
dict: A dict mapping :class:`~kafka.TopicPartition` to the
|
|
587
|
+
partition-level :class:`~kafka.errors.KafkaError` class
|
|
588
|
+
(``NoError`` on success).
|
|
589
|
+
|
|
590
|
+
Raises:
|
|
591
|
+
KafkaError: If the response contains a top-level error (e.g.
|
|
592
|
+
``GroupIdNotFoundError``, ``NonEmptyGroupError``).
|
|
593
|
+
"""
|
|
594
|
+
return self._manager.run(
|
|
595
|
+
self._async_delete_group_offsets, group_id, partitions, group_coordinator_id)
|
|
596
|
+
|
|
597
|
+
# -- Remove group members ---------------------------------------------
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _remove_group_members_batch_request(group_id, members, version):
|
|
601
|
+
_Member = LeaveGroupRequest.MemberIdentity
|
|
602
|
+
identities = []
|
|
603
|
+
for m in members:
|
|
604
|
+
kwargs = {
|
|
605
|
+
'member_id': m.member_id if m.member_id is not None else '',
|
|
606
|
+
'group_instance_id': m.group_instance_id,
|
|
607
|
+
}
|
|
608
|
+
if version >= 5:
|
|
609
|
+
kwargs['reason'] = m.reason
|
|
610
|
+
identities.append(_Member(**kwargs))
|
|
611
|
+
return LeaveGroupRequest(
|
|
612
|
+
group_id=group_id,
|
|
613
|
+
members=identities,
|
|
614
|
+
min_version=3,
|
|
615
|
+
max_version=version,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
@staticmethod
|
|
619
|
+
def _remove_group_members_process_batch_response(response):
|
|
620
|
+
top_level = Errors.for_code(response.error_code)
|
|
621
|
+
if top_level is not Errors.NoError:
|
|
622
|
+
raise top_level(
|
|
623
|
+
"LeaveGroupRequest failed with response '{}'.".format(response))
|
|
624
|
+
return {
|
|
625
|
+
(m.member_id or m.group_instance_id): Errors.for_code(m.error_code)
|
|
626
|
+
for m in response.members
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async def _async_remove_group_members(self, group_id, members,
|
|
630
|
+
group_coordinator_id=None):
|
|
631
|
+
if not members:
|
|
632
|
+
return {}
|
|
633
|
+
if group_coordinator_id is None:
|
|
634
|
+
group_coordinator_id = await self._find_coordinator_id(group_id)
|
|
635
|
+
|
|
636
|
+
version = self._manager.broker_version_data.api_version(LeaveGroupRequest)
|
|
637
|
+
batch_supported = version >= 3
|
|
638
|
+
|
|
639
|
+
if batch_supported:
|
|
640
|
+
request = self._remove_group_members_batch_request(
|
|
641
|
+
group_id, members, version)
|
|
642
|
+
response = await self._manager.send(request, node_id=group_coordinator_id)
|
|
643
|
+
return self._remove_group_members_process_batch_response(response)
|
|
644
|
+
|
|
645
|
+
results = {}
|
|
646
|
+
for m in members:
|
|
647
|
+
if m.group_instance_id is not None:
|
|
648
|
+
raise Errors.UnsupportedVersionError(
|
|
649
|
+
"Broker does not support removing members by group.instance.id; "
|
|
650
|
+
"requires LeaveGroup v3+ (Kafka 2.3+).")
|
|
651
|
+
if not m.member_id:
|
|
652
|
+
raise ValueError(
|
|
653
|
+
"MemberToRemove.member_id is required when broker does not "
|
|
654
|
+
"support batched LeaveGroupRequest (v3+).")
|
|
655
|
+
request = LeaveGroupRequest(
|
|
656
|
+
group_id=group_id,
|
|
657
|
+
member_id=m.member_id,
|
|
658
|
+
max_version=2,
|
|
659
|
+
)
|
|
660
|
+
response = await self._manager.send(request, node_id=group_coordinator_id)
|
|
661
|
+
results[m.member_id or m.group_instance_id] = Errors.for_code(response.error_code)
|
|
662
|
+
return results
|
|
663
|
+
|
|
664
|
+
def remove_group_members(self, group_id, members, group_coordinator_id=None):
|
|
665
|
+
"""Remove members from a consumer group.
|
|
666
|
+
|
|
667
|
+
On brokers supporting LeaveGroup v3+ (Kafka 2.3+), a single batched
|
|
668
|
+
request is sent. On older brokers, falls back to one single-member
|
|
669
|
+
LeaveGroupRequest per member (in which case ``group_instance_id`` is
|
|
670
|
+
not supported and ``member_id`` is required).
|
|
671
|
+
|
|
672
|
+
Arguments:
|
|
673
|
+
group_id (str): The consumer group id.
|
|
674
|
+
members: An iterable of :class:`~kafka.admin.MemberToRemove`.
|
|
675
|
+
Each entry must set at least one of ``member_id`` or,
|
|
676
|
+
if brokers support LeaveGroup v3+, ``group_instance_id``.
|
|
677
|
+
``reason`` is only sent to brokers supporting
|
|
678
|
+
LeaveGroup v5+ (KIP-800).
|
|
679
|
+
|
|
680
|
+
Keyword Arguments:
|
|
681
|
+
group_coordinator_id (int, optional): The node_id of the group's
|
|
682
|
+
coordinator broker. If None, the cluster will be queried to
|
|
683
|
+
locate the coordinator. Default: None.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
dict: A dict mapping :class:`~kafka.admin.MemberToRemove` to the
|
|
687
|
+
per-member :class:`~kafka.errors.KafkaError` class
|
|
688
|
+
(``NoError`` on success). The key's ``reason`` is always None in
|
|
689
|
+
the result (not echoed by the broker).
|
|
690
|
+
|
|
691
|
+
Raises:
|
|
692
|
+
KafkaError: If a batched response contains a top-level error.
|
|
693
|
+
UnsupportedVersionError: If the broker does not support batched
|
|
694
|
+
LeaveGroupRequest and any member uses ``group_instance_id``.
|
|
695
|
+
"""
|
|
696
|
+
return self._manager.run(
|
|
697
|
+
self._async_remove_group_members, group_id, members, group_coordinator_id)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class MemberToRemove:
|
|
701
|
+
"""A consumer group member to remove via Admin.remove_group_members
|
|
702
|
+
|
|
703
|
+
At least one of ``member_id`` (identifying a dynamic group member)
|
|
704
|
+
or ``group_instance_id`` (identifying a static group member) must be set.
|
|
705
|
+
|
|
706
|
+
Keyword Arguments:
|
|
707
|
+
member_id (str or None): The dynamic member id (as assigned by the
|
|
708
|
+
coordinator in JoinGroupResponse). Use None for static-only removal.
|
|
709
|
+
group_instance_id (str or None): The static member instance id (the
|
|
710
|
+
``group.instance.id`` configured on the member). Requires LeaveGroup
|
|
711
|
+
v3+ (Kafka 2.3+).
|
|
712
|
+
reason (str or None): Optional reason for removal (propagated to the
|
|
713
|
+
broker on LeaveGroup v5+; ignored on older brokers).
|
|
714
|
+
"""
|
|
715
|
+
__slots__ = ('member_id', 'group_instance_id', 'reason')
|
|
716
|
+
|
|
717
|
+
def __init__(self, member_id=None, group_instance_id=None, reason=None):
|
|
718
|
+
self.member_id = member_id
|
|
719
|
+
self.group_instance_id = group_instance_id
|
|
720
|
+
self.reason = reason
|
|
721
|
+
|
|
722
|
+
def __repr__(self):
|
|
723
|
+
return "<MemberToRemove member_id={}, group_instance_id={}, reason={}>".format(
|
|
724
|
+
self.member_id, self.group_instance_id, self.reason)
|
|
725
|
+
|
|
726
|
+
def __eq__(self, other):
|
|
727
|
+
return all((
|
|
728
|
+
self.member_id == other.member_id,
|
|
729
|
+
self.group_instance_id == other.group_instance_id,
|
|
730
|
+
self.reason == other.reason,
|
|
731
|
+
))
|
|
732
|
+
|
|
733
|
+
def __hash__(self):
|
|
734
|
+
return hash((self.member_id, self.group_instance_id, self.reason))
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class GroupState(EnumHelper, str, Enum):
|
|
738
|
+
"""Consumer group states as reported by the broker (KIP-518, KIP-848)."""
|
|
739
|
+
UNKNOWN = 'Unknown'
|
|
740
|
+
PREPARING_REBALANCE = 'PreparingRebalance'
|
|
741
|
+
COMPLETING_REBALANCE = 'CompletingRebalance'
|
|
742
|
+
STABLE = 'Stable'
|
|
743
|
+
DEAD = 'Dead'
|
|
744
|
+
EMPTY = 'Empty'
|
|
745
|
+
ASSIGNING = 'Assigning'
|
|
746
|
+
RECONCILING = 'Reconciling'
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class GroupType(EnumHelper, str, Enum):
|
|
750
|
+
"""Consumer group protocol types (KIP-848)."""
|
|
751
|
+
UNKNOWN = 'Unknown'
|
|
752
|
+
CLASSIC = 'classic'
|
|
753
|
+
CONSUMER = 'consumer'
|
|
754
|
+
SHARE = 'share'
|