airbyte-cdk 6.34.0.dev1__py3-none-any.whl → 6.34.0.dev2__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.
@@ -95,6 +95,10 @@ class ConcurrentPerPartitionCursor(Cursor):
95
95
  # the oldest partitions can be efficiently removed, maintaining the most recent partitions.
96
96
  self._cursor_per_partition: OrderedDict[str, ConcurrentCursor] = OrderedDict()
97
97
  self._semaphore_per_partition: OrderedDict[str, threading.Semaphore] = OrderedDict()
98
+
99
+ # Parent-state tracking: store each partition’s parent state in creation order
100
+ self._partition_parent_state_map: OrderedDict[str, Mapping[str, Any]] = OrderedDict()
101
+
98
102
  self._finished_partitions: set[str] = set()
99
103
  self._lock = threading.Lock()
100
104
  self._timer = Timer()
@@ -155,7 +159,45 @@ class ConcurrentPerPartitionCursor(Cursor):
155
159
  and self._semaphore_per_partition[partition_key]._value == 0
156
160
  ):
157
161
  self._update_global_cursor(cursor.state[self.cursor_field.cursor_field_key])
158
- self._emit_state_message()
162
+
163
+ self._check_and_update_parent_state()
164
+
165
+ self._emit_state_message()
166
+
167
+ def _check_and_update_parent_state(self) -> None:
168
+ """
169
+ Pop the leftmost partition state from _partition_parent_state_map only if
170
+ *all partitions* up to (and including) that partition key in _semaphore_per_partition
171
+ are fully finished (i.e. in _finished_partitions and semaphore._value == 0).
172
+ """
173
+ last_closed_state = None
174
+
175
+ while self._partition_parent_state_map:
176
+ # Look at the earliest partition key in creation order
177
+ earliest_key = next(iter(self._partition_parent_state_map))
178
+
179
+ # Verify ALL partitions from the left up to earliest_key are finished
180
+ all_left_finished = True
181
+ for p_key, sem in self._semaphore_per_partition.items():
182
+ # If any earlier partition is still not finished, we must stop
183
+ if p_key not in self._finished_partitions or sem._value != 0:
184
+ all_left_finished = False
185
+ break
186
+ # Once we've reached earliest_key in the semaphore order, we can stop checking
187
+ if p_key == earliest_key:
188
+ break
189
+
190
+ # If the partitions up to earliest_key are not all finished, break the while-loop
191
+ if not all_left_finished:
192
+ break
193
+
194
+ # Otherwise, pop the leftmost entry from parent-state map
195
+ _, closed_parent_state = self._partition_parent_state_map.popitem(last=False)
196
+ last_closed_state = closed_parent_state
197
+
198
+ # Update _parent_state if we actually popped at least one partition
199
+ if last_closed_state is not None:
200
+ self._parent_state = last_closed_state
159
201
 
160
202
  def ensure_at_least_one_state_emitted(self) -> None:
