airbyte-source-shopify 3.0.13.dev202512261812__py3-none-any.whl → 3.0.14.dev202512192257__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.1
2
2
  Name: airbyte-source-shopify
3
- Version: 3.0.13.dev202512261812
3
+ Version: 3.0.14.dev202512192257
4
4
  Summary: Source CDK implementation for Shopify.
5
5
  Home-page: https://airbyte.com
6
6
  License: ELv2
@@ -14,6 +14,7 @@ source_shopify/schemas/custom_collections.json,sha256=ElDY1y_G_VFPOGr9ipU022AZDW
14
14
  source_shopify/schemas/customer_address.json,sha256=kLKylul_xK7eO_aduvDddj078k8aQLM4JtUEpa5Tp_8,2628
15
15
  source_shopify/schemas/customer_journey_summary.json,sha256=GlZmIqFeR1x84DwgwMo28HQ8QseImkhvYxLSNRkUmwg,12733
16
16
  source_shopify/schemas/customers.json,sha256=pywejBEtvMnRHvvnJQhfA8NlCBjELv7XuMlKdpS4cZI,10233
17
+ source_shopify/schemas/deleted_products.json,sha256=7TbqrABNDg9ZFAeLJCPe2Z5jtk9QGbJDuoSKofGd1lg,773
17
18
  source_shopify/schemas/discount_codes.json,sha256=YxGptdpjXwM46fmzs5zoi3m96xAWKXrfllC_oIZW0vo,3730
18
19
  source_shopify/schemas/disputes.json,sha256=Uvzy1Gi954cLxij6H0Vbz0OWS6GS10YaJI4DJfY2x8I,1775
19
20
  source_shopify/schemas/draft_orders.json,sha256=3NaZt9Qk4mY8VO9pB5UBzw3GZibL8MvI0z_tlnMsdBI,24211
@@ -49,7 +50,7 @@ source_shopify/schemas/shop.json,sha256=vEGiTvEYX7qnMq06MRVBycqih49h49xjTNC6gJux
49
50
  source_shopify/schemas/smart_collections.json,sha256=kv7dINsvgzJ0RyKfFNKjU0apdNDXwQaHfnNZfQsshcU,2009
50
51
  source_shopify/schemas/tender_transactions.json,sha256=U8fdT-eflycEPzYSpBDiB0lp9wxmJHgioHTrICflh78,2006
51
52
  source_shopify/schemas/transactions.json,sha256=vbwscH3UcAtbSsC70mBka4oNaFR4S3S6IFBmzR7t37U,10226
52
- source_shopify/scopes.py,sha256=N0njfMHn3Q1AQXuTj5VfjQOio10jaDarpC_oLYnWvqc,6490
53
+ source_shopify/scopes.py,sha256=78f9QL3PJZ9UDx1gIWzNwx5fYJE9OB3vPi9RahB_kFw,6533
53
54
  source_shopify/shopify_graphql/bulk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
55
  source_shopify/shopify_graphql/bulk/exceptions.py,sha256=4dj7Za4xIfwL-zf8joT9svF_RSoGlE3GviMiIl1e1rs,2532
55
56
  source_shopify/shopify_graphql/bulk/job.py,sha256=c3Cg70_Io9jTD-rU-5MvjHaPmJCtcpeqEYnRtFECGOo,28673
@@ -58,13 +59,13 @@ source_shopify/shopify_graphql/bulk/record.py,sha256=X6VGngugv7a_S8UEeDo121BkdCV
58
59
  source_shopify/shopify_graphql/bulk/retry.py,sha256=R5rSJJE8D5zcj6mN-OmmNO2aFZEIdjAlWclDDVW5KPI,2626
59
60
  source_shopify/shopify_graphql/bulk/status.py,sha256=RmuQ2XsYL3iRCpVGxea9F1wXGmbwasDCSXjaTyL4LMA,328
60
61
  source_shopify/shopify_graphql/bulk/tools.py,sha256=nUQ2ZmPTKJNJdfLToR6KJtLKcJFCChSifkAOvwg0Vss,4065
