kafka-python 2.0.2__py2.py3-none-any.whl → 2.0.3__py2.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 CHANGED
@@ -4,7 +4,7 @@ __title__ = 'kafka'
4
4
  from kafka.version import __version__
5
5
  __author__ = 'Dana Powers'
6
6
  __license__ = 'Apache License 2.0'
7
- __copyright__ = 'Copyright 2016 Dana Powers, David Arthur, and Contributors'
7
+ __copyright__ = 'Copyright 2025 Dana Powers, David Arthur, and Contributors'
8
8
 
9
9
  # Set default logging handler to avoid "No handler found" warnings.
10
10
  import logging
kafka/admin/client.py CHANGED
@@ -20,7 +20,7 @@ from kafka.metrics import MetricConfig, Metrics
20
20
  from kafka.protocol.admin import (
21
21
  CreateTopicsRequest, DeleteTopicsRequest, DescribeConfigsRequest, AlterConfigsRequest, CreatePartitionsRequest,
22
22
  ListGroupsRequest, DescribeGroupsRequest, DescribeAclsRequest, CreateAclsRequest, DeleteAclsRequest,
23
- DeleteGroupsRequest
23
+ DeleteGroupsRequest, DescribeLogDirsRequest
24
24
  )
25
25
  from kafka.protocol.commit import GroupCoordinatorRequest, OffsetFetchRequest
26
26
  from kafka.protocol.metadata import MetadataRequest
@@ -146,6 +146,7 @@ class KafkaAdminClient(object):
146
146
  sasl mechanism handshake. Default: one of bootstrap servers
147
147
  sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
148
148
  instance. (See kafka.oauth.abstract). Default: None
149
+ kafka_client (callable): Custom class / callable for creating KafkaClient instances
149
150
 
150
151
  """
151
152
  DEFAULT_CONFIG = {
@@ -186,6 +187,7 @@ class KafkaAdminClient(object):
186
187
  'metric_reporters': [],
187
188
  'metrics_num_samples': 2,
188
189
  'metrics_sample_window_ms': 30000,
190
+ 'kafka_client': KafkaClient,
189
191
  }
190
192
 
191
193
  def __init__(self, **configs):
@@ -205,9 +207,11 @@ class KafkaAdminClient(object):
205
207
  reporters = [reporter() for reporter in self.config['metric_reporters']]
206
208
  self._metrics = Metrics(metric_config, reporters)
207
209
 
208
- self._client = KafkaClient(metrics=self._metrics,
209
- metric_group_prefix='admin',
210
- **self.config)
210
+ self._client = self.config['kafka_client'](
211
+ metrics=self._metrics,
212
+ metric_group_prefix='admin',
213
+ **self.config
214
+ )
211
215
  self._client.check_version(timeout=(self.config['api_version_auto_timeout_ms'] / 1000))
212
216
 
213
217
  # Get auto-discovered version from client if necessary
@@ -351,13 +355,14 @@ class KafkaAdminClient(object):
351
355
  }
352
356
  return groups_coordinators
353
357
 
354
- def _send_request_to_node(self, node_id, request):
358
+ def _send_request_to_node(self, node_id, request, wakeup=True):
355
359
  """Send a Kafka protocol message to a specific broker.
356
360
 
357
361
  Returns a future that may be polled for status and results.
358
362
 
359
363
  :param node_id: The broker id to which to send the message.
360
364
  :param request: The message to send.
365
+ :param wakeup: Optional flag to disable thread-wakeup.
361
366
  :return: A future object that may be polled for status and results.
362
367
  :exception: The exception if the message could not be sent.
363
368
  """
@@ -365,7 +370,7 @@ class KafkaAdminClient(object):
365
370
  # poll until the connection to broker is ready, otherwise send()
366
371
  # will fail with NodeNotReadyError
367
372
  self._client.poll()
368
- return self._client.send(node_id, request)
373
+ return self._client.send(node_id, request, wakeup)
369
374
 
370
375
  def _send_request_to_controller(self, request):
371
376
  """Send a Kafka protocol message to the cluster controller.
