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.
- {airbyte_source_shopify-3.0.8.dev202509081459.dist-info → airbyte_source_shopify-3.1.0.dist-info}/METADATA +1 -1
- {airbyte_source_shopify-3.0.8.dev202509081459.dist-info → airbyte_source_shopify-3.1.0.dist-info}/RECORD +10 -9
- source_shopify/schemas/deleted_products.json +27 -0
- source_shopify/scopes.py +1 -0
- source_shopify/shopify_graphql/bulk/job.py +6 -1
- source_shopify/source.py +2 -0
- source_shopify/streams/base_streams.py +6 -3
- source_shopify/streams/streams.py +111 -0
- {airbyte_source_shopify-3.0.8.dev202509081459.dist-info → airbyte_source_shopify-3.1.0.dist-info}/WHEEL +0 -0
- {airbyte_source_shopify-3.0.8.dev202509081459.dist-info → airbyte_source_shopify-3.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
64
|
-
source_shopify/streams/streams.py,sha256=
|
|
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.
|
|
68
|
-
airbyte_source_shopify-3.0.
|
|
69
|
-
airbyte_source_shopify-3.0.
|
|
70
|
-
airbyte_source_shopify-3.0.
|
|
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
|
|
File without changes
|
|
File without changes
|