airbyte-source-shopify 3.0.6.dev202505192339__py3-none-any.whl → 3.0.8__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.
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: airbyte-source-shopify
3
- Version: 3.0.6.dev202505192339
3
+ Version: 3.0.8
4
4
  Summary: Source CDK implementation for Shopify.
5
+ Home-page: https://airbyte.com
5
6
  License: ELv2
6
7
  Author: Airbyte
7
8
  Author-email: contact@airbyte.io
@@ -15,7 +16,6 @@ Requires-Dist: graphql-query (>=1,<2)
15
16
  Requires-Dist: pendulum (>=2.1.2,<3.0.0)
16
17
  Requires-Dist: sgqlc (==16.3)
17
18
  Project-URL: Documentation, https://docs.airbyte.com/integrations/sources/shopify
18
- Project-URL: Homepage, https://airbyte.com
19
19
  Project-URL: Repository, https://github.com/airbytehq/airbyte
20
20
  Description-Content-Type: text/markdown
21
21
 
@@ -60,11 +60,11 @@ source_shopify/shopify_graphql/bulk/status.py,sha256=RmuQ2XsYL3iRCpVGxea9F1wXGmb
60
60
  source_shopify/shopify_graphql/bulk/tools.py,sha256=nUQ2ZmPTKJNJdfLToR6KJtLKcJFCChSifkAOvwg0Vss,4065
61
61
  source_shopify/source.py,sha256=txb3wIm-3xXd8-5QLSeu2TeHBSnppwy5PEIOEl40mVw,8517
62
62
  source_shopify/spec.json,sha256=ITYWiQ-NrI5VISk5qmUQhp9ChUE2FV18d8xzVzPwvAg,6144
63
- source_shopify/streams/base_streams.py,sha256=Jhxe4rkm330g8Us1C8V-9-Zb7-4z1z5xrNtSI5QjQ28,42272
64
- source_shopify/streams/streams.py,sha256=ys0v66J2NTGEJr1L78FuxZBcFoOpLA-FJwf5OcbvwhM,14215
63
+ source_shopify/streams/base_streams.py,sha256=FFIpHd5_-Z61W_jUucdr8D2MzUete1Y2E50bQDCLakE,41555
64
+ source_shopify/streams/streams.py,sha256=YV1JAuD8SmGDmrt6QOgGgC8hA43ijb6ltgv11OJPBxA,14696
65
65
  source_shopify/transform.py,sha256=mn0htL812_90zc_YszGQa0hHcIZQpYYdmk8IqpZm5TI,4685
66
- source_shopify/utils.py,sha256=sjiBSh7Ygtg1RTsFvGY3oDjAhqEaodFiLmQd2aurgSk,16609
67
- airbyte_source_shopify-3.0.6.dev202505192339.dist-info/METADATA,sha256=AjuciXe4zunbOM2FEdv0qQFUhLeKIQlErtCpVoQiRO0,5325
68
- airbyte_source_shopify-3.0.6.dev202505192339.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
69
- airbyte_source_shopify-3.0.6.dev202505192339.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
70
- airbyte_source_shopify-3.0.6.dev202505192339.dist-info/RECORD,,
66
+ source_shopify/utils.py,sha256=DSqEchu-MQJ7zust7CNfqOkGIv9OSR-5UUsuD-bsDa8,16224
67
+ airbyte_source_shopify-3.0.8.dist-info/METADATA,sha256=vcT2Y4YZvQviGVrZ5dNkGN5jEW9vI2r1N74D6qtcJ5E,5297
68
+ airbyte_source_shopify-3.0.8.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
+ airbyte_source_shopify-3.0.8.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
70
+ airbyte_source_shopify-3.0.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -45,7 +45,6 @@ class ShopifyStream(HttpStream, ABC):
45
45
  super().__init__(authenticator=config["authenticator"])
46
46
  self._transformer = DataTypeEnforcer(self.get_json_schema())
47
47
  self.config = config
48
- self._current_limit = self.limit # Dynamic limit initialized to default
49
48
 
50
49
  @property
51
50
  @abstractmethod
@@ -74,37 +73,24 @@ class ShopifyStream(HttpStream, ABC):
74
73
  return None
75
74
 
76
75
  def request_params(self, next_page_token: Optional[Mapping[str, Any]] = None, **kwargs) -> MutableMapping[str, Any]:
77
- params = {"limit": self._current_limit}
76
+ params = {"limit": self.limit}
78
77
  if next_page_token:
79
- temp_params = dict(next_page_token)
80
- temp_params.pop("limit", None)
81
- params.update(temp_params)
78
+ params.update(**next_page_token)
82
79
  else:
83
80
  params["order"] = f"{self.order_field} asc"
84
- stream_state = kwargs.get("stream_state")
85
- if stream_state is not None:
86
- params[self.filter_field] = stream_state.get(self.filter_field, self.default_filter_field_value)
87
- else:
88
- params[self.filter_field] = self.default_filter_field_value
81
+ params[self.filter_field] = self.default_filter_field_value
89
82
  return params
90
83
 
84
+ @limiter.balance_rate_limit()
91
85
  def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
92
- if response.status_code == requests.codes.OK:
93
- self._current_limit = self.limit
86
+ if response.status_code is requests.codes.OK:
94
87
  try:
95
88
  json_response = response.json()
96
- records = json_response.get(self.data_field, []) if self.data_field else json_response
89
+ records = json_response.get(self.data_field, []) if self.data_field is not None else json_response
97
90
  yield from self.produce_records(records)
98
- except requests.exceptions.JSONDecodeError as e:
99
- error_msg = (
100
- f"Failed to decode JSON from response (status code: {response.status_code}, "
101
- f"content length: {len(response.content)}). JSONDecodeError at position {e.pos}: {e.msg}"
102
- )
103
- self.logger.warning(error_msg)
91
+ except RequestException as e:
92
+ self.logger.warning(f"Unexpected error in `parse_response`: {e}, the actual response data: {response.text}")
104
93
  yield {}
105
- else:
106
- self.logger.warning(f"Non-OK response: {response.status_code}")
107
- yield from []
108
94
 