@@ -1205,7 +1210,7 @@ class KafkaAdminClient(object):
1205
1210
 
1206
1211
  :param response: an OffsetFetchResponse.
1207
1212
  :return: A dictionary composed of TopicPartition keys and
1208
- OffsetAndMetada values.
1213
+ OffsetAndMetadata values.
1209
1214
  """
1210
1215
  if response.API_VERSION <= 3:
1211
1216
 
@@ -1219,7 +1224,7 @@ class KafkaAdminClient(object):
1219
1224
  .format(response))
1220
1225
 
1221
1226
  # transform response into a dictionary with TopicPartition keys and
1222
- # OffsetAndMetada values--this is what the Java AdminClient returns
1227
+ # OffsetAndMetadata values--this is what the Java AdminClient returns
1223
1228
  offsets = {}
1224
1229
  for topic, partitions in response.topics:
1225
1230
  for partition, offset, metadata, error_code in partitions:
@@ -1340,3 +1345,19 @@ class KafkaAdminClient(object):
1340
1345
 
1341
1346
  if future.failed():
1342
1347
  raise future.exception # pylint: disable-msg=raising-bad-type
1348
+
1349
+ def describe_log_dirs(self):
1350
+ """Send a DescribeLogDirsRequest request to a broker.
1351
+
1352
+ :return: A message future
1353
+ """
1354
+ version = self._matching_api_version(DescribeLogDirsRequest)
1355
+ if version <= 0:
1356
+ request = DescribeLogDirsRequest[version]()
1357
+ future = self._send_request_to_node(self._client.least_loaded_node(), request)
1358
+ self._wait_for_futures([future])
1359
+ else:
1360
+ raise NotImplementedError(
1361
+ "Support for DescribeLogDirsRequest_v{} has not yet been added to KafkaAdminClient."
1362
+ .format(version))
1363
+ return future.value
kafka/codec.py CHANGED
@@ -187,7 +187,7 @@ def _detect_xerial_stream(payload):
187
187
  The version is the version of this format as written by xerial,
188
188
  in the wild this is currently 1 as such we only support v1.
189
189
 
190
- Compat is there to claim the miniumum supported version that
190
+ Compat is there to claim the minimum supported version that
191
191
  can read a xerial block stream, presently in the wild this is
192
192
  1.
193
193
  """
kafka/conn.py CHANGED
@@ -24,7 +24,7 @@ import kafka.errors as Errors
24
24
  from kafka.future import Future
25
25
  from kafka.metrics.stats import Avg, Count, Max, Rate
26
26
  from kafka.oauth.abstract import AbstractTokenProvider
27
- from kafka.protocol.admin import SaslHandShakeRequest, DescribeAclsRequest_v2
27
+ from kafka.protocol.admin import SaslHandShakeRequest, DescribeAclsRequest_v2, DescribeClientQuotasRequest
28
28
  from kafka.protocol.commit import OffsetFetchRequest
29
29
  from kafka.protocol.offset import OffsetRequest
30
30
  from kafka.protocol.produce import ProduceRequest
@@ -78,7 +78,7 @@ except ImportError:
78
78
  try:
79
79
  import gssapi
80
80
  from gssapi.raw.misc import GSSError
81
- except ImportError:
81
+ except (ImportError, OSError):
82
82
  #no gssapi available, will disable gssapi mechanism
83
83
  gssapi = None
84
84
  GSSError = None
@@ -496,7 +496,7 @@ class BrokerConnection(object):
496
496
  try:
497
497
  self._sock = self._ssl_context.wrap_socket(
498
498
  self._sock,
499
- server_hostname=self.host,
499
+ server_hostname=self.host.rstrip("."),
500
500
  do_handshake_on_connect=False)
501
501
  except ssl.SSLError as e:
502
502
  log.exception('%s: Failed to wrap socket in SSLContext!', self)
