airbyte-source-shopify 3.0.5.dev202505152023__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: airbyte-source-shopify
3
- Version: 3.0.5.dev202505152023
3
+ Version: 3.0.6.dev202505192339
4
4
  Summary: Source CDK implementation for Shopify.
5
5
  License: ELv2
6
6
  Author: Airbyte
@@ -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=ndDvDlMnjmWm_Ymb1l2skn1mjP-gCC1BscEopjVGkjM,6170
63
- source_shopify/streams/base_streams.py,sha256=FFIpHd5_-Z61W_jUucdr8D2MzUete1Y2E50bQDCLakE,41555
64
- source_shopify/streams/streams.py,sha256=8LkM-SRhbGX2MwfHsjcWY62Z6g0jKZ0QfcS4B-vKPoM,13882
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=CAEKcxIroBo6kRFCyvC1bfOyfbGy7Z7seqZB7Eekl44,14209
67
- airbyte_source_shopify-3.0.5.dev202505152023.dist-info/METADATA,sha256=4a7JyPg4YXoGCGurKvhiRIHwXd4nkQ3pnEUun6_xQ90,5325
68
- airbyte_source_shopify-3.0.5.dev202505152023.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
69
- airbyte_source_shopify-3.0.5.dev202505152023.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
70
- airbyte_source_shopify-3.0.5.dev202505152023.dist-info/RECORD,,
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
@@ -4,7 +4,7 @@
4
4
  "$schema": "https://json-schema.org/draft-07/schema#",
5
5
  "title": "Shopify Source CDK Specifications",
6
6
  "type": "object",
7
- "required": [],
7
+ "required": ["shop"],
8
8
  "additionalProperties": true,
9
9
  "properties": {
10
10
  "shop": {
@@ -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",
@@ -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.limit}
77
+ params = {"limit": self._current_limit}
77
78
  if next_page_token:
78
- params.update(**next_page_token)
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
- params[self.filter_field] = self.default_filter_field_value
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 is requests.codes.OK:
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 is not None else json_response
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 RequestException as e:
92
- self.logger.warning(f"Unexpected error in `parse_response`: {e}, the actual response data: {response.text}")
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))