61
- source_shopify/source.py,sha256=txb3wIm-3xXd8-5QLSeu2TeHBSnppwy5PEIOEl40mVw,8517
62
- source_shopify/spec.json,sha256=vwEY5T3IryqSne0cRcJO53FaLnApuKOKRRS6yQJABpo,6667
63
- source_shopify/streams/base_streams.py,sha256=xYuyH6YjxZYl2x8CsdIYl3AqxBtz5xF_Oioatn5V-Rs,43979
64
- source_shopify/streams/streams.py,sha256=YV1JAuD8SmGDmrt6QOgGgC8hA43ijb6ltgv11OJPBxA,14696
62
+ source_shopify/source.py,sha256=oikoM-VPNk62zlmeAQR59PMxfuXq2s42N7zaqLM6_lo,8575
63
+ source_shopify/spec.json,sha256=ITYWiQ-NrI5VISk5qmUQhp9ChUE2FV18d8xzVzPwvAg,6144
64
+ source_shopify/streams/base_streams.py,sha256=k_4uLaLADLRTUcSmP8uA_830uuzRvnqUaCVGcb0Zpd8,42625
65
+ source_shopify/streams/streams.py,sha256=F_otpKWbwn4Xn_wYVIdPF1MVjaOGke3BlvPG336Ip28,18105
65
66
  source_shopify/transform.py,sha256=mn0htL812_90zc_YszGQa0hHcIZQpYYdmk8IqpZm5TI,4685
66
67
  source_shopify/utils.py,sha256=DSqEchu-MQJ7zust7CNfqOkGIv9OSR-5UUsuD-bsDa8,16224
67
- airbyte_source_shopify-3.0.13.dev202512261812.dist-info/METADATA,sha256=WTrC0o1Xuj-TLuZQsJlsIEoKT3o1GAcXlR8ZiYFDbtU,5314
68
- airbyte_source_shopify-3.0.13.dev202512261812.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- airbyte_source_shopify-3.0.13.dev202512261812.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
70
- airbyte_source_shopify-3.0.13.dev202512261812.dist-info/RECORD,,
68
+ airbyte_source_shopify-3.0.14.dev202512192257.dist-info/METADATA,sha256=YtLc8y1hLx7E1BjYCj3XvskFi68Pvkzl5qHOnaf4nkU,5314
69
+ airbyte_source_shopify-3.0.14.dev202512192257.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
70
+ airbyte_source_shopify-3.0.14.dev202512192257.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
71
+ airbyte_source_shopify-3.0.14.dev202512192257.dist-info/RECORD,,
@@ -0,0 +1,27 @@
1
+ {
2
+ "type": ["object", "null"],
3
+ "additionalProperties": true,
4
+ "properties": {
5
+ "id": {
6
+ "description": "The unique identifier of the deleted product.",
7
+ "type": ["null", "integer"]
8
+ },
9
+ "deleted_at": {
10
+ "description": "The date and time when the product was deleted.",
11
+ "type": ["null", "string"],
12
+ "format": "date-time"
13
+ },
14
+ "deleted_message": {
15
+ "description": "Message related to the deletion of the product.",
16
+ "type": ["null", "string"]
17
+ },
18
+ "deleted_description": {
19
+ "description": "Description of the reason for deletion.",
20
+ "type": ["null", "string"]
21
+ },
22
+ "shop_url": {
23
+ "description": "The URL of the shop where the product was listed.",
24
+ "type": ["null", "string"]
25
+ }
26
+ }
27
+ }
source_shopify/scopes.py CHANGED
@@ -37,6 +37,7 @@ SCOPES_MAPPING: Mapping[str, set[str]] = {
37
37
  "MetafieldDraftOrders": ("read_draft_orders",),
38
38
  # SCOPE: read_products
39
39
  "Products": ("read_products",),
40
+ "DeletedProducts": ("read_products",),
40
41
  "MetafieldProducts": ("read_products",),
41
42
  "ProductImages": ("read_products",),
42
43
  "MetafieldProductImages": ("read_products",),
source_shopify/source.py CHANGED
@@ -27,6 +27,7 @@ from .streams.streams import (
27
27
  CustomerAddress,
28
28
  CustomerJourneySummary,
29
29
  Customers,
30
+ DeletedProducts,
30
31
  DiscountCodes,
31
32
  Disputes,
32
33
  DraftOrders,
@@ -211,6 +212,7 @@ class SourceShopify(AbstractSource):
211
212
  PriceRules(config),
212
213
  ProductImages(config),
213
214
  Products(config),
215
+ DeletedProducts(config),
214
216
  ProductVariants(config),
215
217
  Shop(config),
216
218
  SmartCollections(config),
source_shopify/spec.json CHANGED
@@ -119,15 +119,6 @@
119
119
  "default": 100000,
120
120
  "minimum": 15000,
121
121
  "maximum": 1000000
122
- },
123
- "lookback_window_in_days": {
124
- "type": "integer",
125
- "title": "Lookback Window (in days)",
126
- "description": "The number of days to look back for records that may have been missed due to race conditions or late-arriving data. This is useful for ensuring data completeness in incremental syncs. Setting this to 1 or more will re-fetch records from the specified number of days before the last sync state.",
127
- "default": 0,
128
- "minimum": 0,
129
- "maximum": 30,
130
- "order": 7
131
122
  }
132
123
  }
