airbyte-source-shopify 2.6.7__py3-none-any.whl → 3.0.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-2.6.7.dist-info → airbyte_source_shopify-3.0.0.dist-info}/METADATA +1 -1
- {airbyte_source_shopify-2.6.7.dist-info → airbyte_source_shopify-3.0.0.dist-info}/RECORD +14 -13
- source_shopify/schemas/countries.json +7 -19
- source_shopify/schemas/orders.json +38 -0
- source_shopify/schemas/product_variants.json +4 -8
- source_shopify/schemas/profile_location_groups.json +10 -0
- source_shopify/scopes.py +2 -1
- source_shopify/shopify_graphql/bulk/job.py +16 -8
- source_shopify/shopify_graphql/bulk/query.py +191 -11
- source_shopify/source.py +2 -1
- source_shopify/streams/base_streams.py +30 -4
- source_shopify/streams/streams.py +93 -3
- {airbyte_source_shopify-2.6.7.dist-info → airbyte_source_shopify-3.0.0.dist-info}/WHEEL +0 -0
- {airbyte_source_shopify-2.6.7.dist-info → airbyte_source_shopify-3.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -9,7 +9,7 @@ source_shopify/schemas/balance_transactions.json,sha256=RAU7duUHTWS7nI0pochhTZt5
|
|
|
9
9
|
source_shopify/schemas/blogs.json,sha256=ciBS_3eCf4UJUaB0DPCVadeJR4W6ndq7N0JwykXp0RY,2151
|
|
10
10
|
source_shopify/schemas/collections.json,sha256=2iJMCyAn_yeMKsQVt7jGR3_u3N3CA8QQ6179QvRuwqY,1889
|
|
11
11
|
source_shopify/schemas/collects.json,sha256=dOX0_O7meWELWHYQG_MWqGkWLelAoiIlPtDXuxz9ig8,1173
|
|
12
|
-
source_shopify/schemas/countries.json,sha256=
|
|
12
|
+
source_shopify/schemas/countries.json,sha256=fdJPrd8tQEzzonkunm0hvMbqZeXixPaphHd3PSt8g58,1783
|
|
13
13
|
source_shopify/schemas/custom_collections.json,sha256=ElDY1y_G_VFPOGr9ipU022AZDWKB2LC8n8Cb6FGF8UM,3100
|
|
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
|
|
@@ -38,32 +38,33 @@ source_shopify/schemas/metafield_smart_collections.json,sha256=uk25Bxu5tATE5Cweo
|
|
|
38
38
|
source_shopify/schemas/order_agreements.json,sha256=LywW-Nyynoutitxtj4PukVNmaDknU0GAu4yxa0XQ56U,6821
|
|
39
39
|
source_shopify/schemas/order_refunds.json,sha256=TRja1SGCFYyf--T_M3-JGG0EnwwH50hwzH-RzbQ4tjY,26296
|
|
40
40
|
source_shopify/schemas/order_risks.json,sha256=6gK26TewYqT2KfF7rvMTEQODstom2SS0ViHNJESuB8k,10425
|
|
41
|
-
source_shopify/schemas/orders.json,sha256=
|
|
41
|
+
source_shopify/schemas/orders.json,sha256=SxX6fDtMnEkvm-58uNgaoF47Ep8DIwLpXK09C2MSNA0,105169
|
|
42
42
|
source_shopify/schemas/pages.json,sha256=J7jxtk2nE_q-0oHmGz8orFq3EfSjJqUvF6qPJtiZCrg,2093
|
|
43
43
|
source_shopify/schemas/price_rules.json,sha256=aZ-Re9Oc9g0ci8SPn0_ufvKPsGDBcTaXHVsq_9-Ygq4,7004
|
|
44
44
|
source_shopify/schemas/product_images.json,sha256=0l3ACfOfnXz8N8oMHwvvfZUFKeeuZ2DlRRu5-FxUhaI,1760
|
|
45
|
-
source_shopify/schemas/product_variants.json,sha256=
|
|
45
|
+
source_shopify/schemas/product_variants.json,sha256=hBtmB-OyVlmkQacY0s7Mpe-bMdYjx8mpwMqcGwN05kA,8121
|
|
46
46
|
source_shopify/schemas/products.json,sha256=n7m1ndEnAM7Pee5ca0JZWCl5Y0VffGmTZjqjHhiht1I,22609
|
|
47
|
+
source_shopify/schemas/profile_location_groups.json,sha256=2_QdfMeM_WNDT-06fQfFUHXjN9D6ndKIGcviJxEl8kM,189
|
|
47
48
|
source_shopify/schemas/shop.json,sha256=vEGiTvEYX7qnMq06MRVBycqih49h49xjTNC6gJuxTWs,8137
|
|
48
49
|
source_shopify/schemas/smart_collections.json,sha256=kv7dINsvgzJ0RyKfFNKjU0apdNDXwQaHfnNZfQsshcU,2009
|
|
49
50
|
source_shopify/schemas/tender_transactions.json,sha256=U8fdT-eflycEPzYSpBDiB0lp9wxmJHgioHTrICflh78,2006
|
|
50
51
|
source_shopify/schemas/transactions.json,sha256=vbwscH3UcAtbSsC70mBka4oNaFR4S3S6IFBmzR7t37U,10226
|
|
51
|
-
source_shopify/scopes.py,sha256=
|
|
52
|
+
source_shopify/scopes.py,sha256=N0njfMHn3Q1AQXuTj5VfjQOio10jaDarpC_oLYnWvqc,6490
|
|
52
53
|
source_shopify/shopify_graphql/bulk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
54
|
source_shopify/shopify_graphql/bulk/exceptions.py,sha256=4dj7Za4xIfwL-zf8joT9svF_RSoGlE3GviMiIl1e1rs,2532
|
|
54
|
-
source_shopify/shopify_graphql/bulk/job.py,sha256=
|
|
55
|
-
source_shopify/shopify_graphql/bulk/query.py,sha256=
|
|
55
|
+
source_shopify/shopify_graphql/bulk/job.py,sha256=0a3els2a5Fea-o30rkotq7yoPx56c481zwpZzxDajEw,27832
|
|
56
|
+
source_shopify/shopify_graphql/bulk/query.py,sha256=vo_ZryWIOaMYP0ZmEnL3KSPQJRD2cHuWG2lhYeg7f8c,130977
|
|
56
57
|
source_shopify/shopify_graphql/bulk/record.py,sha256=X6VGngugv7a_S8UEeDo121BkdCVLj5nWlHK76A21kyo,16898
|
|
57
58
|
source_shopify/shopify_graphql/bulk/retry.py,sha256=R5rSJJE8D5zcj6mN-OmmNO2aFZEIdjAlWclDDVW5KPI,2626
|
|
58
59
|
source_shopify/shopify_graphql/bulk/status.py,sha256=RmuQ2XsYL3iRCpVGxea9F1wXGmbwasDCSXjaTyL4LMA,328
|
|
59
60
|
source_shopify/shopify_graphql/bulk/tools.py,sha256=nUQ2ZmPTKJNJdfLToR6KJtLKcJFCChSifkAOvwg0Vss,4065
|
|
60
|
-
source_shopify/source.py,sha256=
|
|
61
|
+
source_shopify/source.py,sha256=txb3wIm-3xXd8-5QLSeu2TeHBSnppwy5PEIOEl40mVw,8517
|
|
61
62
|
source_shopify/spec.json,sha256=ITYWiQ-NrI5VISk5qmUQhp9ChUE2FV18d8xzVzPwvAg,6144
|
|
62
|
-
source_shopify/streams/base_streams.py,sha256
|
|
63
|
-
source_shopify/streams/streams.py,sha256=
|
|
63
|
+
source_shopify/streams/base_streams.py,sha256=FFIpHd5_-Z61W_jUucdr8D2MzUete1Y2E50bQDCLakE,41555
|
|
64
|
+
source_shopify/streams/streams.py,sha256=8LkM-SRhbGX2MwfHsjcWY62Z6g0jKZ0QfcS4B-vKPoM,13882
|
|
64
65
|
source_shopify/transform.py,sha256=mn0htL812_90zc_YszGQa0hHcIZQpYYdmk8IqpZm5TI,4685
|
|
65
66
|
source_shopify/utils.py,sha256=CAEKcxIroBo6kRFCyvC1bfOyfbGy7Z7seqZB7Eekl44,14209
|
|
66
|
-
airbyte_source_shopify-
|
|
67
|
-
airbyte_source_shopify-
|
|
68
|
-
airbyte_source_shopify-
|
|
69
|
-
airbyte_source_shopify-
|
|
67
|
+
airbyte_source_shopify-3.0.0.dist-info/METADATA,sha256=gpaUrJ35t5g2NxtE9jVYCJsx2lSzzjL7Xf_tTB_B-AQ,5309
|
|
68
|
+
airbyte_source_shopify-3.0.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
69
|
+
airbyte_source_shopify-3.0.0.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
|
|
70
|
+
airbyte_source_shopify-3.0.0.dist-info/RECORD,,
|
|
@@ -36,31 +36,19 @@
|
|
|
36
36
|
"description": "Name of the province.",
|
|
37
37
|
"type": ["null", "string"]
|
|
38
38
|
},
|
|
39
|
-
"
|
|
40
|
-
"description": "
|
|
41
|
-
"type": ["null", "number"]
|
|
42
|
-
},
|
|
43
|
-
"tax_name": {
|
|
44
|
-
"description": "Name of the tax applicable for the province.",
|
|
45
|
-
"type": ["null", "string"]
|
|
46
|
-
},
|
|
47
|
-
"tax_type": {
|
|
48
|
-
"description": "Type of tax (e.g., sales tax, VAT) applicable in the province.",
|
|
39
|
+
"translated_name": {
|
|
40
|
+
"description": "Translated name of the province.",
|
|
49
41
|
"type": ["null", "string"]
|
|
50
|
-
},
|
|
51
|
-
"tax_percentage": {
|
|
52
|
-
"description": "Percentage value of tax applicable in the province.",
|
|
53
|
-
"type": ["null", "number"]
|
|
54
42
|
}
|
|
55
43
|
}
|
|
56
44
|
}
|
|
57
45
|
},
|
|
58
|
-
"
|
|
59
|
-
"description": "
|
|
60
|
-
"type": ["null", "
|
|
46
|
+
"rest_of_world": {
|
|
47
|
+
"description": "Whether the country in a catch-all group of countries that are not individually listed or assigned to any specific zone or market.",
|
|
48
|
+
"type": ["null", "boolean"]
|
|
61
49
|
},
|
|
62
|
-
"
|
|
63
|
-
"description": "
|
|
50
|
+
"translated_name": {
|
|
51
|
+
"description": "Translated name of the country",
|
|
64
52
|
"type": ["null", "string"]
|
|
65
53
|
},
|
|
66
54
|
"shop_url": {
|
|
@@ -259,6 +259,10 @@
|
|
|
259
259
|
"description": "The ID of the device used to place the order",
|
|
260
260
|
"type": ["null", "string"]
|
|
261
261
|
},
|
|
262
|
+
"duties_included": {
|
|
263
|
+
"description": "Whether duties included in the subtotal price of the order.",
|
|
264
|
+
"type": ["null", "boolean"]
|
|
265
|
+
},
|
|
262
266
|
"discount_applications": {
|
|
263
267
|
"type": ["null", "array"],
|
|
264
268
|
"items": {
|
|
@@ -340,6 +344,10 @@
|
|
|
340
344
|
"description": "The app ID of the merchant of record",
|
|
341
345
|
"type": ["null", "string"]
|
|
342
346
|
},
|
|
347
|
+
"merchant_business_entity_id": {
|
|
348
|
+
"description": "The business entity ID of the merchant of record.",
|
|
349
|
+
"type": ["null", "string"]
|
|
350
|
+
},
|
|
343
351
|
"name": {
|
|
344
352
|
"description": "The name of the order",
|
|
345
353
|
"type": ["null", "string"]
|
|
@@ -418,6 +426,36 @@
|
|
|
418
426
|
"description": "The terms of payment for the order",
|
|
419
427
|
"type": ["null", "string"]
|
|
420
428
|
},
|
|
429
|
+
"total_cash_rounding_payment_adjustment_set": {
|
|
430
|
+
"description": "The money set that represents the amount adjusted on the order total due to cash rounding.",
|
|
431
|
+
"type": ["null", "object"],
|
|
432
|
+
"properties": {
|
|
433
|
+
"presentment_money": {
|
|
434
|
+
"description": "The amount in the customer's presentment currency.",
|
|
435
|
+
"type": ["null", "object"],
|
|
436
|
+
"properties": {
|
|
437
|
+
"amount": {
|
|
438
|
+
"type": ["null", "string"]
|
|
439
|
+
},
|
|
440
|
+
"currency_code": {
|
|
441
|
+
"type": ["null", "string"]
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
"shop_money": {
|
|
446
|
+
"description": "The amount in the shop's local currency.",
|
|
447
|
+
"type": ["null", "object"],
|
|
448
|
+
"properties": {
|
|
449
|
+
"amount": {
|
|
450
|
+
"type": ["null", "string"]
|
|
451
|
+
},
|
|
452
|
+
"currency_code": {
|
|
453
|
+
"type": ["null", "string"]
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
},
|
|
421
459
|
"phone": {
|
|
422
460
|
"description": "The phone number of the customer",
|
|
423
461
|
"type": ["null", "string"]
|
|
@@ -34,14 +34,6 @@
|
|
|
34
34
|
"description": "The original price of the variant before any discount",
|
|
35
35
|
"type": ["null", "string"]
|
|
36
36
|
},
|
|
37
|
-
"fulfillment_service": {
|
|
38
|
-
"description": "The fulfillment service for the variant",
|
|
39
|
-
"type": ["null", "string"]
|
|
40
|
-
},
|
|
41
|
-
"inventory_management": {
|
|
42
|
-
"description": "The method used to manage inventory for the variant",
|
|
43
|
-
"type": ["null", "string"]
|
|
44
|
-
},
|
|
45
37
|
"option1": {
|
|
46
38
|
"description": "The value for option 1 of the variant",
|
|
47
39
|
"type": ["null", "string"]
|
|
@@ -236,6 +228,10 @@
|
|
|
236
228
|
"tax_code": {
|
|
237
229
|
"description": "The tax code for the product variant.",
|
|
238
230
|
"type": ["null", "string"]
|
|
231
|
+
},
|
|
232
|
+
"tracked": {
|
|
233
|
+
"description": "Whether inventory levels are tracked for the item.",
|
|
234
|
+
"type": ["null", "boolean"]
|
|
239
235
|
}
|
|
240
236
|
}
|
|
241
237
|
}
|
source_shopify/scopes.py
CHANGED
|
@@ -72,12 +72,13 @@ SCOPES_MAPPING: Mapping[str, set[str]] = {
|
|
|
72
72
|
"MetafieldArticles": ("read_online_store_pages",),
|
|
73
73
|
"Blogs": ("read_online_store_pages",),
|
|
74
74
|
"MetafieldBlogs": ("read_online_store_pages",),
|
|
75
|
+
# SCOPE: read_shipping
|
|
76
|
+
"Countries": ("read_shipping",),
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
ALWAYS_PERMITTED_STREAMS: List[str] = [
|
|
78
80
|
"MetafieldShops",
|
|
79
81
|
"Shop",
|
|
80
|
-
"Countries",
|
|
81
82
|
]
|
|
82
83
|
|
|
83
84
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
7
8
|
from time import sleep, time
|
|
8
9
|
from typing import Any, Final, Iterable, List, Mapping, Optional
|
|
9
10
|
|
|
@@ -23,6 +24,16 @@ from .status import ShopifyBulkJobStatus
|
|
|
23
24
|
from .tools import END_OF_FILE, BulkTools
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
class BulkOperationUserErrorCode(Enum):
|
|
28
|
+
"""
|
|
29
|
+
Possible error codes that can be returned by BulkOperationUserError.
|
|
30
|
+
https://shopify.dev/docs/api/admin-graphql/latest/enums/BulkOperationUserErrorCode
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
INVALID = "INVALID"
|
|
34
|
+
OPERATION_IN_PROGRESS = "OPERATION_IN_PROGRESS"
|
|
35
|
+
|
|
36
|
+
|
|
26
37
|
@dataclass
|
|
27
38
|
class ShopifyBulkManager:
|
|
28
39
|
http_client: HttpClient
|
|
@@ -246,8 +257,7 @@ class ShopifyBulkManager:
|
|
|
246
257
|
_, canceled_response = self.http_client.send_request(
|
|
247
258
|
http_method="POST",
|
|
248
259
|
url=self.base_url,
|
|
249
|
-
|
|
250
|
-
headers={"Content-Type": "application/graphql"},
|
|
260
|
+
json={"query": ShopifyBulkTemplates.cancel(self._job_id)},
|
|
251
261
|
request_kwargs={},
|
|
252
262
|
)
|
|
253
263
|
# mark the job was self-canceled
|
|
@@ -405,8 +415,7 @@ class ShopifyBulkManager:
|
|
|
405
415
|
_, response = self.http_client.send_request(
|
|
406
416
|
http_method="POST",
|
|
407
417
|
url=self.base_url,
|
|
408
|
-
|
|
409
|
-
headers={"Content-Type": "application/graphql"},
|
|
418
|
+
json={"query": ShopifyBulkTemplates.status(self._job_id)},
|
|
410
419
|
request_kwargs={},
|
|
411
420
|
)
|
|
412
421
|
self._job_healthcheck(response)
|
|
@@ -419,18 +428,17 @@ class ShopifyBulkManager:
|
|
|
419
428
|
Error example:
|
|
420
429
|
[
|
|
421
430
|
{
|
|
431
|
+
'code': 'OPERATION_IN_PROGRESS',
|
|
422
432
|
'field': None,
|
|
423
433
|
'message': 'A bulk query operation for this app and shop is already in progress: gid://shopify/BulkOperation/4039184154813.',
|
|
424
434
|
}
|
|
425
435
|
]
|
|
426
436
|
"""
|
|
427
|
-
|
|
428
|
-
concurrent_job_pattern = "A bulk query operation for this app and shop is already in progress"
|
|
429
437
|
# the errors are handled in `job_job_check_for_errors`
|
|
430
438
|
if errors:
|
|
431
439
|
for error in errors:
|
|
432
|
-
|
|
433
|
-
if
|
|
440
|
+
error_code = error.get("code", "") if isinstance(error, dict) else ""
|
|
441
|
+
if error_code == BulkOperationUserErrorCode.OPERATION_IN_PROGRESS.value:
|
|
434
442
|
return True
|
|
435
443
|
return False
|
|
436
444
|
|
|
@@ -11,6 +11,7 @@ from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Union
|
|
|
11
11
|
|
|
12
12
|
from attr import dataclass
|
|
13
13
|
from graphql_query import Argument, Field, InlineFragment, Operation, Query
|
|
14
|
+
from setuptools.command.alias import alias
|
|
14
15
|
|
|
15
16
|
from .tools import BULK_PARENT_KEY, BulkTools
|
|
16
17
|
|
|
@@ -68,6 +69,7 @@ class ShopifyBulkTemplates:
|
|
|
68
69
|
createdAt
|
|
69
70
|
}
|
|
70
71
|
userErrors {
|
|
72
|
+
code
|
|
71
73
|
field
|
|
72
74
|
message
|
|
73
75
|
}
|
|
@@ -2652,7 +2654,15 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2652
2654
|
Field(name="src", alias="image_src"),
|
|
2653
2655
|
Field(name="url", alias="image_url"),
|
|
2654
2656
|
]
|
|
2655
|
-
|
|
2657
|
+
measurement_fields = [
|
|
2658
|
+
Field(name="weight", fields=["value", "unit"]),
|
|
2659
|
+
]
|
|
2660
|
+
inventory_item_fields = [
|
|
2661
|
+
Field(name="id", alias="inventory_item_id"),
|
|
2662
|
+
Field(name="tracked", alias="tracked"),
|
|
2663
|
+
Field(name="requiresShipping", alias="requires_shipping"),
|
|
2664
|
+
Field(name="measurement", alias="measurement", fields=measurement_fields),
|
|
2665
|
+
]
|
|
2656
2666
|
query_nodes: List[Field] = [
|
|
2657
2667
|
"__typename",
|
|
2658
2668
|
"id",
|
|
@@ -2662,25 +2672,19 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2662
2672
|
"position",
|
|
2663
2673
|
"inventoryPolicy",
|
|
2664
2674
|
"compareAtPrice",
|
|
2665
|
-
"inventoryManagement",
|
|
2666
2675
|
"createdAt",
|
|
2667
2676
|
"updatedAt",
|
|
2668
2677
|
"taxable",
|
|
2669
2678
|
"barcode",
|
|
2670
|
-
"weight",
|
|
2671
|
-
"weightUnit",
|
|
2672
2679
|
"inventoryQuantity",
|
|
2673
|
-
"requiresShipping",
|
|
2674
2680
|
"availableForSale",
|
|
2675
2681
|
"displayName",
|
|
2676
2682
|
"taxCode",
|
|
2677
2683
|
Field(name="selectedOptions", alias="options", fields=option_fields),
|
|
2678
|
-
Field(name="weight", alias="grams"),
|
|
2679
2684
|
Field(name="image", fields=image_fields),
|
|
2680
2685
|
Field(name="inventoryQuantity", alias="old_inventory_quantity"),
|
|
2681
2686
|
Field(name="product", fields=[Field(name="id", alias="product_id")]),
|
|
2682
|
-
Field(name="
|
|
2683
|
-
Field(name="inventoryItem", fields=[Field(name="id", alias="inventory_item_id")]),
|
|
2687
|
+
Field(name="inventoryItem", fields=inventory_item_fields),
|
|
2684
2688
|
] + presentment_prices
|
|
2685
2689
|
|
|
2686
2690
|
return query_nodes
|
|
@@ -2741,17 +2745,21 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2741
2745
|
# unnest mandatory fields from their placeholders
|
|
2742
2746
|
record["product_id"] = self._unnest_and_resolve_id(record, "product", "product_id")
|
|
2743
2747
|
record["inventory_item_id"] = self._unnest_and_resolve_id(record, "inventoryItem", "inventory_item_id")
|
|
2748
|
+
inventory_item = record.get("inventoryItem")
|
|
2749
|
+
measurement_weight = record.get("inventoryItem", {}).get("measurement", {}).get("weight")
|
|
2750
|
+
record["weight"] = measurement_weight.get("value") if measurement_weight.get("value") else 0.0
|
|
2751
|
+
record["weight_unit"] = measurement_weight.get("unit") if measurement_weight else None
|
|
2752
|
+
record["tracked"] = inventory_item.get("tracked") if inventory_item else None
|
|
2753
|
+
record["requires_shipping"] = inventory_item.get("requires_shipping") if inventory_item else None
|
|
2744
2754
|
record["image_id"] = self._unnest_and_resolve_id(record, "image", "image_id")
|
|
2745
2755
|
image = record.get("image", {})
|
|
2746
2756
|
record["image_src"] = image.get("image_src") if image else None
|
|
2747
2757
|
record["image_url"] = image.get("image_url") if image else None
|
|
2748
|
-
# unnest `fulfillment_service` from `fulfillmentService`
|
|
2749
|
-
record["fulfillment_service"] = record.get("fulfillmentService", {}).get("fulfillment_service")
|
|
2750
2758
|
# cast the `price` to number, could be literally `None`
|
|
2751
2759
|
price = record.get("price")
|
|
2752
2760
|
record["price"] = float(price) if price else None
|
|
2753
2761
|
# cast the `grams` to integer
|
|
2754
|
-
record["grams"] = int(record.get("
|
|
2762
|
+
record["grams"] = int(record.get("weight", 0))
|
|
2755
2763
|
# convert date-time cursors
|
|
2756
2764
|
record["createdAt"] = self.tools.from_iso8601_to_rfc3339(record, "createdAt")
|
|
2757
2765
|
record["updatedAt"] = self.tools.from_iso8601_to_rfc3339(record, "updatedAt")
|
|
@@ -3193,3 +3201,175 @@ class OrderAgreement(ShopifyBulkQuery):
|
|
|
3193
3201
|
record["agreements"] = agreements_with_sales if agreements_with_sales else {}
|
|
3194
3202
|
|
|
3195
3203
|
yield record
|
|
3204
|
+
|
|
3205
|
+
|
|
3206
|
+
class DeliveryZoneList:
|
|
3207
|
+
query_name = "deliveryProfiles"
|
|
3208
|
+
operation_name = "DeliveryZoneList"
|
|
3209
|
+
operation_type = "query"
|
|
3210
|
+
|
|
3211
|
+
query_nodes: List[str] = []
|
|
3212
|
+
|
|
3213
|
+
page_size = 100
|
|
3214
|
+
|
|
3215
|
+
def resolve(self, query: Query) -> str:
|
|
3216
|
+
# return the constructed query operation
|
|
3217
|
+
return Operation(type=self.operation_type, name=self.operation_name, queries=[query]).render()
|
|
3218
|
+
|
|
3219
|
+
def build(self, name: str, query_args: Mapping[str, Any] = None) -> Query:
|
|
3220
|
+
arguments = [Argument(name="first", value=self.page_size)]
|
|
3221
|
+
|
|
3222
|
+
if query_args:
|
|
3223
|
+
if query_args.get("cursor"):
|
|
3224
|
+
cursor = '"' + query_args["cursor"] + '"'
|
|
3225
|
+
arguments.append(Argument(name="after", value=cursor))
|
|
3226
|
+
|
|
3227
|
+
query = Query(name=name, arguments=arguments, fields=self.query_nodes)
|
|
3228
|
+
# return constructed query
|
|
3229
|
+
return query
|
|
3230
|
+
|
|
3231
|
+
def query(self, query_args: Mapping[str, Any] = None) -> Query:
|
|
3232
|
+
return self.build(self.query_name, query_args)
|
|
3233
|
+
|
|
3234
|
+
def get(self, query_args: Mapping[str, Any] = None) -> str:
|
|
3235
|
+
query: Query = self.query(query_args)
|
|
3236
|
+
return self.resolve(query)
|
|
3237
|
+
|
|
3238
|
+
|
|
3239
|
+
class ProfileLocationGroups(ShopifyBulkQuery):
|
|
3240
|
+
query_name = "deliveryProfiles"
|
|
3241
|
+
filter_field = None
|
|
3242
|
+
|
|
3243
|
+
record_composition = {"new_record": "DeliveryProfile"}
|
|
3244
|
+
|
|
3245
|
+
query_nodes: List[Field] = [
|
|
3246
|
+
"__typename",
|
|
3247
|
+
Field(
|
|
3248
|
+
name="profileLocationGroups",
|
|
3249
|
+
fields=[Field(name="locationGroup", fields=["id"])],
|
|
3250
|
+
),
|
|
3251
|
+
]
|
|
3252
|
+
|
|
3253
|
+
|
|
3254
|
+
class DeliveryProfile(DeliveryZoneList):
|
|
3255
|
+
"""
|
|
3256
|
+
query DeliveryZoneList {
|
|
3257
|
+
deliveryProfiles(
|
|
3258
|
+
first: 1
|
|
3259
|
+
) {
|
|
3260
|
+
pageInfo {
|
|
3261
|
+
hasNextPage
|
|
3262
|
+
endCursor
|
|
3263
|
+
}
|
|
3264
|
+
nodes {
|
|
3265
|
+
profileLocationGroups(
|
|
3266
|
+
locationGroupId: "<locationGroupId>"
|
|
3267
|
+
) {
|
|
3268
|
+
locationGroupZones(
|
|
3269
|
+
first: 100
|
|
3270
|
+
) {
|
|
3271
|
+
nodes {
|
|
3272
|
+
zone {
|
|
3273
|
+
id
|
|
3274
|
+
name
|
|
3275
|
+
countries {
|
|
3276
|
+
id
|
|
3277
|
+
name
|
|
3278
|
+
translatedName
|
|
3279
|
+
code {
|
|
3280
|
+
countryCode
|
|
3281
|
+
restOfWorld
|
|
3282
|
+
}
|
|
3283
|
+
provinces {
|
|
3284
|
+
id
|
|
3285
|
+
translatedName
|
|
3286
|
+
name
|
|
3287
|
+
code
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
pageInfo {
|
|
3293
|
+
hasNextPage
|
|
3294
|
+
endCursor
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
"""
|
|
3302
|
+
|
|
3303
|
+
page_size = 1
|
|
3304
|
+
sub_page_size = 100
|
|
3305
|
+
|
|
3306
|
+
def __init__(self, location_group_id: str, location_group_zones_cursor: str = None):
|
|
3307
|
+
self.location_group_id = location_group_id
|
|
3308
|
+
self.location_group_zones_cursor = location_group_zones_cursor
|
|
3309
|
+
|
|
3310
|
+
@property
|
|
3311
|
+
def query_nodes(self) -> Optional[Union[List[Field], List[str]]]:
|
|
3312
|
+
location_group_id = '"' + self.location_group_id + '"'
|
|
3313
|
+
location_group_zones_arguments = [Argument(name="first", value=self.sub_page_size)]
|
|
3314
|
+
if self.location_group_zones_cursor:
|
|
3315
|
+
cursor = '"' + self.location_group_zones_cursor + '"'
|
|
3316
|
+
location_group_zones_arguments.append(Argument(name="after", value=cursor))
|
|
3317
|
+
|
|
3318
|
+
query_nodes: List[Field] = [
|
|
3319
|
+
Field(name="pageInfo", fields=["hasNextPage", "endCursor"]),
|
|
3320
|
+
Field(
|
|
3321
|
+
name="nodes",
|
|
3322
|
+
fields=[
|
|
3323
|
+
Field(
|
|
3324
|
+
name="profileLocationGroups",
|
|
3325
|
+
arguments=[Argument(name="locationGroupId", value=location_group_id)],
|
|
3326
|
+
fields=[
|
|
3327
|
+
Field(
|
|
3328
|
+
name="locationGroupZones",
|
|
3329
|
+
arguments=location_group_zones_arguments,
|
|
3330
|
+
fields=[
|
|
3331
|
+
Field(
|
|
3332
|
+
name="nodes",
|
|
3333
|
+
fields=[
|
|
3334
|
+
Field(
|
|
3335
|
+
name="zone",
|
|
3336
|
+
fields=[
|
|
3337
|
+
"id",
|
|
3338
|
+
"name",
|
|
3339
|
+
Field(
|
|
3340
|
+
name="countries",
|
|
3341
|
+
fields=[
|
|
3342
|
+
"id",
|
|
3343
|
+
"name",
|
|
3344
|
+
Field(name="translatedName", alias="translated_name"),
|
|
3345
|
+
Field(
|
|
3346
|
+
name="code",
|
|
3347
|
+
fields=[
|
|
3348
|
+
Field(name="countryCode", alias="country_code"),
|
|
3349
|
+
Field(name="restOfWorld", alias="rest_of_world"),
|
|
3350
|
+
],
|
|
3351
|
+
),
|
|
3352
|
+
Field(
|
|
3353
|
+
name="provinces",
|
|
3354
|
+
fields=[
|
|
3355
|
+
"id",
|
|
3356
|
+
"name",
|
|
3357
|
+
"code",
|
|
3358
|
+
Field(name="translatedName", alias="translated_name"),
|
|
3359
|
+
],
|
|
3360
|
+
),
|
|
3361
|
+
],
|
|
3362
|
+
),
|
|
3363
|
+
],
|
|
3364
|
+
)
|
|
3365
|
+
],
|
|
3366
|
+
),
|
|
3367
|
+
Field(name="pageInfo", fields=["hasNextPage", "endCursor"]),
|
|
3368
|
+
],
|
|
3369
|
+
),
|
|
3370
|
+
],
|
|
3371
|
+
),
|
|
3372
|
+
],
|
|
3373
|
+
),
|
|
3374
|
+
]
|
|
3375
|
+
return query_nodes
|
source_shopify/source.py
CHANGED
|
@@ -57,6 +57,7 @@ from .streams.streams import (
|
|
|
57
57
|
ProductImages,
|
|
58
58
|
Products,
|
|
59
59
|
ProductVariants,
|
|
60
|
+
ProfileLocationGroups,
|
|
60
61
|
Shop,
|
|
61
62
|
SmartCollections,
|
|
62
63
|
TenderTransactions,
|
|
@@ -216,7 +217,7 @@ class SourceShopify(AbstractSource):
|
|
|
216
217
|
TenderTransactions(config),
|
|
217
218
|
self.select_transactions_stream(config),
|
|
218
219
|
CustomerAddress(config),
|
|
219
|
-
Countries(config),
|
|
220
|
+
Countries(config=config, parent=ProfileLocationGroups(config)),
|
|
220
221
|
]
|
|
221
222
|
|
|
222
223
|
return [
|
|
@@ -15,10 +15,10 @@ import requests
|
|
|
15
15
|
from requests.exceptions import RequestException
|
|
16
16
|
from source_shopify.http_request import ShopifyErrorHandler
|
|
17
17
|
from source_shopify.shopify_graphql.bulk.job import ShopifyBulkManager
|
|
18
|
-
from source_shopify.shopify_graphql.bulk.query import ShopifyBulkQuery
|
|
18
|
+
from source_shopify.shopify_graphql.bulk.query import DeliveryZoneList, ShopifyBulkQuery
|
|
19
19
|
from source_shopify.transform import DataTypeEnforcer
|
|
20
|
+
from source_shopify.utils import ApiTypeEnum, ShopifyNonRetryableErrors
|
|
20
21
|
from source_shopify.utils import EagerlyCachedStreamState as stream_state_cache
|
|
21
|
-
from source_shopify.utils import ShopifyNonRetryableErrors
|
|
22
22
|
from source_shopify.utils import ShopifyRateLimiter as limiter
|
|
23
23
|
|
|
24
24
|
from airbyte_cdk.models import SyncMode
|
|
@@ -33,7 +33,7 @@ class ShopifyStream(HttpStream, ABC):
|
|
|
33
33
|
logger = logging.getLogger("airbyte")
|
|
34
34
|
|
|
35
35
|
# Latest Stable Release
|
|
36
|
-
api_version = "
|
|
36
|
+
api_version = "2025-01"
|
|
37
37
|
# Page size
|
|
38
38
|
limit = 250
|
|
39
39
|
|
|
@@ -89,7 +89,7 @@ class ShopifyStream(HttpStream, ABC):
|
|
|
89
89
|
records = json_response.get(self.data_field, []) if self.data_field is not None else json_response
|
|
90
90
|
yield from self.produce_records(records)
|
|
91
91
|
except RequestException as e:
|
|
92
|
-
self.logger.warning(f"Unexpected error in `
|
|
92
|
+
self.logger.warning(f"Unexpected error in `parse_response`: {e}, the actual response data: {response.text}")
|
|
93
93
|
yield {}
|
|
94
94
|
|
|
95
95
|
def produce_records(
|
|
@@ -855,3 +855,29 @@ class IncrementalShopifyGraphQlBulkStream(IncrementalShopifyStream):
|
|
|
855
855
|
yield from self.filter_records_newer_than_state(stream_state, self.sort_output_asc(records))
|
|
856
856
|
# add log message about the checkpoint value
|
|
857
857
|
self.emit_checkpoint_message()
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
class FullRefreshShopifyGraphQlBulkStream(ShopifyStream):
|
|
861
|
+
data_field = "graphql"
|
|
862
|
+
http_method = "POST"
|
|
863
|
+
|
|
864
|
+
query: DeliveryZoneList
|
|
865
|
+
response_field: str
|
|
866
|
+
|
|
867
|
+
def request_body_json(
|
|
868
|
+
self,
|
|
869
|
+
stream_state: Optional[Mapping[str, Any]],
|
|
870
|
+
stream_slice: Optional[Mapping[str, Any]] = None,
|
|
871
|
+
next_page_token: Optional[Mapping[str, Any]] = None,
|
|
872
|
+
) -> Optional[Mapping[str, Any]]:
|
|
873
|
+
return {"query": self.query().get()}
|
|
874
|
+
|
|
875
|
+
@limiter.balance_rate_limit(api_type=ApiTypeEnum.graphql.value)
|
|
876
|
+
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
|
877
|
+
if response.status_code is requests.codes.OK:
|
|
878
|
+
try:
|
|
879
|
+
json_response = response.json().get("data", {}).get(self.response_field, {}).get("nodes", [])
|
|
880
|
+
yield from json_response
|
|
881
|
+
except RequestException as e:
|
|
882
|
+
self.logger.warning(f"Unexpected error in `parse_response`: {e}, the actual response data: {response.text}")
|
|
883
|
+
yield {}
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
#
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
from typing import Any, Mapping, MutableMapping
|
|
6
|
+
from typing import Any, Iterable, Mapping, MutableMapping, Optional
|
|
7
7
|
|
|
8
|
+
import requests
|
|
8
9
|
from source_shopify.shopify_graphql.bulk.query import (
|
|
9
10
|
Collection,
|
|
10
11
|
CustomerAddresses,
|
|
11
12
|
CustomerJourney,
|
|
13
|
+
DeliveryProfile,
|
|
12
14
|
DiscountCode,
|
|
13
15
|
FulfillmentOrder,
|
|
14
16
|
InventoryItem,
|
|
@@ -26,13 +28,16 @@ from source_shopify.shopify_graphql.bulk.query import (
|
|
|
26
28
|
Product,
|
|
27
29
|
ProductImage,
|
|
28
30
|
ProductVariant,
|
|
31
|
+
ProfileLocationGroups,
|
|
29
32
|
Transaction,
|
|
30
33
|
)
|
|
31
34
|
|
|
35
|
+
from airbyte_cdk import HttpSubStream
|
|
32
36
|
from airbyte_cdk.sources.streams.core import package_name_from_class
|
|
33
37
|
from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader
|
|
34
38
|
|
|
35
39
|
from .base_streams import (
|
|
40
|
+
FullRefreshShopifyGraphQlBulkStream,
|
|
36
41
|
IncrementalShopifyGraphQlBulkStream,
|
|
37
42
|
IncrementalShopifyNestedStream,
|
|
38
43
|
IncrementalShopifyStream,
|
|
@@ -327,5 +332,90 @@ class CustomerAddress(IncrementalShopifyGraphQlBulkStream):
|
|
|
327
332
|
cursor_field = "id"
|
|
328
333
|
|
|
329
334
|
|
|
330
|
-
class
|
|
331
|
-
|
|
335
|
+
class ProfileLocationGroups(IncrementalShopifyGraphQlBulkStream):
|
|
336
|
+
bulk_query: ProfileLocationGroups = ProfileLocationGroups
|
|
337
|
+
filter_field = None
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class Countries(HttpSubStream, FullRefreshShopifyGraphQlBulkStream):
|
|
341
|
+
# https://shopify.dev/docs/api/admin-graphql/latest/queries/deliveryProfiles
|
|
342
|
+
_page_cursor = None
|
|
343
|
+
_sub_page_cursor = None
|
|
344
|
+
|
|
345
|
+
_synced_countries_ids = []
|
|
346
|
+
|
|
347
|
+
query = DeliveryProfile
|
|
348
|
+
response_field = "deliveryProfiles"
|
|
349
|
+
|
|
350
|
+
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
|
|
351
|
+
json_response = response.json().get("data", {})
|
|
352
|
+
if not json_response:
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
page_info = json_response.get("deliveryProfiles", {}).get("pageInfo", {})
|
|
356
|
+
|
|
357
|
+
sub_page_info = {"hasNextPage": False}
|
|
358
|
+
# only one top page in query
|
|
359
|
+
delivery_profiles_nodes = json_response.get("deliveryProfiles", {}).get("nodes")
|
|
360
|
+
if delivery_profiles_nodes:
|
|
361
|
+
profile_location_groups = delivery_profiles_nodes[0].get("profileLocationGroups")
|
|
362
|
+
if profile_location_groups:
|
|
363
|
+
sub_page_info = (
|
|
364
|
+
# only first one
|
|
365
|
+
profile_location_groups[0].get("locationGroupZones", {}).get("pageInfo", {})
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
if not sub_page_info["hasNextPage"] and not page_info["hasNextPage"]:
|
|
369
|
+
return None
|
|
370
|
+
if sub_page_info["hasNextPage"]:
|
|
371
|
+
# The cursor to retrieve nodes after in the connection. Typically, you should pass the endCursor of the previous page as after.
|
|
372
|
+
self._sub_page_cursor = sub_page_info["endCursor"]
|
|
373
|
+
if page_info["hasNextPage"] and not sub_page_info["hasNextPage"]:
|
|
374
|
+
# The cursor to retrieve nodes after in the connection. Typically, you should pass the endCursor of the previous page as after.
|
|
375
|
+
self._page_cursor = page_info["endCursor"]
|
|
376
|
+
self._sub_page_cursor = None
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"cursor": self._page_cursor,
|
|
380
|
+
"sub_cursor": self._sub_page_cursor,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
def request_body_json(
|
|
384
|
+
self,
|
|
385
|
+
stream_state: Optional[Mapping[str, Any]],
|
|
386
|
+
stream_slice: Optional[Mapping[str, Any]] = None,
|
|
387
|
+
next_page_token: Optional[Mapping[str, Any]] = None,
|
|
388
|
+
) -> Optional[Mapping[str, Any]]:
|
|
389
|
+
location_group_id = stream_slice["parent"]["profile_location_groups"][0]["locationGroup"]["id"]
|
|
390
|
+
return {
|
|
391
|
+
"query": self.query(location_group_id=location_group_id, location_group_zones_cursor=self._sub_page_cursor).get(
|
|
392
|
+
query_args={
|
|
393
|
+
"cursor": self._page_cursor,
|
|
394
|
+
}
|
|
395
|
+
),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]:
|
|
399
|
+
# TODO: to refactor this using functools + partial, see comment https://github.com/airbytehq/airbyte/pull/55823#discussion_r2016335672
|
|
400
|
+
for node in super().parse_response(response, **kwargs):
|
|
401
|
+
for location_group in node.get("profileLocationGroups", []):
|
|
402
|
+
for location_group_zone in location_group.get("locationGroupZones", {}).get("nodes", []):
|
|
403
|
+
for country in location_group_zone.get("zone", {}).get("countries"):
|
|
404
|
+
country = self._process_country(country)
|
|
405
|
+
if country["id"] not in self._synced_countries_ids:
|
|
406
|
+
self._synced_countries_ids.append(country["id"])
|
|
407
|
+
yield country
|
|
408
|
+
|
|
409
|
+
def _process_country(self, country: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
410
|
+
country["id"] = int(country["id"].split("/")[-1])
|
|
411
|
+
|
|
412
|
+
for province in country.get("provinces", []):
|
|
413
|
+
province["id"] = int(province["id"].split("/")[-1])
|
|
414
|
+
province["country_id"] = country["id"]
|
|
415
|
+
|
|
416
|
+
if country.get("code"):
|
|
417
|
+
country["rest_of_world"] = country["code"]["rest_of_world"] if country["code"].get("rest_of_world") is not None else "*"
|
|
418
|
+
country["code"] = country["code"]["country_code"] if country["code"].get("country_code") is not None else "*"
|
|
419
|
+
|
|
420
|
+
country["shop_url"] = self.config["shop"]
|
|
421
|
+
return country
|
|
File without changes
|
{airbyte_source_shopify-2.6.7.dist-info → airbyte_source_shopify-3.0.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|