@@ -916,7 +916,7 @@ class BrokerConnection(object):
916
916
  with self._lock:
917
917
  if self.state is ConnectionStates.DISCONNECTED:
918
918
  return
919
- log.info('%s: Closing connection. %s', self, error or '')
919
+ log.log(logging.ERROR if error else logging.INFO, '%s: Closing connection. %s', self, error or '')
920
920
  self._update_reconnect_backoff()
921
921
  self._sasl_auth_future = None
922
922
  self._protocol = KafkaProtocol(
@@ -1169,6 +1169,7 @@ class BrokerConnection(object):
1169
1169
  # in reverse order. As soon as we find one that works, return it
1170
1170
  test_cases = [
1171
1171
  # format (<broker version>, <needed struct>)
1172
+ ((2, 6, 0), DescribeClientQuotasRequest[0]),
1172
1173
  ((2, 5, 0), DescribeAclsRequest_v2),
1173
1174
  ((2, 4, 0), ProduceRequest[8]),
1174
1175
  ((2, 3, 0), FetchRequest[11]),
kafka/consumer/fetcher.py CHANGED
@@ -125,7 +125,7 @@ class Fetcher(six.Iterator):
125
125
  log.debug("Sending FetchRequest to node %s", node_id)
126
126
  future = self._client.send(node_id, request, wakeup=False)
127
127
  future.add_callback(self._handle_fetch_response, request, time.time())
128
- future.add_errback(log.error, 'Fetch to node %s failed: %s', node_id)
128
+ future.add_errback(self._handle_fetch_error, node_id)
129
129
  futures.append(future)
130
130
  self._fetch_futures.extend(futures)
131
131
  self._clean_done_fetch_futures()
@@ -778,6 +778,14 @@ class Fetcher(six.Iterator):
778
778
  self._sensors.fetch_throttle_time_sensor.record(response.throttle_time_ms)
779
779
  self._sensors.fetch_latency.record((time.time() - send_time) * 1000)
780
780
 
781
+ def _handle_fetch_error(self, node_id, exception):
782
+ log.log(
783
+ logging.INFO if isinstance(exception, Errors.Cancelled) else logging.ERROR,
784
+ 'Fetch to node %s failed: %s',
785
+ node_id,
786
+ exception
787
+ )
788
+
781
789
  def _parse_fetched_data(self, completed_fetch):
782
790
  tp = completed_fetch.topic_partition
783
791
  fetch_offset = completed_fetch.fetched_offset
@@ -817,8 +825,9 @@ class Fetcher(six.Iterator):
817
825
  position)
818
826
  unpacked = list(self._unpack_message_set(tp, records))
819
827
  parsed_records = self.PartitionRecords(fetch_offset, tp, unpacked)
820
- last_offset = unpacked[-1].offset
821
- self._sensors.records_fetch_lag.record(highwater - last_offset)
828
+ if unpacked:
829
+ last_offset = unpacked[-1].offset
830
+ self._sensors.records_fetch_lag.record(highwater - last_offset)
822
831
  num_bytes = records.valid_bytes()
823
832
  records_count = len(unpacked)
824
833
  elif records.size_in_bytes() > 0:
kafka/consumer/group.py CHANGED
@@ -244,6 +244,7 @@ class KafkaConsumer(six.Iterator):
244
244
  sasl mechanism handshake. Default: one of bootstrap servers
245
245
  sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
246
246
  instance. (See kafka.oauth.abstract). Default: None
247
+ kafka_client (callable): Custom class / callable for creating KafkaClient instances
247
248
 
248
249
  Note:
249
250
  Configuration parameters are described in more detail at
@@ -306,6 +307,7 @@ class KafkaConsumer(six.Iterator):
306
307
  'sasl_kerberos_domain_name': None,
307
308
  'sasl_oauth_token_provider': None,
308
309
  'legacy_iterator': False, # enable to revert to < 1.4.7 iterator
310
+ 'kafka_client': KafkaClient,
309
311
  }
