airbyte-source-shopify 2.7.0.dev202503201259__py3-none-any.whl → 3.0.1__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.7.0.dev202503201259.dist-info → airbyte_source_shopify-3.0.1.dist-info}/METADATA +1 -1
- {airbyte_source_shopify-2.7.0.dev202503201259.dist-info → airbyte_source_shopify-3.0.1.dist-info}/RECORD +14 -13
- {airbyte_source_shopify-2.7.0.dev202503201259.dist-info → airbyte_source_shopify-3.0.1.dist-info}/WHEEL +1 -1
- source_shopify/schemas/countries.json +7 -19
- source_shopify/schemas/orders.json +38 -0
- source_shopify/schemas/product_variants.json +0 -8
- source_shopify/schemas/profile_location_groups.json +10 -0
- source_shopify/scopes.py +2 -1
- source_shopify/shopify_graphql/bulk/job.py +14 -4
- source_shopify/shopify_graphql/bulk/query.py +176 -4
- source_shopify/source.py +2 -1
- source_shopify/streams/base_streams.py +29 -3
- source_shopify/streams/streams.py +93 -3
- {airbyte_source_shopify-2.7.0.dev202503201259.dist-info → airbyte_source_shopify-3.0.1.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.1.dist-info/METADATA,sha256=H0AnLS1mNv6FYq0zzX5F0N8FlAPMMHWe21NlbAHLMHo,5309
|
|
68
|
+
airbyte_source_shopify-3.0.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
69
|
+
airbyte_source_shopify-3.0.1.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
|
|
70
|
+
airbyte_source_shopify-3.0.1.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"]
|
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
|
|
@@ -417,18 +428,17 @@ class ShopifyBulkManager:
|
|
|
417
428
|
Error example:
|
|
418
429
|
[
|
|
419
430
|
{
|
|
431
|
+
'code': 'OPERATION_IN_PROGRESS',
|
|
420
432
|
'field': None,
|
|
421
433
|
'message': 'A bulk query operation for this app and shop is already in progress: gid://shopify/BulkOperation/4039184154813.',
|
|
422
434
|
}
|
|
423
435
|
]
|
|
424
436
|
"""
|
|
425
|
-
|
|
426
|
-
concurrent_job_pattern = "A bulk query operation for this app and shop is already in progress"
|
|
427
437
|
# the errors are handled in `job_job_check_for_errors`
|
|
428
438
|
if errors:
|
|
429
439
|
for error in errors:
|
|
430
|
-
|
|
431
|
-
if
|
|
440
|
+
error_code = error.get("code", "") if isinstance(error, dict) else ""
|
|
441
|
+
if error_code == BulkOperationUserErrorCode.OPERATION_IN_PROGRESS.value:
|
|
432
442
|
return True
|
|
433
443
|
return False
|
|
434
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
|
}
|
|
@@ -2658,7 +2660,7 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2658
2660
|
inventory_item_fields = [
|
|
2659
2661
|
Field(name="id", alias="inventory_item_id"),
|
|
2660
2662
|
Field(name="tracked", alias="tracked"),
|
|
2661
|
-
Field(name="requiresShipping", alias="
|
|
2663
|
+
Field(name="requiresShipping", alias="requires_shipping"),
|
|
2662
2664
|
Field(name="measurement", alias="measurement", fields=measurement_fields),
|
|
2663
2665
|
]
|
|
2664
2666
|
query_nodes: List[Field] = [
|
|
@@ -2748,13 +2750,11 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2748
2750
|
record["weight"] = measurement_weight.get("value") if measurement_weight.get("value") else 0.0
|
|
2749
2751
|
record["weight_unit"] = measurement_weight.get("unit") if measurement_weight else None
|
|
2750
2752
|
record["tracked"] = inventory_item.get("tracked") if inventory_item else None
|
|
2751
|
-
record["requires_shipping"] = inventory_item.get("
|
|
2753
|
+
record["requires_shipping"] = inventory_item.get("requires_shipping") if inventory_item else None
|
|
2752
2754
|
record["image_id"] = self._unnest_and_resolve_id(record, "image", "image_id")
|
|
2753
2755
|
image = record.get("image", {})
|
|
2754
2756
|
record["image_src"] = image.get("image_src") if image else None
|
|
2755
2757
|
record["image_url"] = image.get("image_url") if image else None
|
|
2756
|
-
# unnest `fulfillment_service` from `fulfillmentService`
|
|
2757
|
-
record["fulfillment_service"] = record.get("fulfillmentService", {}).get("fulfillment_service")
|
|
2758
2758
|
# cast the `price` to number, could be literally `None`
|
|
2759
2759
|
price = record.get("price")
|
|
2760
2760
|
record["price"] = float(price) if price else None
|
|
@@ -3201,3 +3201,175 @@ class OrderAgreement(ShopifyBulkQuery):
|
|
|
3201
3201
|
record["agreements"] = agreements_with_sales if agreements_with_sales else {}
|
|
3202
3202
|
|
|
3203
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
|
|
@@ -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
|