airbyte-source-shopify 3.0.8.dev202509081459__py3-none-any.whl → 3.1.0__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.8.dev202509081459
3
+ Version: 3.1.0
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,22 +50,22 @@ 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
- source_shopify/shopify_graphql/bulk/job.py,sha256=qSd1EAVAjOazPRHc7G48OJJao3xqF9foKqxD0pIWick,28192
56
+ source_shopify/shopify_graphql/bulk/job.py,sha256=c3Cg70_Io9jTD-rU-5MvjHaPmJCtcpeqEYnRtFECGOo,28673
56
57
  source_shopify/shopify_graphql/bulk/query.py,sha256=D8rnI1SDw50-Gt18lt7YwwNNdsbVMbBfxZa9xVJZbto,130981
57
58
  source_shopify/shopify_graphql/bulk/record.py,sha256=X6VGngugv7a_S8UEeDo121BkdCVLj5nWlHK76A21kyo,16898
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/source.py,sha256=oikoM-VPNk62zlmeAQR59PMxfuXq2s42N7zaqLM6_lo,8575
62
63
  source_shopify/spec.json,sha256=ITYWiQ-NrI5VISk5qmUQhp9ChUE2FV18d8xzVzPwvAg,6144
63
- source_shopify/streams/base_streams.py,sha256=j3x38czi13KgvDuP7oHmfTClDr9S6Jfh_r-J97BF9Iw,42322
64
- source_shopify/streams/streams.py,sha256=D70Ik1vU75NKlmJMnS7W2-5gApA2ANq9eRnKligMTNw,14555
64
+ source_shopify/streams/base_streams.py,sha256=k_4uLaLADLRTUcSmP8uA_830uuzRvnqUaCVGcb0Zpd8,42625
65
+ source_shopify/streams/streams.py,sha256=96ZzuhlKny2scejzRNhL7IHQ2FJ6e7Z4Qfot6nccfQg,18899
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.8.dev202509081459.dist-info/METADATA,sha256=GUfH4CzDOyfH0Afvotv0IHmYcTGXCwoq44UpHficGHE,5313
68
- airbyte_source_shopify-3.0.8.dev202509081459.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
69
- airbyte_source_shopify-3.0.8.dev202509081459.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
70
- airbyte_source_shopify-3.0.8.dev202509081459.dist-info/RECORD,,
68
+ airbyte_source_shopify-3.1.0.dist-info/METADATA,sha256=zkgsUWjEe9nD_Mh6PfX0HgFTYBCShhc6I6oULF8ozrM,5297
69
+ airbyte_source_shopify-3.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
70
+ airbyte_source_shopify-3.1.0.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
71
+ airbyte_source_shopify-3.1.0.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",),
@@ -531,9 +531,14 @@ class ShopifyBulkManager:
531
531
  self, slice_end: datetime, checkpointed_cursor: Optional[str] = None, filter_checkpointed_cursor: Optional[str] = None
532
532
  ) -> datetime:
533
533
  """
534
- Choose between the existing `slice_end` value or `checkpointed_cursor` value, if provided.
534
+ Choose between the existing `slice_end` value or `checkpointed_cursor` value or `filter_checkpointed_cursor` value, if provided.
535
535
 
536
536
  Optionally: raises the `transient` error if the checkpoint collision occurs.
537
+
538
+ Note: filter_checkpointed_cursor is only used when cursor field is ID for streams like Customer Address etc.
539
+ This method should return a datetime from last checkpointed value to adjust slice end, when cursor value is ID (int type)
540
+ method gets end datetime from filter_checkpointed_cursor, which is value from filter field from last record.
541
+ See https://github.com/airbytehq/oncall/issues/9052 for more details.
537
542
  """
538
543
 
539
544
  if checkpointed_cursor:
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),
@@ -222,6 +222,11 @@ class IncrementalShopifyStream(ShopifyStream, ABC):
222
222
  return params
223
223
 
224
224
  def track_checkpoint_cursor(self, record_value: Union[str, int], filter_record_value: Optional[str] = None) -> None:
225
+ """
226
+ Tracks _checkpoint_cursor value (values from cursor field) and _filter_checkpointed_cursor value (value from filter field).
227
+ _filter_checkpointed_cursor value is only used when cursor field is ID for streams like Customer Address etc.
228
+ When after canceled/failed job source tries to adjust stream slice (see ShopifyBulkManager._adjust_slice_end()).
229
+ """
225
230
  if self.filter_by_state_checkpoint:
226
231
  # set checkpoint cursor
227
232
  if not self._checkpoint_cursor:
@@ -231,9 +236,7 @@ class IncrementalShopifyStream(ShopifyStream, ABC):
231
236
  self._checkpoint_cursor = record_value
232
237
 
233
238
  if filter_record_value:
234
- if not self._filter_checkpointed_cursor:
235
- self._filter_checkpointed_cursor = filter_record_value
236
- if str(filter_record_value) >= str(self._filter_checkpointed_cursor):
239
+ if not self._filter_checkpointed_cursor or str(filter_record_value) >= str(self._filter_checkpointed_cursor):
237
240
  self._filter_checkpointed_cursor = filter_record_value