161
203
  """
@@ -201,13 +243,19 @@ class ConcurrentPerPartitionCursor(Cursor):
201
243
 
202
244
  slices = self._partition_router.stream_slices()
203
245
  self._timer.start()
204
- for partition in slices:
205
- yield from self._generate_slices_from_partition(partition)
246
+ for partition, last, parent_state in iterate_with_last_flag_and_state(
247
+ slices, self._partition_router.get_stream_state
248
+ ):
249
+ yield from self._generate_slices_from_partition(partition, parent_state)
206
250
 
207
- def _generate_slices_from_partition(self, partition: StreamSlice) -> Iterable[StreamSlice]:
251
+ def _generate_slices_from_partition(
252
+ self, partition: StreamSlice, parent_state: Mapping[str, Any]
253
+ ) -> Iterable[StreamSlice]:
208
254
  # Ensure the maximum number of partitions is not exceeded
209
255
  self._ensure_partition_limit()
210
256
 
257
+ partition_key = self._to_partition_key(partition.partition)
258
+
211
259
  cursor = self._cursor_per_partition.get(self._to_partition_key(partition.partition))
212
260
  if not cursor:
213
261
  cursor = self._create_cursor(
@@ -216,18 +264,26 @@ class ConcurrentPerPartitionCursor(Cursor):
216
264
  )
217
265
  with self._lock:
218
266
  self._number_of_partitions += 1
219
- self._cursor_per_partition[self._to_partition_key(partition.partition)] = cursor
220
- self._semaphore_per_partition[self._to_partition_key(partition.partition)] = (
221
- threading.Semaphore(0)
222
- )
267
+ self._cursor_per_partition[partition_key] = cursor
268
+ self._semaphore_per_partition[partition_key] = threading.Semaphore(0)
269
+
270
+ with self._lock:
271
+ if (
272
+ len(self._partition_parent_state_map) == 0
273
+ or self._partition_parent_state_map[
274
+ next(reversed(self._partition_parent_state_map))
275
+ ]
276
+ != parent_state
277
+ ):
278
+ self._partition_parent_state_map[partition_key] = deepcopy(parent_state)
223
279
 
224
280
  for cursor_slice, is_last_slice, _ in iterate_with_last_flag_and_state(
225
281
  cursor.stream_slices(),
226
282
  lambda: None,
227
283
  ):
228
- self._semaphore_per_partition[self._to_partition_key(partition.partition)].release()
284
+ self._semaphore_per_partition[partition_key].release()
229
285
  if is_last_slice:
230
- self._finished_partitions.add(self._to_partition_key(partition.partition))
286
+ self._finished_partitions.add(partition_key)
231
287
  yield StreamSlice(
232
288
  partition=partition, cursor_slice=cursor_slice, extra_fields=partition.extra_fields
233
289
  )
@@ -338,9 +394,6 @@ class ConcurrentPerPartitionCursor(Cursor):
338
394
  self._cursor_per_partition[self._to_partition_key(state["partition"])] = (
339
395
  self._create_cursor(state["cursor"])
340
396
  )
341
- self._semaphore_per_partition[self._to_partition_key(state["partition"])] = (
342
- threading.Semaphore(0)
343
- )
344
397
 
345
398
  # set default state for missing partitions if it is per partition with fallback to global
346
399
  if self._GLOBAL_STATE_KEY in stream_state:
@@ -17,6 +17,7 @@ class SupportedHttpMethods(str, Enum):
17
17
  GET = "get"
18
18
  PATCH = "patch"
19
19
  POST = "post"
20
+ PUT = "put"
20
21
  DELETE = "delete"
21
22
 
22
23
 
@@ -77,7 +78,7 @@ class HttpMocker(contextlib.ContextDecorator):
77
78
  additional_matcher=self._matches_wrapper(matcher),
78
79
  response_list=[
79
80
  {
80
- "text": response.body,
81
+ self._get_body_field(response): response.body,
81
82
  "status_code": response.status_code,
82
83
  "headers": response.headers,
83
84
  }
@@ -85,6 +86,10 @@ class HttpMocker(contextlib.ContextDecorator):
85
86
  ],
86
87
  )
87
88
 
89
+ @staticmethod
90
+ def _get_body_field(response: HttpResponse) -> str:
91
+ return "text" if isinstance(response.body, str) else "content"
92
+
88
93
  def get(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None:
89
94
  self._mock_request_method(SupportedHttpMethods.GET, request, responses)
90
95
 
@@ -98,6 +103,9 @@ class HttpMocker(contextlib.ContextDecorator):
98
103
  ) -> None:
99
104
  self._mock_request_method(SupportedHttpMethods.POST, request, responses)
100
105
 
106
+ def put(self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]) -> None:
107
+ self._mock_request_method(SupportedHttpMethods.PUT, request, responses)
108
+
101
109
  def delete(
102
110
  self, request: HttpRequest, responses: Union[HttpResponse, List[HttpResponse]]
103
111
  ) -> None:
@@ -1,19 +1,22 @@
1
1
  # Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2
2
 
3
3
  from types import MappingProxyType
4
- from typing import Mapping
4
+ from typing import Mapping, Union
5
5
 
6
6
 
7
7
  class HttpResponse:
8
8
  def __init__(
9
- self, body: str, status_code: int = 200, headers: Mapping[str, str] = MappingProxyType({})
9
+ self,
10
+ body: Union[str, bytes],
11
+ status_code: int = 200,
12
+ headers: Mapping[str, str] = MappingProxyType({}),
10
13
  ):
11
14
  self._body = body
12
15
  self._status_code = status_code
13
16
  self._headers = headers
14
17
 
15
18
  @property
16
- def body(self) -> str:
19
+ def body(self) -> Union[str, bytes]:
17
20
  return self._body
18
21
 
19
22
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-cdk
3
- Version: 6.34.0.dev1
3
+ Version: 6.34.0.dev2
4
4
  Summary: A framework for writing Airbyte Connectors.
5
5
  Home-page: https://airbyte.com
6
6
  License: MIT
@@ -92,7 +92,7 @@ airbyte_cdk/sources/declarative/extractors/record_selector.py,sha256=HCqx7IyENM_
92
92
  airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py,sha256=LhqGDfX06_dDYLKsIVnwQ_nAWCln-v8PV7Wgt_QVeTI,6533
93
93
  airbyte_cdk/sources/declarative/extractors/type_transformer.py,sha256=d6Y2Rfg8pMVEEnHllfVksWZdNVOU55yk34O03dP9muY,1626
94
94
  airbyte_cdk/sources/declarative/incremental/__init__.py,sha256=U1oZKtBaEC6IACmvziY9Wzg7Z8EgF4ZuR7NwvjlB_Sk,1255
95
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=Pg2phEFT9T8AzUjK6hVhn0rgR3yY6JPF-Dfv0g1m5dQ,19191
95
+ airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=T4a-WMMFzPNyMpt-aNyw-eoW90hZNO3M2-Sy57jYpCw,21418
96
96
  airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py,sha256=Rbe6lJLTtZ5en33MwZiB9-H9-AwDMNHgwBZs8EqhYqk,22172
97
97
  airbyte_cdk/sources/declarative/incremental/declarative_cursor.py,sha256=5Bhw9VRPyIuCaD0wmmq_L3DZsa-rJgtKSEUzSd8YYD0,536
98
98
  airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py,sha256=9HO-QbL9akvjq2NP7l498RwLA4iQZlBMQW1tZbt34I8,15943
@@ -333,9 +333,9 @@ airbyte_cdk/test/catalog_builder.py,sha256=-y05Cz1x0Dlk6oE9LSKhCozssV2gYBNtMdV5Y
333
333
  airbyte_cdk/test/entrypoint_wrapper.py,sha256=9XBii_YguQp0d8cykn3hy102FsJcwIBQzSB7co5ho0s,9802
334
334
  airbyte_cdk/test/mock_http/__init__.py,sha256=jE5kC6CQ0OXkTqKhciDnNVZHesBFVIA2YvkdFGwva7k,322
335
335
  airbyte_cdk/test/mock_http/matcher.py,sha256=4Qj8UnJKZIs-eodshryce3SN1Ayc8GZpBETmP6hTEyc,1446
336
- airbyte_cdk/test/mock_http/mocker.py,sha256=HJjgFdapr7OALj0sfk-LVXYBiymbUDieaGa8U1_q730,7358
336
+ airbyte_cdk/test/mock_http/mocker.py,sha256=ghX44cLwhs7lqz1gYMizGX8zfPnDvt3YNI2w5jLpzIs,7726
337
337
  airbyte_cdk/test/mock_http/request.py,sha256=tdB8cqk2vLgCDTOKffBKsM06llYs4ZecgtH6DKyx6yY,4112
338
- airbyte_cdk/test/mock_http/response.py,sha256=U9KEsUkK2dPXYwnfwrwp6CcYSSpMYKLjfTrPFKSMCaM,602
338
+ airbyte_cdk/test/mock_http/response.py,sha256=s4-cQQqTtmeej0pQDWqmG0vUWpHS-93lIWMpW3zSVyU,662
339
339
  airbyte_cdk/test/mock_http/response_builder.py,sha256=debPx_lRYBaQVSwCoKLa0F8KFk3h0qG7bWxFBATa0cc,7958
340
340
  airbyte_cdk/test/state_builder.py,sha256=kLPql9lNzUJaBg5YYRLJlY_Hy5JLHJDVyKPMZMoYM44,946
341
341
  airbyte_cdk/test/utils/__init__.py,sha256=Hu-1XT2KDoYjDF7-_ziDwv5bY3PueGjANOCbzeOegDg,57
@@ -360,9 +360,9 @@ airbyte_cdk/utils/slice_hasher.py,sha256=EDxgROHDbfG-QKQb59m7h_7crN1tRiawdf5uU7G
360
360
  airbyte_cdk/utils/spec_schema_transformations.py,sha256=-5HTuNsnDBAhj-oLeQXwpTGA0HdcjFOf2zTEMUTTg_Y,816
361
361
  airbyte_cdk/utils/stream_status_utils.py,sha256=ZmBoiy5HVbUEHAMrUONxZvxnvfV9CesmQJLDTAIWnWw,1171
362
362
  airbyte_cdk/utils/traced_exception.py,sha256=C8uIBuCL_E4WnBAOPSxBicD06JAldoN9fGsQDp463OY,6292
363
- airbyte_cdk-6.34.0.dev1.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
364
- airbyte_cdk-6.34.0.dev1.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
365
- airbyte_cdk-6.34.0.dev1.dist-info/METADATA,sha256=zRWv4t7GvXHf9bPXmsf8vFuPd63eiYFXXGeMkUchcDw,6015
366
- airbyte_cdk-6.34.0.dev1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
367
- airbyte_cdk-6.34.0.dev1.dist-info/entry_points.txt,sha256=fj-e3PAQvsxsQzyyq8UkG1k8spunWnD4BAH2AwlR6NM,95
368
- airbyte_cdk-6.34.0.dev1.dist-info/RECORD,,
363
+ airbyte_cdk-6.34.0.dev2.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
364
+ airbyte_cdk-6.34.0.dev2.dist-info/LICENSE_SHORT,sha256=aqF6D1NcESmpn-cqsxBtszTEnHKnlsp8L4x9wAh3Nxg,55
365
+ airbyte_cdk-6.34.0.dev2.dist-info/METADATA,sha256=ocEC-CNtHU4hTlGp7U03ZuhKoyy4L2trucsaREiqru0,6015
366
+ airbyte_cdk-6.34.0.dev2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
367
+ airbyte_cdk-6.34.0.dev2.dist-info/entry_points.txt,sha256=fj-e3PAQvsxsQzyyq8UkG1k8spunWnD4BAH2AwlR6NM,95
368
+ airbyte_cdk-6.34.0.dev2.dist-info/RECORD,,