airbyte-cdk 6.26.0.dev0__py3-none-any.whl → 6.26.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.
- airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py +5 -2
- airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py +35 -18
- airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py +1 -1
- airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py +46 -12
- airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py +13 -20
- {airbyte_cdk-6.26.0.dev0.dist-info → airbyte_cdk-6.26.0.dev2.dist-info}/METADATA +1 -1
- {airbyte_cdk-6.26.0.dev0.dist-info → airbyte_cdk-6.26.0.dev2.dist-info}/RECORD +10 -10
- {airbyte_cdk-6.26.0.dev0.dist-info → airbyte_cdk-6.26.0.dev2.dist-info}/LICENSE.txt +0 -0
- {airbyte_cdk-6.26.0.dev0.dist-info → airbyte_cdk-6.26.0.dev2.dist-info}/WHEEL +0 -0
- {airbyte_cdk-6.26.0.dev0.dist-info → airbyte_cdk-6.26.0.dev2.dist-info}/entry_points.txt +0 -0
@@ -264,7 +264,10 @@ class ConcurrentPerPartitionCursor(Cursor):
|
|
264
264
|
if not stream_state:
|
265
265
|
return
|
266
266
|
|
267
|
-
if
|
267
|
+
if (
|
268
|
+
self._PERPARTITION_STATE_KEY not in stream_state
|
269
|
+
and self._GLOBAL_STATE_KEY not in stream_state
|
270
|
+
):
|
268
271
|
# We assume that `stream_state` is in a global format that can be applied to all partitions.
|
269
272
|
# Example: {"global_state_format_key": "global_state_format_value"}
|
270
273
|
self._global_cursor = deepcopy(stream_state)
|
@@ -273,7 +276,7 @@ class ConcurrentPerPartitionCursor(Cursor):
|
|
273
276
|
else:
|
274
277
|
self._lookback_window = int(stream_state.get("lookback_window", 0))
|
275
278
|
|
276
|
-
for state in stream_state
|
279
|
+
for state in stream_state.get(self._PERPARTITION_STATE_KEY, []):
|
277
280
|
self._cursor_per_partition[self._to_partition_key(state["partition"])] = (
|
278
281
|
self._create_cursor(state["cursor"])
|
279
282
|
)
|
@@ -222,6 +222,8 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
222
222
|
next_page_token: Optional[Mapping[str, Any]] = None,
|
223
223
|
) -> Mapping[str, Any]:
|
224
224
|
if stream_slice:
|
225
|
+
if self._to_partition_key(stream_slice.partition) not in self._cursor_per_partition:
|
226
|
+
self._create_cursor_for_partition(self._to_partition_key(stream_slice.partition))
|
225
227
|
return self._partition_router.get_request_params( # type: ignore # this always returns a mapping
|
226
228
|
stream_state=stream_state,
|
227
229
|
stream_slice=StreamSlice(partition=stream_slice.partition, cursor_slice={}),
|
@@ -244,6 +246,8 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
244
246
|
next_page_token: Optional[Mapping[str, Any]] = None,
|
245
247
|
) -> Mapping[str, Any]:
|
246
248
|
if stream_slice:
|
249
|
+
if self._to_partition_key(stream_slice.partition) not in self._cursor_per_partition:
|
250
|
+
self._create_cursor_for_partition(self._to_partition_key(stream_slice.partition))
|
247
251
|
return self._partition_router.get_request_headers( # type: ignore # this always returns a mapping
|
248
252
|
stream_state=stream_state,
|
249
253
|
stream_slice=StreamSlice(partition=stream_slice.partition, cursor_slice={}),
|
@@ -266,6 +270,8 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
266
270
|
next_page_token: Optional[Mapping[str, Any]] = None,
|
267
271
|
) -> Union[Mapping[str, Any], str]:
|
268
272
|
if stream_slice:
|
273
|
+
if self._to_partition_key(stream_slice.partition) not in self._cursor_per_partition:
|
274
|
+
self._create_cursor_for_partition(self._to_partition_key(stream_slice.partition))
|
269
275
|
return self._partition_router.get_request_body_data( # type: ignore # this always returns a mapping
|
270
276
|
stream_state=stream_state,
|
271
277
|
stream_slice=StreamSlice(partition=stream_slice.partition, cursor_slice={}),
|
@@ -288,6 +294,8 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
288
294
|
next_page_token: Optional[Mapping[str, Any]] = None,
|
289
295
|
) -> Mapping[str, Any]:
|
290
296
|
if stream_slice:
|
297
|
+
if self._to_partition_key(stream_slice.partition) not in self._cursor_per_partition:
|
298
|
+
self._create_cursor_for_partition(self._to_partition_key(stream_slice.partition))
|
291
299
|
return self._partition_router.get_request_body_json( # type: ignore # this always returns a mapping
|
292
300
|
stream_state=stream_state,
|
293
301
|
stream_slice=StreamSlice(partition=stream_slice.partition, cursor_slice={}),
|
@@ -303,21 +311,6 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
303
311
|
raise ValueError("A partition needs to be provided in order to get request body json")
|
304
312
|
|
305
313
|
def should_be_synced(self, record: Record) -> bool:
|
306
|
-
if (
|
307
|
-
record.associated_slice
|
308
|
-
and self._to_partition_key(record.associated_slice.partition)
|
309
|
-
not in self._cursor_per_partition
|
310
|
-
):
|
311
|
-
partition_state = (
|
312
|
-
self._state_to_migrate_from
|
313
|
-
if self._state_to_migrate_from
|
314
|
-
else self._NO_CURSOR_STATE
|
315
|
-
)
|
316
|
-
cursor = self._create_cursor(partition_state)
|
317
|
-
|
318
|
-
self._cursor_per_partition[
|
319
|
-
self._to_partition_key(record.associated_slice.partition)
|
320
|
-
] = cursor
|
321
314
|
return self._get_cursor(record).should_be_synced(
|
322
315
|
self._convert_record_to_cursor_record(record)
|
323
316
|
)
|
@@ -356,8 +349,32 @@ class PerPartitionCursor(DeclarativeCursor):
|
|
356
349
|
)
|
357
350
|
partition_key = self._to_partition_key(record.associated_slice.partition)
|
358
351
|
if partition_key not in self._cursor_per_partition:
|
359
|
-
|
360
|
-
"Invalid state as stream slices that are emitted should refer to an existing cursor"
|
361
|
-
)
|
352
|
+
self._create_cursor_for_partition(partition_key)
|
362
353
|
cursor = self._cursor_per_partition[partition_key]
|
363
354
|
return cursor
|
355
|
+
|
356
|
+
def _create_cursor_for_partition(self, partition_key: str) -> None:
|
357
|
+
"""
|
358
|
+
Dynamically creates and initializes a cursor for the specified partition.
|
359
|
+
|
360
|
+
This method is required for `ConcurrentPerPartitionCursor`. For concurrent cursors,
|
361
|
+
stream_slices is executed only for the concurrent cursor, so cursors per partition
|
362
|
+
are not created for the declarative cursor. This method ensures that a cursor is available
|
363
|
+
to create requests for the specified partition. The cursor is initialized
|
364
|
+
with the per-partition state if present in the initial state, or with the global state
|
365
|
+
adjusted by the lookback window, or with the state to migrate from.
|
366
|
+
|
367
|
+
Note:
|
368
|
+
This is a temporary workaround and should be removed once the declarative cursor
|
369
|
+
is decoupled from the concurrent cursor implementation.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
partition_key (str): The unique identifier for the partition for which the cursor
|
373
|
+
needs to be created.
|
374
|
+
"""
|
375
|
+
partition_state = (
|
376
|
+
self._state_to_migrate_from if self._state_to_migrate_from else self._NO_CURSOR_STATE
|
377
|
+
)
|
378
|
+
cursor = self._create_cursor(partition_state)
|
379
|
+
|
380
|
+
self._cursor_per_partition[partition_key] = cursor
|
@@ -2407,7 +2407,7 @@ class ModelToComponentFactory:
|
|
2407
2407
|
if (
|
2408
2408
|
not isinstance(stream_slicer, DatetimeBasedCursor)
|
2409
2409
|
or type(stream_slicer) is not DatetimeBasedCursor
|
2410
|
-
)
|
2410
|
+
):
|
2411
2411
|
# Many of the custom component implementations of DatetimeBasedCursor override get_request_params() (or other methods).
|
2412
2412
|
# Because we're decoupling RequestOptionsProvider from the Cursor, custom components will eventually need to reimplement
|
2413
2413
|
# their own RequestOptionsProvider. However, right now the existing StreamSlicer/Cursor still can act as the SimpleRetriever's
|
@@ -295,24 +295,58 @@ class SubstreamPartitionRouter(PartitionRouter):
|
|
295
295
|
return
|
296
296
|
|
297
297
|
if not parent_state and incremental_dependency:
|
298
|
-
#
|
299
|
-
|
300
|
-
substream_state = substream_state[0] if substream_state else {} # type: ignore [assignment] # Incorrect type for assignment
|
301
|
-
parent_state = {}
|
302
|
-
|
303
|
-
# Copy child state to parent streams with incremental dependencies
|
304
|
-
if substream_state:
|
305
|
-
for parent_config in self.parent_stream_configs:
|
306
|
-
if parent_config.incremental_dependency:
|
307
|
-
parent_state[parent_config.stream.name] = {
|
308
|
-
parent_config.stream.cursor_field: substream_state
|
309
|
-
}
|
298
|
+
# Migrate child state to parent state format
|
299
|
+
parent_state = self._migrate_child_state_to_parent_state(stream_state)
|
310
300
|
|
311
301
|
# Set state for each parent stream with an incremental dependency
|
312
302
|
for parent_config in self.parent_stream_configs:
|
313
303
|
if parent_config.incremental_dependency:
|
314
304
|
parent_config.stream.state = parent_state.get(parent_config.stream.name, {})
|
315
305
|
|
306
|
+
def _migrate_child_state_to_parent_state(self, stream_state: StreamState) -> StreamState:
|
307
|
+
"""
|
308
|
+
Migrate the child stream state to the parent stream's state format.
|
309
|
+
|
310
|
+
This method converts the global or child state into a format compatible with parent
|
311
|
+
streams. The migration occurs only for parent streams with incremental dependencies.
|
312
|
+
The method filters out per-partition states and retains only the global state in the
|
313
|
+
format `{cursor_field: cursor_value}`.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
stream_state (StreamState): The state to migrate. Expected formats include:
|
317
|
+
- {"updated_at": "2023-05-27T00:00:00Z"}
|
318
|
+
- {"states": [...] } (ignored during migration)
|
319
|
+
|
320
|
+
Returns:
|
321
|
+
StreamState: A migrated state for parent streams in the format:
|
322
|
+
{
|
323
|
+
"parent_stream_name": {"parent_stream_cursor": "2023-05-27T00:00:00Z"}
|
324
|
+
}
|
325
|
+
|
326
|
+
Example:
|
327
|
+
Input: {"updated_at": "2023-05-27T00:00:00Z"}
|
328
|
+
Output: {
|
329
|
+
"parent_stream_name": {"parent_stream_cursor": "2023-05-27T00:00:00Z"}
|
330
|
+
}
|
331
|
+
"""
|
332
|
+
substream_state_values = list(stream_state.values())
|
333
|
+
substream_state = substream_state_values[0] if substream_state_values else {}
|
334
|
+
|
335
|
+
# Ignore per-partition states or invalid formats
|
336
|
+
if isinstance(substream_state, (list, dict)) or len(substream_state_values) != 1:
|
337
|
+
return {}
|
338
|
+
|
339
|
+
# Copy child state to parent streams with incremental dependencies
|
340
|
+
parent_state = {}
|
341
|
+
if substream_state:
|
342
|
+
for parent_config in self.parent_stream_configs:
|
343
|
+
if parent_config.incremental_dependency:
|
344
|
+
parent_state[parent_config.stream.name] = {
|
345
|
+
parent_config.stream.cursor_field: substream_state
|
346
|
+
}
|
347
|
+
|
348
|
+
return parent_state
|
349
|
+
|
316
350
|
def get_stream_state(self) -> Optional[Mapping[str, StreamState]]:
|
317
351
|
"""
|
318
352
|
Get the state of the parent streams.
|
@@ -154,8 +154,9 @@ class DynamicSchemaLoader(SchemaLoader):
|
|
154
154
|
transformed_properties = self._transform(properties, {})
|
155
155
|
|
156
156
|
return {
|
157
|
-
"$schema": "
|
157
|
+
"$schema": "https://json-schema.org/draft-07/schema#",
|
158
158
|
"type": "object",
|
159
|
+
"additionalProperties": True,
|
159
160
|
"properties": transformed_properties,
|
160
161
|
}
|
161
162
|
|
@@ -220,25 +221,17 @@ class DynamicSchemaLoader(SchemaLoader):
|
|
220
221
|
)
|
221
222
|
|
222
223
|
def _resolve_complex_type(self, complex_type: ComplexFieldType) -> Mapping[str, Any]:
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
else
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
items_type = current_type.items
|
235
|
-
else:
|
236
|
-
types.append(current_type.items)
|
237
|
-
continue # Skip the following lines until the stack is resolved
|
238
|
-
field_type["items"] = self._get_airbyte_type(items_type)
|
239
|
-
resolved_type = field_type
|
240
|
-
|
241
|
-
return resolved_type
|
224
|
+
if not complex_type.items:
|
225
|
+
return self._get_airbyte_type(complex_type.field_type)
|
226
|
+
|
227
|
+
field_type = self._get_airbyte_type(complex_type.field_type)
|
228
|
+
field_type["items"] = (
|
229
|
+
self._get_airbyte_type(complex_type.items)
|
230
|
+
if isinstance(complex_type.items, str)
|
231
|
+
else self._resolve_complex_type(complex_type.items)
|
232
|
+
)
|
233
|
+
|
234
|
+
return field_type
|
242
235
|
|
243
236
|
def _replace_type_if_not_valid(
|
244
237
|
self,
|
@@ -88,11 +88,11 @@ airbyte_cdk/sources/declarative/extractors/record_selector.py,sha256=tjNwcURmlyD
|
|
88
88
|
airbyte_cdk/sources/declarative/extractors/response_to_file_extractor.py,sha256=LhqGDfX06_dDYLKsIVnwQ_nAWCln-v8PV7Wgt_QVeTI,6533
|
89
89
|
airbyte_cdk/sources/declarative/extractors/type_transformer.py,sha256=d6Y2Rfg8pMVEEnHllfVksWZdNVOU55yk34O03dP9muY,1626
|
90
90
|
airbyte_cdk/sources/declarative/incremental/__init__.py,sha256=U1oZKtBaEC6IACmvziY9Wzg7Z8EgF4ZuR7NwvjlB_Sk,1255
|
91
|
-
airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=
|
91
|
+
airbyte_cdk/sources/declarative/incremental/concurrent_partition_cursor.py,sha256=0Ey_Rb8mf7BrhQTXLTMFRhe35-RoBw62OecKyGAyjSw,14592
|
92
92
|
airbyte_cdk/sources/declarative/incremental/datetime_based_cursor.py,sha256=_UzUnSIUsDbRgbFTXgSyZEFb4ws-KdhdQPWO8mFbV7U,22028
|
93
93
|
airbyte_cdk/sources/declarative/incremental/declarative_cursor.py,sha256=5Bhw9VRPyIuCaD0wmmq_L3DZsa-rJgtKSEUzSd8YYD0,536
|
94
94
|
airbyte_cdk/sources/declarative/incremental/global_substream_cursor.py,sha256=9HO-QbL9akvjq2NP7l498RwLA4iQZlBMQW1tZbt34I8,15943
|
95
|
-
airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py,sha256=
|
95
|
+
airbyte_cdk/sources/declarative/incremental/per_partition_cursor.py,sha256=9IAJTCiRUXvhFFz-IhZtYh_KfAjLHqthsYf2jErQRls,17728
|
96
96
|
airbyte_cdk/sources/declarative/incremental/per_partition_with_global.py,sha256=2YBOA2NnwAeIKlIhSwUB_W-FaGnPcmrG_liY7b4mV2Y,8365
|
97
97
|
airbyte_cdk/sources/declarative/incremental/resumable_full_refresh_cursor.py,sha256=10LFv1QPM-agVKl6eaANmEBOfd7gZgBrkoTcMggsieQ,4809
|
98
98
|
airbyte_cdk/sources/declarative/interpolation/__init__.py,sha256=tjUJkn3B-iZ-p7RP2c3dVZejrGiQeooGmS5ibWTuUL4,437
|
@@ -115,14 +115,14 @@ airbyte_cdk/sources/declarative/parsers/custom_code_compiler.py,sha256=958MMX6_Z
|
|
115
115
|
airbyte_cdk/sources/declarative/parsers/custom_exceptions.py,sha256=Rir9_z3Kcd5Es0-LChrzk-0qubAsiK_RSEnLmK2OXm8,553
|
116
116
|
airbyte_cdk/sources/declarative/parsers/manifest_component_transformer.py,sha256=CXwTfD3wSQq3okcqwigpprbHhSURUokh4GK2OmOyKC8,9132
|
117
117
|
airbyte_cdk/sources/declarative/parsers/manifest_reference_resolver.py,sha256=IWUOdF03o-aQn0Occo1BJCxU0Pz-QILk5L67nzw2thw,6803
|
118
|
-
airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=
|
118
|
+
airbyte_cdk/sources/declarative/parsers/model_to_component_factory.py,sha256=zfWJLlopJklDK1xvoUy2qMFcnSklmQ7wwEbdWVxYlw0,122917
|
119
119
|
airbyte_cdk/sources/declarative/partition_routers/__init__.py,sha256=HJ-Syp3p7RpyR_OK0X_a2kSyISfu3W-PKrRI16iY0a8,957
|
120
120
|
airbyte_cdk/sources/declarative/partition_routers/async_job_partition_router.py,sha256=n82J15S8bjeMZ5uROu--P3hnbQoxkY5v7RPHYx7g7ro,2929
|
121
121
|
airbyte_cdk/sources/declarative/partition_routers/cartesian_product_stream_slicer.py,sha256=c5cuVFM6NFkuQqG8Z5IwkBuwDrvXZN1CunUOM_L0ezg,6892
|
122
122
|
airbyte_cdk/sources/declarative/partition_routers/list_partition_router.py,sha256=t7pRdFWfFWJtQQG19c9PVeMODyO2BknRTakpM5U9N-8,4844
|
123
123
|
airbyte_cdk/sources/declarative/partition_routers/partition_router.py,sha256=YyEIzdmLd1FjbVP3QbQ2VFCLW_P-OGbVh6VpZShp54k,2218
|
124
124
|
airbyte_cdk/sources/declarative/partition_routers/single_partition_router.py,sha256=SKzKjSyfccq4dxGIh-J6ejrgkCHzaiTIazmbmeQiRD4,1942
|
125
|
-
airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py,sha256=
|
125
|
+
airbyte_cdk/sources/declarative/partition_routers/substream_partition_router.py,sha256=pEz-P6D5TGtP4isNfmtakgKD95PqMLo6fasCVLIguWk,16760
|
126
126
|
airbyte_cdk/sources/declarative/requesters/README.md,sha256=eL1I4iLkxaw7hJi9S9d18_XcRl-R8lUSjqBVJJzvXmg,2656
|
127
127
|
airbyte_cdk/sources/declarative/requesters/__init__.py,sha256=d7a3OoHbqaJDyyPli3nqqJ2yAW_SLX6XDaBAKOwvpxw,364
|
128
128
|
airbyte_cdk/sources/declarative/requesters/error_handlers/__init__.py,sha256=SkEDcJxlT1683rNx93K9whoS0OyUukkuOfToGtgpF58,776
|
@@ -170,7 +170,7 @@ airbyte_cdk/sources/declarative/retrievers/retriever.py,sha256=XPLs593Xv8c5cKMc3
|
|
170
170
|
airbyte_cdk/sources/declarative/retrievers/simple_retriever.py,sha256=kgnhVQxRlFqJs2-rDu2-QH-p-GzQU3nKmSp6_aq8u0s,24550
|
171
171
|
airbyte_cdk/sources/declarative/schema/__init__.py,sha256=xU45UvM5O4c1PSM13UHpCdh5hpW3HXy9vRRGEiAC1rg,795
|
172
172
|
airbyte_cdk/sources/declarative/schema/default_schema_loader.py,sha256=KTACrIE23a83wsm3Rd9Eb4K6-20lrGqYxTHNp9yxsso,1820
|
173
|
-
airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py,sha256=
|
173
|
+
airbyte_cdk/sources/declarative/schema/dynamic_schema_loader.py,sha256=d8tfDiDcJiunvN_Yalyfx5ISY5A-iIW3HbPwX2Hagh4,10702
|
174
174
|
airbyte_cdk/sources/declarative/schema/inline_schema_loader.py,sha256=bVETE10hRsatRJq3R3BeyRR0wIoK3gcP1gcpVRQ_P5U,464
|
175
175
|
airbyte_cdk/sources/declarative/schema/json_file_schema_loader.py,sha256=5Wl-fqW-pVf_dxJ4yGHMAFfC4JjKHYJhqFJT1xA57F4,4177
|
176
176
|
airbyte_cdk/sources/declarative/schema/schema_loader.py,sha256=kjt8v0N5wWKA5zyLnrDLxf1PJKdUqvQq2RVnAOAzNSY,379
|
@@ -350,8 +350,8 @@ airbyte_cdk/utils/slice_hasher.py,sha256=-pHexlNYoWYPnXNH-M7HEbjmeJe9Zk7SJijdQ7d
|
|
350
350
|
airbyte_cdk/utils/spec_schema_transformations.py,sha256=-5HTuNsnDBAhj-oLeQXwpTGA0HdcjFOf2zTEMUTTg_Y,816
|
351
351
|
airbyte_cdk/utils/stream_status_utils.py,sha256=ZmBoiy5HVbUEHAMrUONxZvxnvfV9CesmQJLDTAIWnWw,1171
|
352
352
|
airbyte_cdk/utils/traced_exception.py,sha256=C8uIBuCL_E4WnBAOPSxBicD06JAldoN9fGsQDp463OY,6292
|
353
|
-
airbyte_cdk-6.26.0.
|
354
|
-
airbyte_cdk-6.26.0.
|
355
|
-
airbyte_cdk-6.26.0.
|
356
|
-
airbyte_cdk-6.26.0.
|
357
|
-
airbyte_cdk-6.26.0.
|
353
|
+
airbyte_cdk-6.26.0.dev2.dist-info/LICENSE.txt,sha256=Wfe61S4BaGPj404v8lrAbvhjYR68SHlkzeYrg3_bbuM,1051
|
354
|
+
airbyte_cdk-6.26.0.dev2.dist-info/METADATA,sha256=AHdZ_4ifgRo92ehCzwq8zihpxL4Sg-1mPR-RYovPBkk,6001
|
355
|
+
airbyte_cdk-6.26.0.dev2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
356
|
+
airbyte_cdk-6.26.0.dev2.dist-info/entry_points.txt,sha256=fj-e3PAQvsxsQzyyq8UkG1k8spunWnD4BAH2AwlR6NM,95
|
357
|
+
airbyte_cdk-6.26.0.dev2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|