238
241
 
239
242
  def should_checkpoint(self, index: int) -> bool:
@@ -4,6 +4,7 @@
4
4
 
5
5
 
6
6
  import logging
7
+ import sys
7
8
  from typing import Any, Iterable, Mapping, MutableMapping, Optional
8
9
 
9
10
  import requests
@@ -134,10 +135,120 @@ class Products(IncrementalShopifyGraphQlBulkStream):
134
135
  bulk_query: Product = Product
135
136
 
136
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
+ Note: This stream extends IncrementalShopifyStream (REST base class) rather than IncrementalShopifyGraphQlBulkStream
145
+ because it uses Shopify's standard GraphQL Events API, NOT the Bulk Operations API (bulkOperationRunQuery).
146
+ The Events API has a fundamentally different architecture:
147
+ - Uses immediate queries with cursor pagination (not async job creation + file download)
148
+ - Returns results in response.data.events.nodes (not JSONL files)
149
+ - Requires different URL endpoint (/graphql.json), request format (POST with query in body), and pagination logic
150
+
151
+ Therefore, url_base, path, request_params, next_page_token, and parse_response are overridden to accommodate
152
+ the GraphQL request/response structure while maintaining incremental sync capabilities from the base class.
153
+ """
154
+
155
+ data_field = "graphql"
156
+ cursor_field = "deleted_at"
157
+ http_method = "POST"
158
+ filter_field = None
159
+
160
+ _page_cursor: Optional[str] = None
161
+
162
+ EVENTS_QUERY = """
163
+ query GetDeletedProductEvents($first: Int!, $after: String, $query: String) {
164
+ events(first: $first, after: $after, query: $query, sortKey: CREATED_AT) {
165
+ pageInfo {
166
+ hasNextPage
167
+ endCursor
168
+ }
169
+ nodes {
170
+ ... on BasicEvent {
171
+ id
172
+ createdAt
173
+ message
174
+ subjectId
175
+ subjectType
176
+ }
177
+ }
178
+ }
179
+ }
180
+ """
181
+
182
+ @property
183
+ def url_base(self) -> str:
184
+ return f"https://{self.config['shop']}.myshopify.com/admin/api/{self.api_version}/graphql.json"
185
+
186
+ def path(self, **kwargs) -> str:
187
+ return ""
188
+
189
+ def request_params(
190
+ self, stream_state: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None, **kwargs
191
+ ) -> MutableMapping[str, Any]:
192
+ return {}
193
+
194
+ def request_body_json(
195
+ self,
196
+ stream_state: Optional[Mapping[str, Any]] = None,
197
+ stream_slice: Optional[Mapping[str, Any]] = None,
198
+ next_page_token: Optional[Mapping[str, Any]] = None,
199
+ ) -> Optional[Mapping[str, Any]]:
200
+ query_filter = "action:destroy AND subject_type:Product"
201
+ if stream_state and stream_state.get(self.cursor_field):
202
+ state_value = stream_state[self.cursor_field]
203
+ query_filter += f" AND created_at:>'{state_value}'"
204
+
205
+ variables = {
206
+ "first": 250,
207
+ "query": query_filter,
208
+ }
209
+
210
+ if next_page_token and next_page_token.get("cursor"):
211
+ variables["after"] = next_page_token["cursor"]
212
+
213
+ return {
214
+ "query": self.EVENTS_QUERY,
215
+ "variables": variables,
216
+ }
217
+
218
+ def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
219
+ json_response = response.json()
220
+ page_info = json_response.get("data", {}).get("events", {}).get("pageInfo", {})
221
+ if page_info.get("hasNextPage"):
222
+ return {"cursor": page_info.get("endCursor")}
223
+ return None
224
+
225
+ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
226
+ json_response = response.json()
227
+ events = json_response.get("data", {}).get("events", {}).get("nodes", [])
228
+ for event in events:
229
+ if event.get("subjectType") == "PRODUCT" and event.get("subjectId"):
230
+ subject_id = event.get("subjectId", "")
231
+ product_id = int(subject_id.split("/")[-1]) if "/" in subject_id else None
232
+ if product_id:
233
+ yield {
234
+ "id": product_id,
235
+ "deleted_at": event.get("createdAt"),
236
+ "deleted_message": event.get("message"),
237
+ "deleted_description": None,
238
+ "shop_url": self.config.get("shop"),
239
+ }
240
+
241
+
137
242
  class MetafieldProducts(IncrementalShopifyGraphQlBulkStream):
138
243
  parent_stream_class = Products
139
244
  bulk_query: MetafieldProduct = MetafieldProduct
140
245
 
246
+ state_checkpoint_interval = sys.maxsize
247
+
248
+ @property
249
+ def filter_by_state_checkpoint(self) -> bool:
250
+ return True
251
+
141
252
 
142
253
  class ProductImages(IncrementalShopifyGraphQlBulkStream):
143
254
  parent_stream_class = Products