310
312
  DEFAULT_SESSION_TIMEOUT_MS_0_9 = 30000
311
313
 
@@ -353,7 +355,7 @@ class KafkaConsumer(six.Iterator):
353
355
  log.warning('use api_version=%s [tuple] -- "%s" as str is deprecated',
354
356
  str(self.config['api_version']), str_version)
355
357
 
356
- self._client = KafkaClient(metrics=self._metrics, **self.config)
358
+ self._client = self.config['kafka_client'](metrics=self._metrics, **self.config)
357
359
 
358
360
  # Get auto-discovered version from client if necessary
359
361
  if self.config['api_version'] is None:
@@ -651,7 +653,7 @@ class KafkaConsumer(six.Iterator):
651
653
  # Poll for new data until the timeout expires
652
654
  start = time.time()
653
655
  remaining = timeout_ms
654
- while True:
656
+ while not self._closed:
655
657
  records = self._poll_once(remaining, max_records, update_offsets=update_offsets)
656
658
  if records:
657
659
  return records
@@ -660,7 +662,9 @@ class KafkaConsumer(six.Iterator):
660
662
  remaining = timeout_ms - elapsed_ms
661
663
 
662
664
  if remaining <= 0:
663
- return {}
665
+ break
666
+
667
+ return {}
664
668
 
665
669
  def _poll_once(self, timeout_ms, max_records, update_offsets=True):
666
670
  """Do one round of polling. In addition to checking for new data, this does
@@ -648,15 +648,19 @@ class StickyPartitionAssignor(AbstractPartitionAssignor):
648
648
 
649
649
  @classmethod
650
650
  def metadata(cls, topics):
651
- if cls.member_assignment is None:
651
+ return cls._metadata(topics, cls.member_assignment, cls.generation)
652
+
653
+ @classmethod
654
+ def _metadata(cls, topics, member_assignment_partitions, generation=-1):
655
+ if member_assignment_partitions is None:
652
656
  log.debug("No member assignment available")
653
657
  user_data = b''
654
658
  else:
655
659
  log.debug("Member assignment is available, generating the metadata: generation {}".format(cls.generation))
656
660
  partitions_by_topic = defaultdict(list)
657
- for topic_partition in cls.member_assignment: # pylint: disable=not-an-iterable
661
+ for topic_partition in member_assignment_partitions:
658
662
  partitions_by_topic[topic_partition.topic].append(topic_partition.partition)
659
- data = StickyAssignorUserDataV1(six.iteritems(partitions_by_topic), cls.generation)
663
+ data = StickyAssignorUserDataV1(six.viewitems(partitions_by_topic), generation)
660
664
  user_data = data.encode()
661
665
  return ConsumerProtocolMemberMetadata(cls.version, list(topics), user_data)
662
666
 
kafka/coordinator/base.py CHANGED
@@ -952,7 +952,7 @@ class HeartbeatThread(threading.Thread):
952
952
  # disable here to prevent propagating an exception to this
953
953
  # heartbeat thread
954
954
  # must get client._lock, or maybe deadlock at heartbeat
955
- # failure callbak in consumer poll
955
+ # failure callback in consumer poll
956
956
  self.coordinator._client.poll(timeout_ms=0)
957
957
 
958
958
  with self.coordinator._lock:
@@ -990,6 +990,11 @@ class HeartbeatThread(threading.Thread):
990
990
  # foreground thread has stalled in between calls to
991
991
  # poll(), so we explicitly leave the group.
992
992
  log.warning('Heartbeat poll expired, leaving group')
993
+ ### XXX
994
+ # maybe_leave_group acquires client + coordinator lock;
995
+ # if we hold coordinator lock before calling, we risk deadlock
996
+ # release() is safe here because this is the last code in the current context
997
+ self.coordinator._lock.release()
993
998
  self.coordinator.maybe_leave_group()
994
999
 
995
1000
  elif not self.coordinator.heartbeat.should_heartbeat():
kafka/producer/kafka.py CHANGED
@@ -233,7 +233,7 @@ class KafkaProducer(object):
233
233
  should verify that the certificate matches the brokers hostname.
234
234
  default: true.
235
235
  ssl_cafile (str): optional filename of ca file to use in certificate
236
- veriication. default: none.
236
+ verification. default: none.
237
237
  ssl_certfile (str): optional filename of file in pem format containing
238
238
  the client certificate, as well as any ca certificates needed to
239
239
  establish the certificate's authenticity. default: none.
@@ -280,10 +280,11 @@ class KafkaProducer(object):
280
280
  sasl mechanism handshake. Default: one of bootstrap servers
281
281
  sasl_oauth_token_provider (AbstractTokenProvider): OAuthBearer token provider
282
282
  instance. (See kafka.oauth.abstract). Default: None
283
+ kafka_client (callable): Custom class / callable for creating KafkaClient instances
283
284
 
284
285
  Note:
285
286
  Configuration parameters are described in more detail at
286
- https://kafka.apache.org/0100/configuration.html#producerconfigs
287
+ https://kafka.apache.org/0100/documentation/#producerconfigs
287
288
  """
