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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: airbyte-source-shopify
3
- Version: 2.7.0.dev202503201259
3
+ Version: 3.0.1
4
4
  Summary: Source CDK implementation for Shopify.
5
5
  License: ELv2
6
6
  Author: Airbyte
@@ -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=HrDHbtuc1WHyAzKVMqllTzcYdDmPQ2kwnEwY3MjTgyg,2161
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=DbipJlJqW2lzuGF2ce4JLRa4H491kVVAWBsoh8YcV50,103922
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=aTubzg5lhK52kyxzy_2pD1xOAkE4qOGgOoDUsbvOJY8,8400
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=UVDERXNismYSnRaXwUXcn8x7Z6EtoNKOHts0Vs_Kl2I,6443
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=UsYeXhjNJNHTv7su7p66rJ3BC5cNJkj3c9Iyuc2klbg,27535
55
- source_shopify/shopify_graphql/bulk/query.py,sha256=4DtHwa5AaJyHP28EnqGdRH3TV68JyrN74yJmEfQehw0,124455
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=WmtSMHMop9NvREj0VUR6P8qWYWyv21y6rFk42bPBzAo,8445
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=UMbGFMDWVdkKR3d7-JkR5GbaIJVGaWiMpWnaWefWFNE,40491
63
- source_shopify/streams/streams.py,sha256=UoWe89U5xfM2jdgCpWqRv5S4tXGM7GTMU0gq8nCr9oU,9618
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-2.7.0.dev202503201259.dist-info/METADATA,sha256=J1czuCYa9LmfwyNo3UxaA3VS8L9SPzQ7--kjQmUW-GQ,5325
67
- airbyte_source_shopify-2.7.0.dev202503201259.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
68
- airbyte_source_shopify-2.7.0.dev202503201259.dist-info/entry_points.txt,sha256=SyTwKSsPk9MCdPf01saWpnp8hcmZOgBssVcSIvMbBeQ,57
69
- airbyte_source_shopify-2.7.0.dev202503201259.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -36,31 +36,19 @@
36
36
  "description": "Name of the province.",
37
37
  "type": ["null", "string"]
38
38
  },
39
- "tax": {
40
- "description": "Tax information for the province.",
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
- "tax": {
59
- "description": "Overall tax information for the country.",
60
- "type": ["null", "number"]
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
- "tax_name": {
63
- "description": "Name of the tax applicable for the country.",
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"]
@@ -0,0 +1,10 @@
1
+ {
2
+ "type": ["null", "object"],
3
+ "additionalProperties": true,
4
+ "properties": {
5
+ "id": {
6
+ "description": "ID of the location group.",
7
+ "type": ["null", "string"]
8
+ }
9
+ }
10
+ }
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
- message = error.get("message", "") if isinstance(error, dict) else ""
431
- if concurrent_job_pattern in message:
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="requiresShipping"),
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("requiresShipping") if inventory_item else None
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 `parse_ersponse`: {e}, the actual response data: {response.text}")
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 Countries(ShopifyStream):
331
- data_field = "countries"
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