airbyte-source-shopify 3.0.6.dev202504252338__py3-none-any.whl → 3.0.6.dev202505192339__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_source_shopify-3.0.6.dev202504252338.dist-info → airbyte_source_shopify-3.0.6.dev202505192339.dist-info}/METADATA +1 -1
- {airbyte_source_shopify-3.0.6.dev202504252338.dist-info → airbyte_source_shopify-3.0.6.dev202505192339.dist-info}/RECORD +8 -8
- {airbyte_source_shopify-3.0.6.dev202504252338.dist-info → airbyte_source_shopify-3.0.6.dev202505192339.dist-info}/WHEEL +1 -1
- source_shopify/spec.json +2 -4
- source_shopify/streams/base_streams.py +22 -8
- source_shopify/streams/streams.py +6 -0
- source_shopify/utils.py +51 -1
- {airbyte_source_shopify-3.0.6.dev202504252338.dist-info → airbyte_source_shopify-3.0.6.dev202505192339.dist-info}/entry_points.txt +0 -0
|
@@ -59,12 +59,12 @@ source_shopify/shopify_graphql/bulk/retry.py,sha256=R5rSJJE8D5zcj6mN-OmmNO2aFZEI
|
|
|
59
59
|
source_shopify/shopify_graphql/bulk/status.py,sha256=RmuQ2XsYL3iRCpVGxea9F1wXGmbwasDCSXjaTyL4LMA,328
|
|
60
60
|
source_shopify/shopify_graphql/bulk/tools.py,sha256=nUQ2ZmPTKJNJdfLToR6KJtLKcJFCChSifkAOvwg0Vss,4065
|
|
61
61
|
source_shopify/source.py,sha256=txb3wIm-3xXd8-5QLSeu2TeHBSnppwy5PEIOEl40mVw,8517
|
|
62
|
-
source_shopify/spec.json,sha256=
|
|
63
|
-
source_shopify/streams/base_streams.py,sha256=
|
|
64
|
-
source_shopify/streams/streams.py,sha256=
|
|
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
|
|
65
65
|
source_shopify/transform.py,sha256=mn0htL812_90zc_YszGQa0hHcIZQpYYdmk8IqpZm5TI,4685
|
|
66
|
-
source_shopify/utils.py,sha256=
|
|
67
|
-
airbyte_source_shopify-3.0.6.
|
|
68
|
-
airbyte_source_shopify-3.0.6.
|
|
69
|
-
airbyte_source_shopify-3.0.6.
|
|
70
|
-
airbyte_source_shopify-3.0.6.
|
|
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,,
|
source_shopify/spec.json
CHANGED
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"description": "The name of your Shopify store found in the URL. For example, if your URL was https://NAME.myshopify.com, then the name would be 'NAME' or 'NAME.myshopify.com'.",
|
|
14
14
|
"pattern": "^(?!https://)(?!https://).*",
|
|
15
15
|
"examples": ["my-store", "my-store.myshopify.com"],
|
|
16
|
-
"order": 1
|
|
17
|
-
"airbyte_hidden": true
|
|
16
|
+
"order": 1
|
|
18
17
|
},
|
|
19
18
|
"credentials": {
|
|
20
19
|
"title": "Shopify Authorization Method",
|
|
@@ -74,8 +73,7 @@
|
|
|
74
73
|
"airbyte_secret": true,
|
|
75
74
|
"order": 1
|
|
76
75
|
}
|
|
77
|
-
}
|
|
78
|
-
"airbyte_hidden": true
|
|
76
|
+
}
|
|
79
77
|
}
|
|
80
78
|
]
|
|
81
79
|
},
|
|
@@ -45,6 +45,7 @@ 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
|
|
48
49
|
|
|
49
50
|
@property
|
|
50
51
|
@abstractmethod
|
|
@@ -73,24 +74,37 @@ class ShopifyStream(HttpStream, ABC):
|
|
|
73
74
|
return None
|
|
74
75
|
|
|
75
76
|
def request_params(self, next_page_token: Optional[Mapping[str, Any]] = None, **kwargs) -> MutableMapping[str, Any]:
|
|
76
|
-
params = {"limit": self.
|
|
77
|
+
params = {"limit": self._current_limit}
|
|
77
78
|
if next_page_token:
|
|
78
|
-
|
|
79
|
+
temp_params = dict(next_page_token)
|
|
80
|
+
temp_params.pop("limit", None)
|
|
81
|
+
params.update(temp_params)
|
|
79
82
|
else:
|
|
80
83
|
params["order"] = f"{self.order_field} asc"
|
|
81
|
-
|
|
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
|
|
82
89
|
return params
|
|
83
90
|
|
|
84
|
-
@limiter.balance_rate_limit()
|
|
85
91
|
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
|
86
|
-
if response.status_code
|
|
92
|
+
if response.status_code == requests.codes.OK:
|
|
93
|
+
self._current_limit = self.limit
|
|
87
94
|
try:
|
|
88
95
|
json_response = response.json()
|
|
89
|
-
records = json_response.get(self.data_field, []) if self.data_field
|
|
96
|
+
records = json_response.get(self.data_field, []) if self.data_field else json_response
|
|
90
97
|
yield from self.produce_records(records)
|
|
91
|
-
except
|
|
92
|
-
|
|
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)
|
|
93
104
|
yield {}
|
|
105
|
+
else:
|
|
106
|
+
self.logger.warning(f"Non-OK response: {response.status_code}")
|
|
107
|
+
yield from []
|
|
94
108
|
|
|
95
109
|
def produce_records(
|
|
96
110
|
self, records: Optional[Union[Iterable[Mapping[str, Any]], Mapping[str, Any]]] = None
|
|
@@ -31,9 +31,11 @@ from source_shopify.shopify_graphql.bulk.query import (
|
|
|
31
31
|
ProfileLocationGroups,
|
|
32
32
|
Transaction,
|
|
33
33
|
)
|
|
34
|
+
from source_shopify.utils import LimitReducingErrorHandler
|
|
34
35
|
|
|
35
36
|
from airbyte_cdk import HttpSubStream
|
|
36
37
|
from airbyte_cdk.sources.streams.core import package_name_from_class
|
|
38
|
+
from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
|
|
37
39
|
from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader
|
|
38
40
|
|
|
39
41
|
from .base_streams import (
|
|
@@ -93,6 +95,10 @@ class Orders(IncrementalShopifyStreamWithDeletedEvents):
|
|
|
93
95
|
params["status"] = "any"
|
|
94
96
|
return params
|
|
95
97
|
|
|
98
|
+
def get_error_handler(self) -> Optional[ErrorHandler]:
|
|
99
|
+
default_handler = super().get_error_handler()
|
|
100
|
+
return LimitReducingErrorHandler(stream=self, default_handler=default_handler)
|
|
101
|
+
|
|
96
102
|
|
|
97
103
|
class Disputes(IncrementalShopifyStream):
|
|
98
104
|
data_field = "disputes"
|
source_shopify/utils.py
CHANGED
|
@@ -7,11 +7,13 @@ import enum
|
|
|
7
7
|
import logging
|
|
8
8
|
from functools import wraps
|
|
9
9
|
from time import sleep
|
|
10
|
-
from typing import Any, Callable, Dict, Final, List, Mapping, Optional
|
|
10
|
+
from typing import Any, Callable, Dict, Final, List, Mapping, Optional, Union
|
|
11
|
+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
11
12
|
|
|
12
13
|
import requests
|
|
13
14
|
|
|
14
15
|
from airbyte_cdk.models import FailureType
|
|
16
|
+
from airbyte_cdk.sources.streams.http.error_handlers import ErrorHandler
|
|
15
17
|
from airbyte_cdk.sources.streams.http.error_handlers.response_models import ErrorResolution, ResponseAction
|
|
16
18
|
from airbyte_cdk.utils import AirbyteTracedException
|
|
17
19
|
|
|
@@ -323,3 +325,51 @@ class EagerlyCachedStreamState:
|
|
|
323
325
|
return func(*args, **kwargs)
|
|
324
326
|
|
|
325
327
|
return decorator
|
|
328
|
+
|
|
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.
|
|
333
|
+
"""
|
|
334
|
+
|
|
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
|
|
346
|
+
|
|
347
|
+
def interpret_response(self, response_or_exception: Optional[Union[requests.Response, Exception]]) -> ErrorResolution:
|
|
348
|
+
if isinstance(response_or_exception, requests.Response):
|
|
349
|
+
response = response_or_exception
|
|
350
|
+
if response.status_code == 500:
|
|
351
|
+
current_limit = self.stream._current_limit
|
|
352
|
+
if current_limit > 1:
|
|
353
|
+
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
|
|
357
|
+
return ErrorResolution(
|
|
358
|
+
response_action=ResponseAction.RETRY,
|
|
359
|
+
failure_type=FailureType.transient_error,
|
|
360
|
+
error_message=f"Server error 500: Reduced limit to {new_limit}",
|
|
361
|
+
)
|
|
362
|
+
return ErrorResolution(
|
|
363
|
+
response_action=ResponseAction.FAIL,
|
|
364
|
+
failure_type=FailureType.transient_error,
|
|
365
|
+
error_message="Persistent 500 error after reducing limit to 1",
|
|
366
|
+
)
|
|
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))
|