288
289
  DEFAULT_CONFIG = {
289
290
  'bootstrap_servers': 'localhost',
@@ -332,7 +333,8 @@ class KafkaProducer(object):
332
333
  'sasl_plain_password': None,
333
334
  'sasl_kerberos_service_name': 'kafka',
334
335
  'sasl_kerberos_domain_name': None,
335
- 'sasl_oauth_token_provider': None
336
+ 'sasl_oauth_token_provider': None,
337
+ 'kafka_client': KafkaClient,
336
338
  }
337
339
 
338
340
  _COMPRESSORS = {
@@ -378,9 +380,10 @@ class KafkaProducer(object):
378
380
  reporters = [reporter() for reporter in self.config['metric_reporters']]
379
381
  self._metrics = Metrics(metric_config, reporters)
380
382
 
381
- client = KafkaClient(metrics=self._metrics, metric_group_prefix='producer',
382
- wakeup_timeout_ms=self.config['max_block_ms'],
383
- **self.config)
383
+ client = self.config['kafka_client'](
384
+ metrics=self._metrics, metric_group_prefix='producer',
385
+ wakeup_timeout_ms=self.config['max_block_ms'],
386
+ **self.config)
384
387
 
385
388
  # Get auto-discovered version from client if necessary
386
389
  if self.config['api_version'] is None:
@@ -43,4 +43,7 @@ API_KEYS = {
43
43
  40: 'ExpireDelegationToken',
44
44
  41: 'DescribeDelegationToken',
45
45
  42: 'DeleteGroups',
46
+ 45: 'AlterPartitionReassignments',
47
+ 46: 'ListPartitionReassignments',
48
+ 48: 'DescribeClientQuotas',
46
49
  }
kafka/protocol/admin.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import absolute_import
2
2
 
3
3
  from kafka.protocol.api import Request, Response
4
- from kafka.protocol.types import Array, Boolean, Bytes, Int8, Int16, Int32, Int64, Schema, String
4
+ from kafka.protocol.types import Array, Boolean, Bytes, Int8, Int16, Int32, Int64, Schema, String, Float64, CompactString, CompactArray, TaggedFields
5
5
 
6
6
 
7
7
  class ApiVersionResponse_v0(Response):
@@ -719,7 +719,7 @@ class DescribeConfigsResponse_v1(Response):
719
719
  ('config_names', String('utf-8')),
720
720
  ('config_value', String('utf-8')),
721
721
  ('read_only', Boolean),
722
- ('is_default', Boolean),
722
+ ('config_source', Int8),
723
723
  ('is_sensitive', Boolean),
724
724
  ('config_synonyms', Array(
725
725
  ('config_name', String('utf-8')),
@@ -790,6 +790,48 @@ DescribeConfigsResponse = [
790
790
  ]
791
791
 
792
792
 
793
+ class DescribeLogDirsResponse_v0(Response):
794
+ API_KEY = 35
795
+ API_VERSION = 0
796
+ FLEXIBLE_VERSION = True
797
+ SCHEMA = Schema(
798
+ ('throttle_time_ms', Int32),
799
+ ('log_dirs', Array(
800
+ ('error_code', Int16),
801
+ ('log_dir', String('utf-8')),
802
+ ('topics', Array(
803
+ ('name', String('utf-8')),
804
+ ('partitions', Array(
805
+ ('partition_index', Int32),
806
+ ('partition_size', Int64),
807
+ ('offset_lag', Int64),
808
+ ('is_future_key', Boolean)
809
+ ))
810
+ ))
811
+ ))
812
+ )
813
+
814
+
815
+ class DescribeLogDirsRequest_v0(Request):
816
+ API_KEY = 35
817
+ API_VERSION = 0
818
+ RESPONSE_TYPE = DescribeLogDirsResponse_v0
819
+ SCHEMA = Schema(
820
+ ('topics', Array(
821
+ ('topic', String('utf-8')),
822
+ ('partitions', Int32)
823
+ ))
824
+ )
825
+
826
+
827
+ DescribeLogDirsResponse = [
828
+ DescribeLogDirsResponse_v0,
829
+ ]
830
+ DescribeLogDirsRequest = [
831
+ DescribeLogDirsRequest_v0,
832
+ ]
833
+
834
+
793
835
  class SaslAuthenticateResponse_v0(Response):
794
836
  API_KEY = 36
795
837
  API_VERSION = 0
@@ -923,3 +965,132 @@ DeleteGroupsRequest = [
923
965
  DeleteGroupsResponse = [
924
966
  DeleteGroupsResponse_v0, DeleteGroupsResponse_v1
925
967
  ]
968
+
969
+
970
+ class DescribeClientQuotasResponse_v0(Response):
971
+ API_KEY = 48
972
+ API_VERSION = 0
973
+ SCHEMA = Schema(
974
+ ('throttle_time_ms', Int32),
975
+ ('error_code', Int16),
976
+ ('error_message', String('utf-8')),
977
+ ('entries', Array(
978
+ ('entity', Array(
979
+ ('entity_type', String('utf-8')),
980
+ ('entity_name', String('utf-8')))),
981
+ ('values', Array(
982
+ ('name', String('utf-8')),
983
+ ('value', Float64))))),
984
+ )
985
+
986
+
987
+ class DescribeClientQuotasRequest_v0(Request):
988
+ API_KEY = 48
989
+ API_VERSION = 0
990
+ RESPONSE_TYPE = DescribeClientQuotasResponse_v0
991
+ SCHEMA = Schema(
992
+ ('components', Array(
993
+ ('entity_type', String('utf-8')),
994
+ ('match_type', Int8),
995
+ ('match', String('utf-8')),
996
+ )),
997
+ ('strict', Boolean)
998
+ )
999
+
1000
+
1001
+ DescribeClientQuotasRequest = [
1002
+ DescribeClientQuotasRequest_v0,
1003
+ ]
1004
+
1005
+ DescribeClientQuotasResponse = [
1006
+ DescribeClientQuotasResponse_v0,
1007
+ ]
1008
+
1009
+
1010
+ class AlterPartitionReassignmentsResponse_v0(Response):
1011
+ API_KEY = 45
1012
+ API_VERSION = 0
1013
+ SCHEMA = Schema(
1014
+ ("throttle_time_ms", Int32),
1015
+ ("error_code", Int16),
1016
+ ("error_message", CompactString("utf-8")),
1017
+ ("responses", CompactArray(
1018
+ ("name", CompactString("utf-8")),
1019
+ ("partitions", CompactArray(
1020
+ ("partition_index", Int32),
1021
+ ("error_code", Int16),
1022
+ ("error_message", CompactString("utf-8")),
1023
+ ("tags", TaggedFields)
1024
+ )),
1025
+ ("tags", TaggedFields)
1026
+ )),
1027
+ ("tags", TaggedFields)
1028
+ )
1029
+
1030
+
1031
+ class AlterPartitionReassignmentsRequest_v0(Request):
1032
+ FLEXIBLE_VERSION = True
1033
+ API_KEY = 45
1034
+ API_VERSION = 0
1035
+ RESPONSE_TYPE = AlterPartitionReassignmentsResponse_v0
1036
+ SCHEMA = Schema(
1037
+ ("timeout_ms", Int32),
1038
+ ("topics", CompactArray(
1039
+ ("name", CompactString("utf-8")),
1040
+ ("partitions", CompactArray(
1041
+ ("partition_index", Int32),
1042
+ ("replicas", CompactArray(Int32)),
1043
+ ("tags", TaggedFields)
1044
+ )),
1045
+ ("tags", TaggedFields)
1046
+ )),
1047
+ ("tags", TaggedFields)
1048
+ )
1049
+
1050
+
1051
+ AlterPartitionReassignmentsRequest = [AlterPartitionReassignmentsRequest_v0]
1052
+
1053
+ AlterPartitionReassignmentsResponse = [AlterPartitionReassignmentsResponse_v0]
1054
+
1055
+
1056
+ class ListPartitionReassignmentsResponse_v0(Response):
1057
+ API_KEY = 46
1058
+ API_VERSION = 0
1059
+ SCHEMA = Schema(
1060
+ ("throttle_time_ms", Int32),
1061
+ ("error_code", Int16),
1062
+ ("error_message", CompactString("utf-8")),
1063
+ ("topics", CompactArray(
1064
+ ("name", CompactString("utf-8")),
1065
+ ("partitions", CompactArray(
1066
+ ("partition_index", Int32),
1067
+ ("replicas", CompactArray(Int32)),
1068
+ ("adding_replicas", CompactArray(Int32)),
1069
+ ("removing_replicas", CompactArray(Int32)),
1070
+ ("tags", TaggedFields)
1071
+ )),
1072
+ ("tags", TaggedFields)
1073
+ )),
1074
+ ("tags", TaggedFields)
1075
+ )
1076
+
1077
+
1078
+ class ListPartitionReassignmentsRequest_v0(Request):
1079
+ FLEXIBLE_VERSION = True
1080
+ API_KEY = 46
1081
+ API_VERSION = 0
1082
+ RESPONSE_TYPE = ListPartitionReassignmentsResponse_v0
1083
+ SCHEMA = Schema(
1084
+ ("timeout_ms", Int32),
1085
+ ("topics", CompactArray(
1086
+ ("name", CompactString("utf-8")),
1087
+ ("partition_index", CompactArray(Int32)),
1088
+ ("tags", TaggedFields)
1089
+ )),
1090
+ ("tags", TaggedFields)
1091
+ )
1092
+
1093
+
1094
+ ListPartitionReassignmentsRequest = [ListPartitionReassignmentsRequest_v0]
1095
+
1096
+ ListPartitionReassignmentsResponse = [ListPartitionReassignmentsResponse_v0]
kafka/protocol/api.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import absolute_import
3
3
  import abc
4
4
 
5
5
  from kafka.protocol.struct import Struct
6
- from kafka.protocol.types import Int16, Int32, String, Schema, Array
6
+ from kafka.protocol.types import Int16, Int32, String, Schema, Array, TaggedFields
7
7
 
8
8
 
9
9
  class RequestHeader(Struct):
@@ -20,9 +20,40 @@ class RequestHeader(Struct):
20
20
  )
21
21
 
22
22
 
23
+ class RequestHeaderV2(Struct):
24
+ # Flexible response / request headers end in field buffer
25
+ SCHEMA = Schema(
26
+ ('api_key', Int16),
27
+ ('api_version', Int16),
28
+ ('correlation_id', Int32),
29
+ ('client_id', String('utf-8')),
30
+ ('tags', TaggedFields),
31
+ )
32
+
33
+ def __init__(self, request, correlation_id=0, client_id='kafka-python', tags=None):
34
+ super(RequestHeaderV2, self).__init__(
35
+ request.API_KEY, request.API_VERSION, correlation_id, client_id, tags or {}
36
+ )
37
+
38
+
39
+ class ResponseHeader(Struct):
40
+ SCHEMA = Schema(
41
+ ('correlation_id', Int32),
42
+ )
43
+
44
+
45
+ class ResponseHeaderV2(Struct):
46
+ SCHEMA = Schema(
47
+ ('correlation_id', Int32),
48
+ ('tags', TaggedFields),
49
+ )
50
+
51
+
23
52
  class Request(Struct):
24
53
  __metaclass__ = abc.ABCMeta
25
54
 
55
+ FLEXIBLE_VERSION = False
56
+
26
57
  @abc.abstractproperty
27
58
  def API_KEY(self):
28
59
  """Integer identifier for api request"""
@@ -50,6 +81,16 @@ class Request(Struct):
50
81
  def to_object(self):
51
82
  return _to_object(self.SCHEMA, self)
52
83
 
84
+ def build_request_header(self, correlation_id, client_id):
85
+ if self.FLEXIBLE_VERSION:
86
+ return RequestHeaderV2(self, correlation_id=correlation_id, client_id=client_id)
87
+ return RequestHeader(self, correlation_id=correlation_id, client_id=client_id)
88
+
89
+ def parse_response_header(self, read_buffer):
90
+ if self.FLEXIBLE_VERSION:
91
+ return ResponseHeaderV2.decode(read_buffer)
92
+ return ResponseHeader.decode(read_buffer)
93
+
53
94
 
54
95
  class Response(Struct):
55
96
  __metaclass__ = abc.ABCMeta
kafka/protocol/parser.py CHANGED
@@ -4,10 +4,9 @@ import collections
4
4
  import logging
5
5
 
6
6
  import kafka.errors as Errors
7
- from kafka.protocol.api import RequestHeader
8
7
  from kafka.protocol.commit import GroupCoordinatorResponse
9
8
  from kafka.protocol.frame import KafkaBytes
10
- from kafka.protocol.types import Int32
9
+ from kafka.protocol.types import Int32, TaggedFields
11
10
  from kafka.version import __version__
12
11
 
13
12
  log = logging.getLogger(__name__)
@@ -59,9 +58,8 @@ class KafkaProtocol(object):
59
58
  log.debug('Sending request %s', request)
60
59
  if correlation_id is None:
61
60
  correlation_id = self._next_correlation_id()
62
- header = RequestHeader(request,
63
- correlation_id=correlation_id,
64
- client_id=self._client_id)
61
+
62
+ header = request.build_request_header(correlation_id=correlation_id, client_id=self._client_id)
65
63
  message = b''.join([header.encode(), request.encode()])
66
64
  size = Int32.encode(len(message))
67
65
  data = size + message
@@ -135,17 +133,12 @@ class KafkaProtocol(object):
135
133
  return responses
136
134
 
137
135
  def _process_response(self, read_buffer):
138
- recv_correlation_id = Int32.decode(read_buffer)
139
- log.debug('Received correlation id: %d', recv_correlation_id)
140
-
141
136
  if not self.in_flight_requests:
142
- raise Errors.CorrelationIdError(
143
- 'No in-flight-request found for server response'
144
- ' with correlation ID %d'
145
- % (recv_correlation_id,))
146
-
137
+ raise Errors.CorrelationIdError('No in-flight-request found for server response')
147
138
  (correlation_id, request) = self.in_flight_requests.popleft()
148
-
139
+ response_header = request.parse_response_header(read_buffer)
140
+ recv_correlation_id = response_header.correlation_id
141
+ log.debug('Received correlation id: %d', recv_correlation_id)
149
142
  # 0.8.2 quirk
150
143
  if (recv_correlation_id == 0 and
151
144
  correlation_id != 0 and