133
124
  },
@@ -209,24 +209,6 @@ class IncrementalShopifyStream(ShopifyStream, ABC):
209
209
  current_state_value = current_stream_state.get(self.cursor_field) or self.default_state_comparison_value
210
210
  return {self.cursor_field: max(last_record_value, current_state_value)}
211
211
 
212
- def _apply_lookback_window(self, state_value: str) -> str:
213
- """
214
- Apply the lookback window to the state value by subtracting the configured number of days.
215
- This helps capture records that may have been missed due to race conditions or late-arriving data.
216
- """
217
- lookback_days = self.config.get("lookback_window_in_days", 0)
218
- if lookback_days > 0:
219
- state_datetime = pdm.parse(state_value)
220
- adjusted_datetime = state_datetime.subtract(days=lookback_days)
221
- # Ensure we don't go before the configured start_date
222
- start_date = self.config.get("start_date")
223
- if start_date:
224
- start_datetime = pdm.parse(start_date)
225
- if adjusted_datetime < start_datetime:
226
- adjusted_datetime = start_datetime
227
- return adjusted_datetime.to_rfc3339_string()
228
- return state_value
229
-
230
212
  @stream_state_cache.cache_stream_state
231
213
  def request_params(
232
214
  self, stream_state: Optional[Mapping[str, Any]] = None, next_page_token: Optional[Mapping[str, Any]] = None, **kwargs
@@ -236,11 +218,7 @@ class IncrementalShopifyStream(ShopifyStream, ABC):
236
218
  if not next_page_token:
237
219
  params["order"] = f"{self.order_field} asc"
238
220
  if stream_state:
239
- state_value = stream_state.get(self.cursor_field)
240
- # Apply lookback window to go back N days from the state
241
- if state_value and self.filter_field != "since_id":
242
- state_value = self._apply_lookback_window(state_value)
243
- params[self.filter_field] = state_value
221
+ params[self.filter_field] = stream_state.get(self.cursor_field)
244
222
  return params
245
223
 
246
224
  def track_checkpoint_cursor(self, record_value: Union[str, int], filter_record_value: Optional[str] = None) -> None:
@@ -844,9 +822,6 @@ class IncrementalShopifyGraphQlBulkStream(IncrementalShopifyStream):
844
822
  def stream_slices(self, stream_state: Optional[Mapping[str, Any]] = None, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]:
845
823
  if self.filter_field:
846
824
  state = self._get_state_value(stream_state)
847
- # Apply lookback window only when we have a stream state (not on first sync)
848
- if stream_state:
849
- state = self._apply_lookback_window(state)
850
825
  start = pdm.parse(state)
851
826
  end = pdm.now()
852
827
  while start < end:
@@ -135,6 +135,100 @@ class Products(IncrementalShopifyGraphQlBulkStream):
135
135
  bulk_query: Product = Product
136
136
 
137
137
 
138
+ class DeletedProducts(IncrementalShopifyStream):
139
+ """
140
+ Stream for fetching deleted products using the Shopify GraphQL Events API.
141
+ This stream queries events with action:destroy and subject_type:Product to get deleted product records.
142
+ https://shopify.dev/docs/api/admin-graphql/latest/queries/events
143
+ """
144
+
145
+ data_field = "graphql"
146
+ cursor_field = "deleted_at"
147
+ http_method = "POST"
148
+ filter_field = None
149
+
150
+ _page_cursor: Optional[str] = None
151
+
152
+ EVENTS_QUERY = """
153
+ query GetDeletedProductEvents($first: Int!, $after: String, $query: String) {
154
+ events(first: $first, after: $after, query: $query, sortKey: CREATED_AT) {
155
+ pageInfo {
156
+ hasNextPage
157
+ endCursor
158
+ }
159
+ nodes {
160
+ ... on BasicEvent {
161
+ id
162
+ createdAt
163
+ message
164
+ subjectId
165
+ subjectType
166
+ }
167
+ }
168
+ }
169
+ }
170
+ """
171
+
172
+ @property
173
+ def url_base(self) -> str:
174
+ return f"https://{self.config['shop']}.myshopify.com/admin/api/{self.config['api_version']}/graphql.json"
175
+
176
+ def path(self, **kwargs) -> str:
177
+ return ""
178
+
179
+ def request_params(
180
+ self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs
181
+ ) -> MutableMapping[str, Any]:
182
+ return {}
183
+
184
+ def request_body_json(
185
+ self,
186
+ stream_state: Optional[Mapping[str, Any]] = None,
187
+ stream_slice: Optional[Mapping[str, Any]] = None,
188
+ next_page_token: Optional[Mapping[str, Any]] = None,
189
+ ) -> Optional[Mapping[str, Any]]:
190
+ query_filter = "action:destroy AND subject_type:Product"
191
+ if stream_state and stream_state.get(self.cursor_field):
192
+ state_value = stream_state[self.cursor_field]
193
+ query_filter += f" AND created_at:>'{state_value}'"
194
+
195
+ variables = {
196
+ "first": 250,
197
+ "query": query_filter,
198
+ }
199
+
200
+ if next_page_token and next_page_token.get("cursor"):
201
+ variables["after"] = next_page_token["cursor"]
202
+
203
+ return {
204
+ "query": self.EVENTS_QUERY,
205
+ "variables": variables,
206
+ }
207
+
208
+ def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
209
+ json_response = response.json()
210
+ page_info = json_response.get("data", {}).get("events", {}).get("pageInfo", {})
211
+ if page_info.get("hasNextPage"):
212
+ return {"cursor": page_info.get("endCursor")}
213
+ return None
214
+
215
+ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
216
+ json_response = response.json()
217
+ events = json_response.get("data", {}).get("events", {}).get("nodes", [])
218
+ for event in events:
219
+ if event.get("subjectType") == "Product" and event.get("subjectId"):
220
+ subject_id = event.get("subjectId", "")
221
+ product_id = int(subject_id.split("/")[-1]) if "/" in subject_id else None
222
+ if product_id:
223
+ yield {
224
+ "id": product_id,
225
+ "deleted_at": event.get("createdAt"),
226
+ "deleted_message": event.get("message"),
227
+ "deleted_description": None,
228
+ "shop_url": self.config.get("shop"),
229
+ }
230
+
231
+
138
232
  class MetafieldProducts(IncrementalShopifyGraphQlBulkStream):
139
233
  parent_stream_class = Products
140
234
  bulk_query: MetafieldProduct = MetafieldProduct