109
95
  def produce_records(
110
96
  self, records: Optional[Union[Iterable[Mapping[str, Any]], Mapping[str, Any]]] = None
@@ -3,6 +3,8 @@
3
3
  #
4
4
 
5
5
 
6
+ import logging
7
+ import sys
6
8
  from typing import Any, Iterable, Mapping, MutableMapping, Optional
7
9
 
8
10
  import requests
@@ -31,11 +33,12 @@ from source_shopify.shopify_graphql.bulk.query import (
31
33
  ProfileLocationGroups,
32
34
  Transaction,
33
35
  )
34
- from source_shopify.utils import LimitReducingErrorHandler
36
+ from source_shopify.utils import LimitReducingErrorHandler, ShopifyNonRetryableErrors
35
37
 
36
38
  from airbyte_cdk import HttpSubStream
37
39
  from airbyte_cdk.sources.streams.core import package_name_from_class
38
40
  from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
41
+ from airbyte_cdk.sources.streams.http.error_handlers.default_error_mapping import DEFAULT_ERROR_MAPPING
39
42
  from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader
40
43
 
41
44
  from .base_streams import (
@@ -86,18 +89,24 @@ class MetafieldCustomers(IncrementalShopifyGraphQlBulkStream):
86
89
  class Orders(IncrementalShopifyStreamWithDeletedEvents):
87
90
  data_field = "orders"
88
91
  deleted_events_api_name = "Order"
92
+ initial_limit = 250
89
93
 
90
- def request_params(
91
- self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs
92
- ) -> MutableMapping[str, Any]:
94
+ def __init__(self, config: Mapping[str, Any]):
95
+ self._error_handler = LimitReducingErrorHandler(
96
+ max_retries=5,
97
+ error_mapping=DEFAULT_ERROR_MAPPING | ShopifyNonRetryableErrors("orders"),
98
+ )
99
+ super().__init__(config)
100
+
101
+ def request_params(self, stream_state=None, next_page_token=None, **kwargs):
93
102
  params = super().request_params(stream_state=stream_state, next_page_token=next_page_token, **kwargs)
103
+ params["limit"] = self.initial_limit # Always start with the default limit; error handler will mutate on retry
94
104
  if not next_page_token:
95
105
  params["status"] = "any"
96
106
  return params
97
107
 
98
- def get_error_handler(self) -> Optional[ErrorHandler]:
99
- default_handler = super().get_error_handler()
100
- return LimitReducingErrorHandler(stream=self, default_handler=default_handler)
108
+ def get_error_handler(self):
109
+ return self._error_handler
101
110
 
102
111
 
103
112
  class Disputes(IncrementalShopifyStream):
@@ -130,6 +139,12 @@ class MetafieldProducts(IncrementalShopifyGraphQlBulkStream):
130
139
  parent_stream_class = Products
131
140
  bulk_query: MetafieldProduct = MetafieldProduct
132
141
 
142
+ state_checkpoint_interval = sys.maxsize
143
+
144
+ @property
145
+ def filter_by_state_checkpoint(self) -> bool:
146
+ return True
147
+
133
148
 
134
149
  class ProductImages(IncrementalShopifyGraphQlBulkStream):
135
150
  parent_stream_class = Products
source_shopify/utils.py CHANGED
@@ -13,7 +13,7 @@ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
13
13
  import requests
14
14
 
15
15
  from airbyte_cdk.models import FailureType
16
- from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
16
+ from airbyte_cdk.sources.streams.http.error_handlers import HttpStatusErrorHandler
17
17
  from airbyte_cdk.sources.streams.http.error_handlers.response_models import ErrorResolution, ResponseAction
18
18
  from airbyte_cdk.utils import AirbyteTracedException
19
19
 
@@ -327,49 +327,36 @@ class EagerlyCachedStreamState:
327
327
  return decorator
328
328
 
329
329
 
330
- class LimitReducingErrorHandler(ErrorHandler):
331
- """Custom error handler that reduces the request limit (of items/page) on 500 errors and retries.
332
- limit is halved on each retry until it reaches 1. We still exponentially backoff on retries in addition to this.
330
+ class LimitReducingErrorHandler(HttpStatusErrorHandler):
331
+ """
332
+ Error handler that halves the page size (limit) on each 500 error, down to 1.
333
+ No stream instance required; operates directly on the request URL.
333
334
  """
334
335
 
335
- def __init__(self, stream: "ShopifyStream", default_handler: ErrorHandler):
336
- self.stream = stream
337
- self.default_handler = default_handler
338
-
339
- @property
340
- def max_retries(self) -> Optional[int]:
341
- return self.default_handler.max_retries
342
-
343
- @property
344
- def max_time(self) -> Optional[int]:
345
- return self.default_handler.max_time
336
+ def __init__(self, max_retries: int, error_mapping: dict):
337
+ super().__init__(logger=None, max_retries=max_retries, error_mapping=error_mapping)
346
338
 
347
- def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]]) -> ErrorResolution:
339
+ def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]] = None) -> ErrorResolution:
348
340
  if isinstance(response_or_exception, requests.Response):
349
341
  response = response_or_exception
350
342
  if response.status_code == 500:
351
- current_limit = self.stream._current_limit
343
+ # Extract current limit from the URL, default to 250 if not present
344
+ parsed = urlparse(response.request.url)
345
+ query = parse_qs(parsed.query)
346
+ current_limit = int(query.get("limit", ["250"])[0])
352
347
  if current_limit > 1:
353
348
  new_limit = max(1, current_limit // 2)
354
- self.stream._current_limit = new_limit
355
- new_url = self.update_limit_in_url(response.request.url, new_limit)
356
- response.request.url = new_url
349
+ query["limit"] = [str(new_limit)]
350
+ new_query = urlencode(query, doseq=True)
351
+ response.request.url = urlunparse(parsed._replace(query=new_query))
357
352
  return ErrorResolution(
358
353
  response_action=ResponseAction.RETRY,
359
354
  failure_type=FailureType.transient_error,
360
- error_message=f"Server error 500: Reduced limit to {new_limit}",
355
+ error_message=f"Server error 500: Reduced limit to {new_limit} and updating request URL",
361
356
  )
362
357
  return ErrorResolution(
363
358
  response_action=ResponseAction.FAIL,
364
359
  failure_type=FailureType.transient_error,
365
360
  error_message="Persistent 500 error after reducing limit to 1",
366
361
  )
367
- return self.default_handler.interpret_response(response_or_exception)
368
-
369
- def update_limit_in_url(self, url: str, new_limit: int) -> str:
370
- """Update the 'limit' parameter in the URL query string."""
371
- parsed = urlparse(url)
372
- query = parse_qs(parsed.query)
373
- query["limit"] = [str(new_limit)]
374
- new_query = urlencode(query, doseq=True)
375
- return urlunparse(parsed._replace(query=new_query))
362
+ return super().interpret_response(response_or_exception)