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.
Files changed (74) hide show
  1. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/PKG-INFO +1 -1
  2. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/pyproject.toml +1 -1
  3. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/job.py +26 -6
  4. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/query.py +1 -1
  5. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/base_streams.py +4 -4
  6. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/streams/streams.py +30 -7
  7. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/utils.py +11 -8
  8. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/.airbyte-pypi-readme.md +0 -0
  9. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/__init__.py +0 -0
  10. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/auth.py +0 -0
  11. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/config_migrations.py +0 -0
  12. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/http_request.py +0 -0
  13. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/run.py +0 -0
  14. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/abandoned_checkouts.json +0 -0
  15. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/articles.json +0 -0
  16. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/balance_transactions.json +0 -0
  17. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/blogs.json +0 -0
  18. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collection_products.json +0 -0
  19. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collections.json +0 -0
  20. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/collects.json +0 -0
  21. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/countries.json +0 -0
  22. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/custom_collections.json +0 -0
  23. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customer_address.json +0 -0
  24. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customer_journey_summary.json +0 -0
  25. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/customers.json +0 -0
  26. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/deleted_products.json +0 -0
  27. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/discount_codes.json +0 -0
  28. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/discount_codes_sync.json +0 -0
  29. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/disputes.json +0 -0
  30. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/draft_orders.json +0 -0
  31. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/fulfillment_orders.json +0 -0
  32. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/fulfillments.json +0 -0
  33. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/inventory_items.json +0 -0
  34. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/inventory_levels.json +0 -0
  35. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/locations.json +0 -0
  36. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/market_countries.json +0 -0
  37. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_articles.json +0 -0
  38. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_blogs.json +0 -0
  39. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_collections.json +0 -0
  40. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_customers.json +0 -0
  41. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_draft_orders.json +0 -0
  42. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_locations.json +0 -0
  43. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_orders.json +0 -0
  44. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_pages.json +0 -0
  45. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_product_images.json +0 -0
  46. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_product_variants.json +0 -0
  47. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_products.json +0 -0
  48. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_shops.json +0 -0
  49. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/metafield_smart_collections.json +0 -0
  50. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_agreements.json +0 -0
  51. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_refunds.json +0 -0
  52. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/order_risks.json +0 -0
  53. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/orders.json +0 -0
  54. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/pages.json +0 -0
  55. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/price_rules.json +0 -0
  56. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/product_images.json +0 -0
  57. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/product_variants.json +0 -0
  58. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/products.json +0 -0
  59. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/profile_location_groups.json +0 -0
  60. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/shop.json +0 -0
  61. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/smart_collections.json +0 -0
  62. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/tender_transactions.json +0 -0
  63. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/schemas/transactions.json +0 -0
  64. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/scopes.py +0 -0
  65. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/__init__.py +0 -0
  66. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/exceptions.py +0 -0
  67. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/external_sort.py +0 -0
  68. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/record.py +0 -0
  69. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/retry.py +0 -0
  70. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/status.py +0 -0
  71. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/shopify_graphql/bulk/tools.py +0 -0
  72. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/source.py +0 -0
  73. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/spec.json +0 -0
  74. {airbyte_source_shopify-4.2.2 → airbyte_source_shopify-4.2.4}/source_shopify/transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-source-shopify
3
- Version: 4.2.2
3
+ Version: 4.2.4
4
4
  Summary: Source CDK implementation for Shopify.
5
5
  Home-page: https://airbyte.com
6
6
  License: ELv2
@@ -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.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 checkpointed_cursor:
649
- # the partial result has no records to resume from, the rest of the slice must not be skipped
650
- self._raise_job_without_result("exited with FAILED and its partial result has no records to checkpoint from")
651
- if checkpoint_from_self_cancel and not checkpointed_cursor:
652
- # the result has no records to resume from, the slice is re-run with the checkpointing disabled
653
- self._disable_checkpointing("but its result has no records to checkpoint from")
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.insert(index, Field(name=key, alias=self.parent_stream_cursor_alias))
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:
@@ -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, is_throttled_graphql_error
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-07"
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 THROTTLED GraphQL error, everything else follows the status-code mapping
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 is_throttled_graphql_error(errors) else FailureType.system_error,
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 []
@@ -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 LimitReducingErrorHandler, ShopifyNonRetryableErrors, ShopifyRateLimiter, is_throttled_graphql_error
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", {}).get("events", {}).get("pageInfo", {})
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
- events = json_response.get("data", {}).get("events", {}).get("nodes", [])
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 is_throttled_graphql_error(errors):
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 due to throttling."
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 due to throttling.")
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
- code_discount = result.get("data", {}).get("codeDiscountNode", {}).get("codeDiscount") or {}
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 is_throttled_graphql_error(errors: List[Mapping[str, Any]]) -> bool:
105
- """Shopify GraphQL reports throttling as HTTP 200 with `errors[].extensions.code` in GRAPHQL_THROTTLE_ERROR_CODES."""
106
- return any((error.get("extensions") or {}).get("code") in GRAPHQL_THROTTLE_ERROR_CODES for error in errors)
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 as HTTP 200 with `errors[].extensions.code` THROTTLED / MAX_COST_EXCEEDED
377
- (https://shopify.dev/docs/api/admin-graphql/2026-07#status-and-error-codes). Retry such pages instead of
378
- treating the 200 as a success: the cost bucket refills at the shop's restore rate within seconds.
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 is_throttled_graphql_error(body.get("errors") or [] if isinstance(body, dict) else []):
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 throttled, retrying.",
394
+ error_message="Shopify GraphQL request hit a transient error, retrying.",
392
395
  )
393
396
  return super().interpret_response(response_or_exception)