airbyte-source-shopify 2.4.14.dev202407181247__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-2.4.14.dev202407181247.dist-info → airbyte_source_shopify-3.1.0.dist-info}/METADATA +4 -4
- {airbyte_source_shopify-2.4.14.dev202407181247.dist-info → airbyte_source_shopify-3.1.0.dist-info}/RECORD +25 -27
- {airbyte_source_shopify-2.4.14.dev202407181247.dist-info → airbyte_source_shopify-3.1.0.dist-info}/WHEEL +1 -1
- source_shopify/auth.py +0 -1
- source_shopify/config_migrations.py +4 -1
- source_shopify/http_request.py +4 -2
- source_shopify/schemas/countries.json +7 -19
- source_shopify/schemas/customer_journey_summary.json +228 -148
- source_shopify/schemas/deleted_products.json +27 -0
- source_shopify/schemas/orders.json +38 -0
- source_shopify/schemas/product_variants.json +26 -8
- source_shopify/schemas/profile_location_groups.json +10 -0
- source_shopify/scopes.py +7 -6
- source_shopify/shopify_graphql/bulk/exceptions.py +6 -1
- source_shopify/shopify_graphql/bulk/job.py +173 -65
- source_shopify/shopify_graphql/bulk/query.py +440 -88
- source_shopify/shopify_graphql/bulk/record.py +260 -29
- source_shopify/shopify_graphql/bulk/retry.py +12 -12
- source_shopify/shopify_graphql/bulk/tools.py +17 -2
- source_shopify/source.py +6 -10
- source_shopify/spec.json +11 -5
- source_shopify/streams/base_streams.py +181 -54
- source_shopify/streams/streams.py +211 -58
- source_shopify/utils.py +47 -12
- source_shopify/schemas/customer_saved_search.json +0 -32
- source_shopify/schemas/products_graph_ql.json +0 -123
- source_shopify/shopify_graphql/graphql.py +0 -64
- source_shopify/shopify_graphql/schema.py +0 -29442
- {airbyte_source_shopify-2.4.14.dev202407181247.dist-info → airbyte_source_shopify-3.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
}
|
|
@@ -79,7 +81,12 @@ class ShopifyBulkTemplates:
|
|
|
79
81
|
|
|
80
82
|
@dataclass
|
|
81
83
|
class ShopifyBulkQuery:
|
|
82
|
-
|
|
84
|
+
config: Mapping[str, Any]
|
|
85
|
+
parent_stream_cursor_alias: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def shop_id(self) -> int:
|
|
89
|
+
return self.config.get("shop_id")
|
|
83
90
|
|
|
84
91
|
@property
|
|
85
92
|
def tools(self) -> BulkTools:
|
|
@@ -112,6 +119,14 @@ class ShopifyBulkQuery:
|
|
|
112
119
|
"""
|
|
113
120
|
return None
|
|
114
121
|
|
|
122
|
+
@property
|
|
123
|
+
def supports_checkpointing(self) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
The presence of `sort_key = "UPDATED_AT"` for a query instance, usually means,
|
|
126
|
+
the server-side BULK Job results are fetched and ordered correctly, suitable for checkpointing.
|
|
127
|
+
"""
|
|
128
|
+
return self.sort_key == "UPDATED_AT"
|
|
129
|
+
|
|
115
130
|
@property
|
|
116
131
|
def query_nodes(self) -> Optional[Union[List[Field], List[str]]]:
|
|
117
132
|
"""
|
|
@@ -120,6 +135,12 @@ class ShopifyBulkQuery:
|
|
|
120
135
|
"""
|
|
121
136
|
return ["__typename", "id"]
|
|
122
137
|
|
|
138
|
+
def inject_parent_cursor_field(self, nodes: List[Field], key: str = "updatedAt", index: int = 2) -> List[Field]:
|
|
139
|
+
if self.parent_stream_cursor_alias:
|
|
140
|
+
# inject parent cursor key as alias to the `updatedAt` parent cursor field
|
|
141
|
+
nodes.insert(index, Field(name=key, alias=self.parent_stream_cursor_alias))
|
|
142
|
+
return nodes
|
|
143
|
+
|
|
123
144
|
def get(self, filter_field: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None) -> str:
|
|
124
145
|
# define filter query string, if passed
|
|
125
146
|
filter_query = f"{filter_field}:>='{start}' AND {filter_field}:<='{end}'" if filter_field else None
|
|
@@ -191,7 +212,7 @@ class MetafieldType(Enum):
|
|
|
191
212
|
ORDERS = "orders"
|
|
192
213
|
DRAFT_ORDERS = "draftOrders"
|
|
193
214
|
PRODUCTS = "products"
|
|
194
|
-
PRODUCT_IMAGES =
|
|
215
|
+
PRODUCT_IMAGES = "products"
|
|
195
216
|
PRODUCT_VARIANTS = "productVariants"
|
|
196
217
|
COLLECTIONS = "collections"
|
|
197
218
|
LOCATIONS = "locations"
|
|
@@ -273,15 +294,22 @@ class Metafield(ShopifyBulkQuery):
|
|
|
273
294
|
List of available fields:
|
|
274
295
|
https://shopify.dev/docs/api/admin-graphql/unstable/objects/Metafield
|
|
275
296
|
"""
|
|
297
|
+
|
|
298
|
+
nodes = super().query_nodes
|
|
299
|
+
|
|
276
300
|
# define metafield node
|
|
277
301
|
metafield_node = self.get_edge_node("metafields", self.metafield_fields)
|
|
278
302
|
|
|
279
303
|
if isinstance(self.type.value, list):
|
|
280
|
-
|
|
304
|
+
nodes = [*nodes, self.get_edge_node(self.type.value[1], [*nodes, metafield_node])]
|
|
281
305
|
elif isinstance(self.type.value, str):
|
|
282
|
-
|
|
306
|
+
nodes = [*nodes, metafield_node]
|
|
283
307
|
|
|
284
|
-
|
|
308
|
+
nodes = self.inject_parent_cursor_field(nodes)
|
|
309
|
+
|
|
310
|
+
return nodes
|
|
311
|
+
|
|
312
|
+
def _process_metafield(self, record: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
|
|
285
313
|
# resolve parent id from `str` to `int`
|
|
286
314
|
record["owner_id"] = self.tools.resolve_str_id(record.get(BULK_PARENT_KEY))
|
|
287
315
|
# add `owner_resource` field
|
|
@@ -292,7 +320,25 @@ class Metafield(ShopifyBulkQuery):
|
|
|
292
320
|
record["createdAt"] = self.tools.from_iso8601_to_rfc3339(record, "createdAt")
|
|
293
321
|
record["updatedAt"] = self.tools.from_iso8601_to_rfc3339(record, "updatedAt")
|
|
294
322
|
record = self.tools.fields_names_to_snake_case(record)
|
|
295
|
-
|
|
323
|
+
return record
|
|
324
|
+
|
|
325
|
+
def _process_components(self, entity: List[dict]) -> Iterable[MutableMapping[str, Any]]:
|
|
326
|
+
for item in entity:
|
|
327
|
+
# resolve the id from string
|
|
328
|
+
item["admin_graphql_api_id"] = item.get("id")
|
|
329
|
+
item["id"] = self.tools.resolve_str_id(item.get("id"))
|
|
330
|
+
yield self._process_metafield(item)
|
|
331
|
+
|
|
332
|
+
def record_process_components(self, record: MutableMapping[str, Any]) -> Iterable[MutableMapping[str, Any]]:
|
|
333
|
+
# get the joined record components collected for the record
|
|
334
|
+
record_components = record.get("record_components", {})
|
|
335
|
+
# process record components
|
|
336
|
+
if not record_components:
|
|
337
|
+
yield self._process_metafield(record)
|
|
338
|
+
else:
|
|
339
|
+
metafields = record_components.get("Metafield", [])
|
|
340
|
+
if len(metafields) > 0:
|
|
341
|
+
yield from self._process_components(metafields)
|
|
296
342
|
|
|
297
343
|
|
|
298
344
|
class MetafieldCollection(Metafield):
|
|
@@ -331,7 +377,9 @@ class MetafieldCustomer(Metafield):
|
|
|
331
377
|
customers(query: "updated_at:>='2023-02-07T00:00:00+00:00' AND updated_at:<='2023-12-04T00:00:00+00:00'", sortKey: UPDATED_AT) {
|
|
332
378
|
edges {
|
|
333
379
|
node {
|
|
380
|
+
__typename
|
|
334
381
|
id
|
|
382
|
+
customer_updated_at: updatedAt
|
|
335
383
|
metafields {
|
|
336
384
|
edges {
|
|
337
385
|
node {
|
|
@@ -354,6 +402,11 @@ class MetafieldCustomer(Metafield):
|
|
|
354
402
|
|
|
355
403
|
type = MetafieldType.CUSTOMERS
|
|
356
404
|
|
|
405
|
+
record_composition = {
|
|
406
|
+
"new_record": "Customer",
|
|
407
|
+
"record_components": ["Metafield"],
|
|
408
|
+
}
|
|
409
|
+
|
|
357
410
|
|
|
358
411
|
class MetafieldLocation(Metafield):
|
|
359
412
|
"""
|
|
@@ -452,7 +505,9 @@ class MetafieldProduct(Metafield):
|
|
|
452
505
|
products(query: "updated_at:>='2023-02-07T00:00:00+00:00' AND updated_at:<='2023-12-04T00:00:00+00:00'", sortKey: UPDATED_AT) {
|
|
453
506
|
edges {
|
|
454
507
|
node {
|
|
508
|
+
__typename
|
|
455
509
|
id
|
|
510
|
+
product_updated_at: updatedAt
|
|
456
511
|
metafields {
|
|
457
512
|
edges {
|
|
458
513
|
node {
|
|
@@ -475,29 +530,40 @@ class MetafieldProduct(Metafield):
|
|
|
475
530
|
|
|
476
531
|
type = MetafieldType.PRODUCTS
|
|
477
532
|
|
|
533
|
+
record_composition = {
|
|
534
|
+
"new_record": "Product",
|
|
535
|
+
"record_components": ["Metafield"],
|
|
536
|
+
}
|
|
537
|
+
|
|
478
538
|
|
|
479
539
|
class MetafieldProductImage(Metafield):
|
|
480
540
|
"""
|
|
481
541
|
{
|
|
482
|
-
products(query: "updated_at:>='2023-
|
|
542
|
+
products(query: "updated_at:>='2023-01-08T00:00:00+00:00' AND updated_at:<='2024-08-02T15:12:41.689153+00:00'", sortKey: UPDATED_AT) {
|
|
483
543
|
edges {
|
|
484
544
|
node {
|
|
545
|
+
__typename
|
|
485
546
|
id
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
547
|
+
product_updated_at: updatedAt
|
|
548
|
+
media {
|
|
549
|
+
edges {
|
|
550
|
+
node {
|
|
551
|
+
__typename
|
|
489
552
|
id
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
553
|
+
... on MediaImage {
|
|
554
|
+
metafields {
|
|
555
|
+
edges {
|
|
556
|
+
node {
|
|
557
|
+
__typename
|
|
558
|
+
id
|
|
559
|
+
namespace
|
|
560
|
+
value
|
|
561
|
+
key
|
|
562
|
+
description
|
|
563
|
+
createdAt
|
|
564
|
+
updatedAt
|
|
565
|
+
type
|
|
566
|
+
}
|
|
501
567
|
}
|
|
502
568
|
}
|
|
503
569
|
}
|
|
@@ -512,6 +578,32 @@ class MetafieldProductImage(Metafield):
|
|
|
512
578
|
|
|
513
579
|
type = MetafieldType.PRODUCT_IMAGES
|
|
514
580
|
|
|
581
|
+
record_composition = {
|
|
582
|
+
"new_record": "Product",
|
|
583
|
+
"record_components": ["Metafield"],
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def query_nodes(self) -> List[Field]:
|
|
588
|
+
"""
|
|
589
|
+
This is the overide for the default `query_nodes` method,
|
|
590
|
+
because the usual way of retrieving the metafields for product images` was suddently deprecated,
|
|
591
|
+
for `2024-10`, but the changes are reflected in the `2024-04` as well, starting: `2024-08-01T00:06:44`
|
|
592
|
+
|
|
593
|
+
More info here:
|
|
594
|
+
https://shopify.dev/docs/api/release-notes/2024-04#productimage-value-removed
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
# define metafield node
|
|
598
|
+
metafield_node = self.get_edge_node("metafields", self.metafield_fields)
|
|
599
|
+
media_fields: List[Field] = ["__typename", "id", InlineFragment(type="MediaImage", fields=[metafield_node])]
|
|
600
|
+
media_node = self.get_edge_node("media", media_fields)
|
|
601
|
+
|
|
602
|
+
fields: List[Field] = ["__typename", "id", media_node]
|
|
603
|
+
fields = self.inject_parent_cursor_field(fields)
|
|
604
|
+
|
|
605
|
+
return fields
|
|
606
|
+
|
|
515
607
|
|
|
516
608
|
class MetafieldProductVariant(Metafield):
|
|
517
609
|
"""
|
|
@@ -1015,6 +1107,29 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1015
1107
|
term
|
|
1016
1108
|
}
|
|
1017
1109
|
}
|
|
1110
|
+
customerJourney {
|
|
1111
|
+
moments {
|
|
1112
|
+
... on CustomerVisit {
|
|
1113
|
+
id
|
|
1114
|
+
landingPage
|
|
1115
|
+
landingPageHtml
|
|
1116
|
+
occurredAt
|
|
1117
|
+
referralCode
|
|
1118
|
+
referralInfoHtml
|
|
1119
|
+
referrerUrl
|
|
1120
|
+
source
|
|
1121
|
+
sourceDescription
|
|
1122
|
+
sourceType
|
|
1123
|
+
utmParameters {
|
|
1124
|
+
campaign
|
|
1125
|
+
content
|
|
1126
|
+
medium
|
|
1127
|
+
source
|
|
1128
|
+
term
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1018
1133
|
}
|
|
1019
1134
|
}
|
|
1020
1135
|
}
|
|
@@ -1037,6 +1152,16 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1037
1152
|
"sourceDescription",
|
|
1038
1153
|
Field(name="utmParameters", fields=["campaign", "content", "medium", "source", "term"]),
|
|
1039
1154
|
]
|
|
1155
|
+
|
|
1156
|
+
customer_visit_fragment: List[InlineFragment] = [
|
|
1157
|
+
InlineFragment(type="CustomerVisit", fields=visit_fields),
|
|
1158
|
+
]
|
|
1159
|
+
|
|
1160
|
+
# # use this in the next version
|
|
1161
|
+
# moments_fields: List[Field] = [
|
|
1162
|
+
# Field(name="edges", fields=[Field(name="node", fields=customer_visit_fragment)]),
|
|
1163
|
+
# ]
|
|
1164
|
+
|
|
1040
1165
|
customer_journey_summary_fields: List[Field] = [
|
|
1041
1166
|
"ready",
|
|
1042
1167
|
Field(name="momentsCount", fields=["count", "precision"]),
|
|
@@ -1044,6 +1169,8 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1044
1169
|
"daysToConversion",
|
|
1045
1170
|
Field(name="firstVisit", fields=visit_fields),
|
|
1046
1171
|
Field(name="lastVisit", fields=visit_fields),
|
|
1172
|
+
# # use this in the next version
|
|
1173
|
+
# Field(name="moments", fields=moments_fields),
|
|
1047
1174
|
]
|
|
1048
1175
|
|
|
1049
1176
|
query_nodes: List[Field] = [
|
|
@@ -1052,6 +1179,12 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1052
1179
|
"createdAt",
|
|
1053
1180
|
"updatedAt",
|
|
1054
1181
|
Field(name="customerJourneySummary", fields=customer_journey_summary_fields),
|
|
1182
|
+
Field(
|
|
1183
|
+
name="customerJourney",
|
|
1184
|
+
fields=[
|
|
1185
|
+
Field(name="moments", fields=customer_visit_fragment),
|
|
1186
|
+
],
|
|
1187
|
+
),
|
|
1055
1188
|
]
|
|
1056
1189
|
|
|
1057
1190
|
record_composition = {
|
|
@@ -1062,6 +1195,9 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1062
1195
|
self,
|
|
1063
1196
|
visit_data: Mapping[str, Any],
|
|
1064
1197
|
) -> MutableMapping[str, Any]:
|
|
1198
|
+
if not visit_data:
|
|
1199
|
+
return {}
|
|
1200
|
+
|
|
1065
1201
|
# save the id before it's resolved
|
|
1066
1202
|
visit_data["admin_graphql_api_id"] = visit_data.get("id")
|
|
1067
1203
|
# resolve the order_id to str
|
|
@@ -1072,14 +1208,24 @@ class CustomerJourney(ShopifyBulkQuery):
|
|
|
1072
1208
|
visit_data = self.tools.fields_names_to_snake_case(visit_data)
|
|
1073
1209
|
return visit_data
|
|
1074
1210
|
|
|
1211
|
+
def process_moments(self, entity: List[Mapping[str, Any]]) -> List[MutableMapping[str, Any]]:
|
|
1212
|
+
moments = []
|
|
1213
|
+
for item in entity:
|
|
1214
|
+
moments.append(self.process_visit(item))
|
|
1215
|
+
return moments
|
|
1216
|
+
|
|
1075
1217
|
def process_customer_journey(self, record: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
|
|
1076
1218
|
customer_journey_summary = record.get("customerJourneySummary", {})
|
|
1077
1219
|
if customer_journey_summary:
|
|
1078
1220
|
# process first, last visit data
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1221
|
+
customer_journey_summary["firstVisit"] = self.process_visit(customer_journey_summary.get("firstVisit"))
|
|
1222
|
+
customer_journey_summary["lastVisit"] = self.process_visit(customer_journey_summary.get("lastVisit"))
|
|
1223
|
+
|
|
1224
|
+
# # this will be a part of summary in the next api version
|
|
1225
|
+
if customer_journey := record.get("customerJourney", {}):
|
|
1226
|
+
moments = customer_journey.get("moments", [])
|
|
1227
|
+
customer_journey_summary["moments"] = self.process_moments(moments)
|
|
1228
|
+
|
|
1083
1229
|
# cast field names to snake_case
|
|
1084
1230
|
customer_journey_summary = self.tools.fields_names_to_snake_case(customer_journey_summary)
|
|
1085
1231
|
return customer_journey_summary
|
|
@@ -2199,6 +2345,7 @@ class ProductImage(ShopifyBulkQuery):
|
|
|
2199
2345
|
node {
|
|
2200
2346
|
__typename
|
|
2201
2347
|
id
|
|
2348
|
+
products_updated_at: updatedAt
|
|
2202
2349
|
# THE MEDIA NODE IS NEEDED TO PROVIDE THE CURSORS
|
|
2203
2350
|
media {
|
|
2204
2351
|
edges {
|
|
@@ -2275,8 +2422,7 @@ class ProductImage(ShopifyBulkQuery):
|
|
|
2275
2422
|
# media property fields
|
|
2276
2423
|
media_fields: List[Field] = [Field(name="edges", fields=[Field(name="node", fields=media_fragment)])]
|
|
2277
2424
|
|
|
2278
|
-
|
|
2279
|
-
query_nodes: List[Field] = [
|
|
2425
|
+
nodes: List[Field] = [
|
|
2280
2426
|
"__typename",
|
|
2281
2427
|
"id",
|
|
2282
2428
|
Field(name="media", fields=media_fields),
|
|
@@ -2291,6 +2437,10 @@ class ProductImage(ShopifyBulkQuery):
|
|
|
2291
2437
|
"record_components": ["MediaImage", "Image"],
|
|
2292
2438
|
}
|
|
2293
2439
|
|
|
2440
|
+
@property
|
|
2441
|
+
def query_nodes(self) -> List[Field]:
|
|
2442
|
+
return self.inject_parent_cursor_field(self.nodes)
|
|
2443
|
+
|
|
2294
2444
|
def _process_component(self, entity: List[dict]) -> List[dict]:
|
|
2295
2445
|
for item in entity:
|
|
2296
2446
|
# remove the `__parentId` from the object
|
|
@@ -2374,7 +2524,6 @@ class ProductImage(ShopifyBulkQuery):
|
|
|
2374
2524
|
if len(images) > 0:
|
|
2375
2525
|
# convert dates from ISO-8601 to RFC-3339
|
|
2376
2526
|
record["images"] = self._convert_datetime_to_rfc3339(images)
|
|
2377
|
-
|
|
2378
2527
|
yield from self._emit_complete_records(images)
|
|
2379
2528
|
|
|
2380
2529
|
|
|
@@ -2382,8 +2531,7 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2382
2531
|
"""
|
|
2383
2532
|
{
|
|
2384
2533
|
productVariants(
|
|
2385
|
-
query: "
|
|
2386
|
-
sortKey: UPDATED_AT
|
|
2534
|
+
query: "updatedAt:>='2019-04-13T00:00:00+00:00' AND updatedAt:<='2024-04-30T12:16:17.273363+00:00'"
|
|
2387
2535
|
) {
|
|
2388
2536
|
edges {
|
|
2389
2537
|
node {
|
|
@@ -2418,6 +2566,10 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2418
2566
|
color
|
|
2419
2567
|
image {
|
|
2420
2568
|
id
|
|
2569
|
+
image {
|
|
2570
|
+
src
|
|
2571
|
+
url
|
|
2572
|
+
}
|
|
2421
2573
|
}
|
|
2422
2574
|
}
|
|
2423
2575
|
}
|
|
@@ -2425,6 +2577,8 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2425
2577
|
grams: weight
|
|
2426
2578
|
image {
|
|
2427
2579
|
image_id: id
|
|
2580
|
+
image_src: src
|
|
2581
|
+
image_url: url
|
|
2428
2582
|
}
|
|
2429
2583
|
old_inventory_quantity: inventoryQuantity
|
|
2430
2584
|
product {
|
|
@@ -2457,64 +2611,83 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2457
2611
|
"""
|
|
2458
2612
|
|
|
2459
2613
|
query_name = "productVariants"
|
|
2460
|
-
sort_key = "ID"
|
|
2461
2614
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
name="edges",
|
|
2466
|
-
fields=[
|
|
2467
|
-
Field(
|
|
2468
|
-
name="node",
|
|
2469
|
-
fields=["__typename", Field(name="price", fields=prices_fields), Field(name="compareAtPrice", fields=prices_fields)],
|
|
2470
|
-
)
|
|
2471
|
-
],
|
|
2472
|
-
)
|
|
2473
|
-
]
|
|
2615
|
+
@property
|
|
2616
|
+
def _should_include_presentment_prices(self) -> bool:
|
|
2617
|
+
return self.config.get("job_product_variants_include_pres_prices", True)
|
|
2474
2618
|
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
"
|
|
2478
|
-
Field
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2619
|
+
@property
|
|
2620
|
+
def query_nodes(self) -> Optional[Union[List[Field], List[str]]]:
|
|
2621
|
+
prices_fields: List[str] = ["amount", "currencyCode"]
|
|
2622
|
+
presentment_prices_fields: List[Field] = [
|
|
2623
|
+
Field(
|
|
2624
|
+
name="edges",
|
|
2625
|
+
fields=[
|
|
2626
|
+
Field(
|
|
2627
|
+
name="node",
|
|
2628
|
+
fields=[
|
|
2629
|
+
"__typename",
|
|
2630
|
+
Field(name="price", fields=prices_fields),
|
|
2631
|
+
Field(name="compareAtPrice", fields=prices_fields),
|
|
2632
|
+
],
|
|
2633
|
+
)
|
|
2634
|
+
],
|
|
2635
|
+
)
|
|
2636
|
+
]
|
|
2637
|
+
option_value_fields: List[Field] = [
|
|
2638
|
+
"id",
|
|
2639
|
+
"name",
|
|
2640
|
+
Field(name="hasVariants", alias="has_variants"),
|
|
2641
|
+
Field(name="swatch", fields=["color", Field(name="image", fields=["id", Field(name="image", fields=["src", "url"])])]),
|
|
2642
|
+
]
|
|
2643
|
+
option_fields: List[Field] = [
|
|
2644
|
+
"name",
|
|
2645
|
+
"value",
|
|
2646
|
+
Field(name="optionValue", alias="option_value", fields=option_value_fields),
|
|
2647
|
+
]
|
|
2648
|
+
presentment_prices = (
|
|
2649
|
+
[Field(name="presentmentPrices", fields=presentment_prices_fields)] if self._should_include_presentment_prices else []
|
|
2650
|
+
)
|
|
2486
2651
|
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2652
|
+
image_fields = [
|
|
2653
|
+
Field(name="id", alias="image_id"),
|
|
2654
|
+
Field(name="src", alias="image_src"),
|
|
2655
|
+
Field(name="url", alias="image_url"),
|
|
2656
|
+
]
|
|
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
|
+
]
|
|
2666
|
+
query_nodes: List[Field] = [
|
|
2667
|
+
"__typename",
|
|
2668
|
+
"id",
|
|
2669
|
+
"title",
|
|
2670
|
+
"price",
|
|
2671
|
+
"sku",
|
|
2672
|
+
"position",
|
|
2673
|
+
"inventoryPolicy",
|
|
2674
|
+
"compareAtPrice",
|
|
2675
|
+
"createdAt",
|
|
2676
|
+
"updatedAt",
|
|
2677
|
+
"taxable",
|
|
2678
|
+
"barcode",
|
|
2679
|
+
"inventoryQuantity",
|
|
2680
|
+
"availableForSale",
|
|
2681
|
+
"displayName",
|
|
2682
|
+
"taxCode",
|
|
2683
|
+
Field(name="selectedOptions", alias="options", fields=option_fields),
|
|
2684
|
+
Field(name="image", fields=image_fields),
|
|
2685
|
+
Field(name="inventoryQuantity", alias="old_inventory_quantity"),
|
|
2686
|
+
Field(name="product", fields=[Field(name="id", alias="product_id")]),
|
|
2687
|
+
Field(name="inventoryItem", fields=inventory_item_fields),
|
|
2688
|
+
] + presentment_prices
|
|
2689
|
+
|
|
2690
|
+
return query_nodes
|
|
2518
2691
|
|
|
2519
2692
|
record_composition = {
|
|
2520
2693
|
"new_record": "ProductVariant",
|
|
@@ -2572,14 +2745,21 @@ class ProductVariant(ShopifyBulkQuery):
|
|
|
2572
2745
|
# unnest mandatory fields from their placeholders
|
|
2573
2746
|
record["product_id"] = self._unnest_and_resolve_id(record, "product", "product_id")
|
|
2574
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", 0.0) if measurement_weight is not None 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
|
|
2575
2754
|
record["image_id"] = self._unnest_and_resolve_id(record, "image", "image_id")
|
|
2576
|
-
|
|
2577
|
-
record["
|
|
2755
|
+
image = record.get("image", {})
|
|
2756
|
+
record["image_src"] = image.get("image_src") if image else None
|
|
2757
|
+
record["image_url"] = image.get("image_url") if image else None
|
|
2578
2758
|
# cast the `price` to number, could be literally `None`
|
|
2579
2759
|
price = record.get("price")
|
|
2580
2760
|
record["price"] = float(price) if price else None
|
|
2581
2761
|
# cast the `grams` to integer
|
|
2582
|
-
record["grams"] = int(record.get("
|
|
2762
|
+
record["grams"] = int(record.get("weight", 0))
|
|
2583
2763
|
# convert date-time cursors
|
|
2584
2764
|
record["createdAt"] = self.tools.from_iso8601_to_rfc3339(record, "createdAt")
|
|
2585
2765
|
record["updatedAt"] = self.tools.from_iso8601_to_rfc3339(record, "updatedAt")
|
|
@@ -3021,3 +3201,175 @@ class OrderAgreement(ShopifyBulkQuery):
|
|
|
3021
3201
|
record["agreements"] = agreements_with_sales if agreements_with_sales else {}
|
|
3022
3202
|
|
|
3023
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
|