airbyte-source-shopify 4.2.2__tar.gz → 4.2.4__tar.gz
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_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/PKG-INFO +1 -1
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/pyproject.toml +1 -1
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/job.py +26 -6
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/query.py +1 -1
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/base_streams.py +4 -4
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/streams.py +30 -7
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/utils.py +11 -8
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/.airbyte-pypi-readme.md +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/__init__.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/auth.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/config_migrations.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/http_request.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/run.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/abandoned_checkouts.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/articles.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/balance_transactions.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/blogs.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collection_products.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collections.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collects.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/countries.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/custom_collections.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customer_address.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customer_journey_summary.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customers.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/deleted_products.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/discount_codes.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/discount_codes_sync.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/disputes.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/draft_orders.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/fulfillment_orders.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/fulfillments.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/inventory_items.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/inventory_levels.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/locations.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/market_countries.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_articles.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_blogs.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_collections.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_customers.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_draft_orders.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_locations.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_orders.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_pages.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_product_images.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_product_variants.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_products.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_shops.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_smart_collections.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_agreements.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_refunds.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_risks.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/orders.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/pages.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/price_rules.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/product_images.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/product_variants.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/products.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/profile_location_groups.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/shop.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/smart_collections.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/tender_transactions.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/transactions.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/scopes.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/__init__.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/exceptions.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/external_sort.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/record.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/retry.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/status.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/tools.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/source.py +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/spec.json +0 -0
- {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/transform.py +0 -0
|
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
|
|
|
3
3
|
build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
|
-
version = "4.2.
|
|
6
|
+
version = "4.2.4"
|
|
7
7
|
name = "airbyte-source-shopify"
|
|
8
8
|
description = "Source CDK implementation for Shopify."
|
|
9
9
|
authors = [ "Airbyte <contact@airbyte.io>",]
|
|
@@ -600,6 +600,20 @@ class ShopifyBulkManager:
|
|
|
600
600
|
step = self._job_size if self._job_size else self._job_size_min
|
|
601
601
|
return slice_start.add(days=step)
|
|
602
602
|
|
|
603
|
+
def _checkpoint_cursor_within_slice(
|
|
604
|
+
self,
|
|
605
|
+
slice_start: datetime,
|
|
606
|
+
checkpointed_cursor: Optional[str | int],
|
|
607
|
+
filter_checkpointed_cursor: Optional[str],
|
|
608
|
+
) -> bool:
|
|
609
|
+
if not checkpointed_cursor:
|
|
610
|
+
return False
|
|
611
|
+
if isinstance(checkpointed_cursor, str):
|
|
612
|
+
return pdm.parse(checkpointed_cursor) > slice_start
|
|
613
|
+
if isinstance(checkpointed_cursor, int):
|
|
614
|
+
return bool(filter_checkpointed_cursor) and pdm.parse(filter_checkpointed_cursor) > slice_start
|
|
615
|
+
return False
|
|
616
|
+
|
|
603
617
|
def _adjust_slice_end(
|
|
604
618
|
self, slice_end: datetime, checkpointed_cursor: Optional[str] = None, filter_checkpointed_cursor: Optional[str] = None
|
|
605
619
|
) -> datetime:
|
|
@@ -645,12 +659,18 @@ class ShopifyBulkManager:
|
|
|
645
659
|
checkpoint_from_self_cancel = self._job_checkpoint_from_self_cancel
|
|
646
660
|
# set the checkpointing to default, before the next slice is emitted, to avoid inf.loop
|
|
647
661
|
self._reset_checkpointing()
|
|
648
|
-
if checkpoint_from_failed_job and not
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
662
|
+
if checkpoint_from_failed_job and not self._checkpoint_cursor_within_slice(
|
|
663
|
+
slice_start, checkpointed_cursor, filter_checkpointed_cursor
|
|
664
|
+
):
|
|
665
|
+
# the partial result has no records within the current slice to resume from, the rest of the slice must not be skipped
|
|
666
|
+
self._raise_job_without_result(
|
|
667
|
+
"exited with FAILED and its partial result has no records within the slice to checkpoint from"
|
|
668
|
+
)
|
|
669
|
+
if checkpoint_from_self_cancel and not self._checkpoint_cursor_within_slice(
|
|
670
|
+
slice_start, checkpointed_cursor, filter_checkpointed_cursor
|
|
671
|
+
):
|
|
672
|
+
# the result has no records within the current slice to resume from, the slice is re-run with the checkpointing disabled
|
|
673
|
+
self._disable_checkpointing("but its result has no records within the slice to checkpoint from")
|
|
654
674
|
return slice_start
|
|
655
675
|
return self._adjust_slice_end(slice_end, checkpointed_cursor, filter_checkpointed_cursor)
|
|
656
676
|
|
|
@@ -139,7 +139,7 @@ class ShopifyBulkQuery:
|
|
|
139
139
|
def inject_parent_cursor_field(self, nodes: List[Field], key: str = "updatedAt", index: int = 2) -> List[Field]:
|
|
140
140
|
if self.parent_stream_cursor_alias:
|
|
141
141
|
# inject parent cursor key as alias to the `updatedAt` parent cursor field
|
|
142
|
-
nodes
|
|
142
|
+
return [*nodes[:index], Field(name=key, alias=self.parent_stream_cursor_alias), *nodes[index:]]
|
|
143
143
|
return nodes
|
|
144
144
|
|
|
145
145
|
def get(self, filter_field: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None) -> str:
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/base_streams.py
RENAMED
|
@@ -18,7 +18,7 @@ from source_shopify.shopify_graphql.bulk.external_sort import DEFAULT_SORT_CHUNK
|
|
|
18
18
|
from source_shopify.shopify_graphql.bulk.job import ShopifyBulkManager
|
|
19
19
|
from source_shopify.shopify_graphql.bulk.query import DeliveryZoneList, ShopFeatures, ShopifyBulkQuery
|
|
20
20
|
from source_shopify.transform import DataTypeEnforcer
|
|
21
|
-
from source_shopify.utils import ApiTypeEnum, ShopifyGraphQlErrorHandler, ShopifyNonRetryableErrors,
|
|
21
|
+
from source_shopify.utils import ApiTypeEnum, ShopifyGraphQlErrorHandler, ShopifyNonRetryableErrors, is_retryable_graphql_error
|
|
22
22
|
from source_shopify.utils import EagerlyCachedStreamState as stream_state_cache
|
|
23
23
|
from source_shopify.utils import ShopifyRateLimiter as limiter
|
|
24
24
|
|
|
@@ -35,7 +35,7 @@ class ShopifyStream(HttpStream, ABC):
|
|
|
35
35
|
logger = logging.getLogger("airbyte")
|
|
36
36
|
|
|
37
37
|
# Latest Stable Release
|
|
38
|
-
api_version = "2026-
|
|
38
|
+
api_version = "2026-10"
|
|
39
39
|
# Page size
|
|
40
40
|
limit = 250
|
|
41
41
|
|
|
@@ -948,7 +948,7 @@ class FullRefreshShopifyGraphQlBulkStream(ShopifyStream):
|
|
|
948
948
|
response_field: str
|
|
949
949
|
|
|
950
950
|
def get_error_handler(self) -> Optional[ErrorHandler]:
|
|
951
|
-
# retries HTTP 200 pages that carry a
|
|
951
|
+
# retries HTTP 200 pages that carry a transient GraphQL error, everything else follows the status-code mapping
|
|
952
952
|
return ShopifyGraphQlErrorHandler(
|
|
953
953
|
self.logger, max_retries=5, error_mapping=DEFAULT_ERROR_MAPPING | ShopifyNonRetryableErrors(self.name)
|
|
954
954
|
)
|
|
@@ -1005,6 +1005,6 @@ class FullRefreshShopifyGraphQlBulkStream(ShopifyStream):
|
|
|
1005
1005
|
raise AirbyteTracedException(
|
|
1006
1006
|
message=f"Stream `{self.name}`: Shopify GraphQL request failed: "
|
|
1007
1007
|
+ "; ".join(str(error.get("message", error)) for error in errors),
|
|
1008
|
-
failure_type=FailureType.transient_error if
|
|
1008
|
+
failure_type=FailureType.transient_error if is_retryable_graphql_error(errors) else FailureType.system_error,
|
|
1009
1009
|
)
|
|
1010
1010
|
yield from ((json_response.get("data") or {}).get(self.response_field) or {}).get("nodes") or []
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/streams.py
RENAMED
|
@@ -36,7 +36,13 @@ from source_shopify.shopify_graphql.bulk.query import (
|
|
|
36
36
|
Transaction,
|
|
37
37
|
)
|
|
38
38
|
from source_shopify.shopify_graphql.bulk.tools import BulkTools
|
|
39
|
-
from source_shopify.utils import
|
|
39
|
+
from source_shopify.utils import (
|
|
40
|
+
LimitReducingErrorHandler,
|
|
41
|
+
ShopifyGraphQlErrorHandler,
|
|
42
|
+
ShopifyNonRetryableErrors,
|
|
43
|
+
ShopifyRateLimiter,
|
|
44
|
+
is_retryable_graphql_error,
|
|
45
|
+
)
|
|
40
46
|
|
|
41
47
|
from airbyte_cdk import HttpSubStream
|
|
42
48
|
from airbyte_cdk.models import FailureType
|
|
@@ -247,16 +253,29 @@ class DeletedProducts(IncrementalShopifyStream):
|
|
|
247
253
|
"variables": variables,
|
|
248
254
|
}
|
|
249
255
|
|
|
256
|
+
def get_error_handler(self) -> Optional[ErrorHandler]:
|
|
257
|
+
return ShopifyGraphQlErrorHandler(
|
|
258
|
+
self.logger, max_retries=5, error_mapping=DEFAULT_ERROR_MAPPING | ShopifyNonRetryableErrors(self.name)
|
|
259
|
+
)
|
|
260
|
+
|
|
250
261
|
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
|
|
251
262
|
json_response = response.json()
|
|
252
|
-
page_info = json_response.get("data"
|
|
263
|
+
page_info = ((json_response.get("data") or {}).get("events") or {}).get("pageInfo", {})
|
|
253
264
|
if page_info.get("hasNextPage"):
|
|
254
265
|
return {"cursor": page_info.get("endCursor")}
|
|
255
266
|
return None
|
|
256
267
|
|
|
257
268
|
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
|
258
269
|
json_response = response.json()
|
|
259
|
-
|
|
270
|
+
errors = json_response.get("errors")
|
|
271
|
+
if errors and response.ok:
|
|
272
|
+
# an HTTP 200 with GraphQL errors has no usable data, reading it as an empty page would end the sync as complete
|
|
273
|
+
raise AirbyteTracedException(
|
|
274
|
+
message=f"Stream `{self.name}`: Shopify GraphQL request failed: "
|
|
275
|
+
+ "; ".join(str(error.get("message", error)) for error in errors),
|
|
276
|
+
failure_type=FailureType.transient_error if is_retryable_graphql_error(errors) else FailureType.system_error,
|
|
277
|
+
)
|
|
278
|
+
events = ((json_response.get("data") or {}).get("events") or {}).get("nodes", [])
|
|
260
279
|
for event in events:
|
|
261
280
|
if event.get("subjectType") == "PRODUCT" and event.get("subjectId"):
|
|
262
281
|
subject_id = event.get("subjectId", "")
|
|
@@ -607,18 +626,18 @@ class DiscountCodesSync(IncrementalShopifyStream):
|
|
|
607
626
|
result = response.json()
|
|
608
627
|
errors = result.get("errors")
|
|
609
628
|
if errors:
|
|
610
|
-
if
|
|
629
|
+
if is_retryable_graphql_error(errors):
|
|
611
630
|
if attempt < self._GRAPHQL_MAX_RETRIES:
|
|
612
631
|
ShopifyRateLimiter.wait_time(ShopifyRateLimiter.on_unknown_load)
|
|
613
632
|
continue
|
|
614
633
|
raise AirbyteTracedException(
|
|
615
|
-
message="GraphQL query for stream `discount_codes_sync` exceeded max retries
|
|
634
|
+
message="GraphQL query for stream `discount_codes_sync` exceeded max retries on transient errors."
|
|
616
635
|
)
|
|
617
636
|
error_messages = "; ".join(e.get("message", str(e)) for e in errors)
|
|
618
637
|
raise AirbyteTracedException(message=f"GraphQL query failed for stream `discount_codes_sync`: {error_messages}")
|
|
619
638
|
ShopifyRateLimiter.wait_time(ShopifyRateLimiter.get_graphql_api_wait_time(response, threshold=0.9))
|
|
620
639
|
return result
|
|
621
|
-
raise AirbyteTracedException(message="GraphQL query for stream `discount_codes_sync` exceeded max retries
|
|
640
|
+
raise AirbyteTracedException(message="GraphQL query for stream `discount_codes_sync` exceeded max retries on transient errors.")
|
|
622
641
|
|
|
623
642
|
@staticmethod
|
|
624
643
|
def _extract_codes_connection(code_discount: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
@@ -663,7 +682,11 @@ class DiscountCodesSync(IncrementalShopifyStream):
|
|
|
663
682
|
if child_cursor:
|
|
664
683
|
variables["after"] = child_cursor
|
|
665
684
|
result = self._graphql_request(self.CHILD_CODES_QUERY, variables)
|
|
666
|
-
|
|
685
|
+
code_discount_node = (result.get("data") or {}).get("codeDiscountNode")
|
|
686
|
+
if not code_discount_node:
|
|
687
|
+
self.logger.warning(f"Stream `{self.name}`: discount `{parent_gid}` no longer exists, skipping its codes.")
|
|
688
|
+
return
|
|
689
|
+
code_discount = code_discount_node.get("codeDiscount") or {}
|
|
667
690
|
codes_conn = self._extract_codes_connection(code_discount)
|
|
668
691
|
for code_node in codes_conn.get("nodes", []):
|
|
669
692
|
record = self._build_child_record(code_node, parent_gid, parent_meta)
|
|
@@ -99,11 +99,13 @@ class ApiTypeEnum(enum.Enum):
|
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
GRAPHQL_THROTTLE_ERROR_CODES: Final[set] = {"THROTTLED", "MAX_COST_EXCEEDED"}
|
|
102
|
+
# Shopify returns INTERNAL_SERVER_ERROR with HTTP 200 instead of 500 and documents it as retryable
|
|
103
|
+
GRAPHQL_RETRYABLE_ERROR_CODES: Final[set] = GRAPHQL_THROTTLE_ERROR_CODES | {"INTERNAL_SERVER_ERROR"}
|
|
102
104
|
|
|
103
105
|
|
|
104
|
-
def
|
|
105
|
-
"""Shopify GraphQL reports
|
|
106
|
-
return any((error.get("extensions") or {}).get("code") in
|
|
106
|
+
def is_retryable_graphql_error(errors: List[Mapping[str, Any]]) -> bool:
|
|
107
|
+
"""Shopify GraphQL reports transient errors as HTTP 200 with `errors[].extensions.code` in GRAPHQL_RETRYABLE_ERROR_CODES."""
|
|
108
|
+
return any((error.get("extensions") or {}).get("code") in GRAPHQL_RETRYABLE_ERROR_CODES for error in errors)
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
class ShopifyRateLimiter:
|
|
@@ -373,9 +375,10 @@ class LimitReducingErrorHandler(HttpStatusErrorHandler):
|
|
|
373
375
|
|
|
374
376
|
class ShopifyGraphQlErrorHandler(HttpStatusErrorHandler):
|
|
375
377
|
"""
|
|
376
|
-
Shopify GraphQL reports throttling
|
|
377
|
-
(https://shopify.dev/docs/api/admin-graphql/2026-
|
|
378
|
-
treating the 200 as a success: the cost bucket refills at the shop's restore rate
|
|
378
|
+
Shopify GraphQL reports throttling (THROTTLED / MAX_COST_EXCEEDED) and most internal errors (INTERNAL_SERVER_ERROR)
|
|
379
|
+
as HTTP 200 with `errors[].extensions.code` (https://shopify.dev/docs/api/admin-graphql/2026-10#status-and-error-codes).
|
|
380
|
+
Retry such pages instead of treating the 200 as a success: the cost bucket refills at the shop's restore rate
|
|
381
|
+
within seconds, and an internal error is retried like an HTTP 500.
|
|
379
382
|
"""
|
|
380
383
|
|
|
381
384
|
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
|
|
@@ -384,10 +387,10 @@ class ShopifyGraphQlErrorHandler(HttpStatusErrorHandler):
|
|
|
384
387
|
body = response_or_exception.json()
|
|
385
388
|
except requests.exceptions.RequestException:
|
|
386
389
|
body = {}
|
|
387
|
-
if
|
|
390
|
+
if is_retryable_graphql_error(body.get("errors") or [] if isinstance(body, dict) else []):
|
|
388
391
|
return ErrorResolution(
|
|
389
392
|
response_action=ResponseAction.RETRY,
|
|
390
393
|
failure_type=FailureType.transient_error,
|
|
391
|
-
error_message="Shopify GraphQL request
|
|
394
|
+
error_message="Shopify GraphQL request hit a transient error, retrying.",
|
|
392
395
|
)
|
|
393
396
|
return super().interpret_response(response_or_exception)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/config_migrations.py
RENAMED
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/http_request.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/articles.json
RENAMED
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/blogs.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collects.json
RENAMED
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/countries.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customers.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/disputes.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/locations.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/orders.json
RENAMED
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/pages.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/products.json
RENAMED
|
File without changes
|
|
File without changes
|
{airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/shop.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|