shopify-starter-kit 1.0.0

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.
Files changed (28) hide show
  1. package/.agent/skills/shopify-apps/SKILL.md +47 -0
  2. package/.agent/skills/shopify-automation/SKILL.md +172 -0
  3. package/.agent/skills/shopify-development/README.md +60 -0
  4. package/.agent/skills/shopify-development/SKILL.md +368 -0
  5. package/.agent/skills/shopify-development/references/app-development.md +578 -0
  6. package/.agent/skills/shopify-development/references/extensions.md +555 -0
  7. package/.agent/skills/shopify-development/references/themes.md +498 -0
  8. package/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
  9. package/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
  10. package/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
  11. package/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
  12. package/bin/cli.js +3 -0
  13. package/package.json +32 -0
  14. package/src/index.js +116 -0
  15. package/templates/.agent/skills/shopify-apps/SKILL.md +47 -0
  16. package/templates/.agent/skills/shopify-automation/SKILL.md +172 -0
  17. package/templates/.agent/skills/shopify-development/README.md +60 -0
  18. package/templates/.agent/skills/shopify-development/SKILL.md +368 -0
  19. package/templates/.agent/skills/shopify-development/references/app-development.md +578 -0
  20. package/templates/.agent/skills/shopify-development/references/extensions.md +555 -0
  21. package/templates/.agent/skills/shopify-development/references/themes.md +498 -0
  22. package/templates/.agent/skills/shopify-development/scripts/requirements.txt +19 -0
  23. package/templates/.agent/skills/shopify-development/scripts/shopify_graphql.py +428 -0
  24. package/templates/.agent/skills/shopify-development/scripts/shopify_init.py +441 -0
  25. package/templates/.agent/skills/shopify-development/scripts/tests/test_shopify_init.py +379 -0
  26. package/templates/.devcontainer/devcontainer.json +27 -0
  27. package/templates/tests/playwright.config.ts +26 -0
  28. package/templates/tests/vitest.config.ts +9 -0
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Shopify GraphQL Utilities
4
+
5
+ Helper functions for common Shopify GraphQL operations.
6
+ Provides query templates, pagination helpers, and rate limit handling.
7
+
8
+ Usage:
9
+ from shopify_graphql import ShopifyGraphQL
10
+
11
+ client = ShopifyGraphQL(shop_domain, access_token)
12
+ products = client.get_products(first=10)
13
+ """
14
+
15
+ import os
16
+ import time
17
+ import json
18
+ from typing import Dict, List, Optional, Any, Generator
19
+ from dataclasses import dataclass
20
+ from urllib.request import Request, urlopen
21
+ from urllib.error import HTTPError
22
+
23
+
24
+ # API Configuration
25
+ API_VERSION = "2026-01"
26
+ MAX_RETRIES = 3
27
+ RETRY_DELAY = 1.0 # seconds
28
+
29
+
30
+ @dataclass
31
+ class GraphQLResponse:
32
+ """Container for GraphQL response data."""
33
+ data: Optional[Dict[str, Any]] = None
34
+ errors: Optional[List[Dict[str, Any]]] = None
35
+ extensions: Optional[Dict[str, Any]] = None
36
+
37
+ @property
38
+ def is_success(self) -> bool:
39
+ return self.errors is None or len(self.errors) == 0
40
+
41
+ @property
42
+ def query_cost(self) -> Optional[int]:
43
+ """Get the actual query cost from extensions."""
44
+ if self.extensions and 'cost' in self.extensions:
45
+ return self.extensions['cost'].get('actualQueryCost')
46
+ return None
47
+
48
+
49
+ class ShopifyGraphQL:
50
+ """
51
+ Shopify GraphQL API client with built-in utilities.
52
+
53
+ Features:
54
+ - Query templates for common operations
55
+ - Automatic pagination
56
+ - Rate limit handling with exponential backoff
57
+ - Response parsing helpers
58
+ """
59
+
60
+ def __init__(self, shop_domain: str, access_token: str):
61
+ """
62
+ Initialize the GraphQL client.
63
+
64
+ Args:
65
+ shop_domain: Store domain (e.g., 'my-store.myshopify.com')
66
+ access_token: Admin API access token
67
+ """
68
+ self.shop_domain = shop_domain.replace('https://', '').replace('http://', '')
69
+ self.access_token = access_token
70
+ self.base_url = f"https://{self.shop_domain}/admin/api/{API_VERSION}/graphql.json"
71
+
72
+ def execute(self, query: str, variables: Optional[Dict] = None) -> GraphQLResponse:
73
+ """
74
+ Execute a GraphQL query/mutation.
75
+
76
+ Args:
77
+ query: GraphQL query string
78
+ variables: Query variables
79
+
80
+ Returns:
81
+ GraphQLResponse object
82
+ """
83
+ payload = {"query": query}
84
+ if variables:
85
+ payload["variables"] = variables
86
+
87
+ headers = {
88
+ "Content-Type": "application/json",
89
+ "X-Shopify-Access-Token": self.access_token
90
+ }
91
+
92
+ for attempt in range(MAX_RETRIES):
93
+ try:
94
+ request = Request(
95
+ self.base_url,
96
+ data=json.dumps(payload).encode('utf-8'),
97
+ headers=headers,
98
+ method='POST'
99
+ )
100
+
101
+ with urlopen(request, timeout=30) as response:
102
+ result = json.loads(response.read().decode('utf-8'))
103
+ return GraphQLResponse(
104
+ data=result.get('data'),
105
+ errors=result.get('errors'),
106
+ extensions=result.get('extensions')
107
+ )
108
+
109
+ except HTTPError as e:
110
+ if e.code == 429: # Rate limited
111
+ delay = RETRY_DELAY * (2 ** attempt)
112
+ print(f"Rate limited. Retrying in {delay}s...")
113
+ time.sleep(delay)
114
+ continue
115
+ raise
116
+ except Exception as e:
117
+ if attempt == MAX_RETRIES - 1:
118
+ raise
119
+ time.sleep(RETRY_DELAY)
120
+
121
+ return GraphQLResponse(errors=[{"message": "Max retries exceeded"}])
122
+
123
+ # ==================== Query Templates ====================
124
+
125
+ def get_products(
126
+ self,
127
+ first: int = 10,
128
+ query: Optional[str] = None,
129
+ after: Optional[str] = None
130
+ ) -> GraphQLResponse:
131
+ """
132
+ Query products with pagination.
133
+
134
+ Args:
135
+ first: Number of products to fetch (max 250)
136
+ query: Optional search query
137
+ after: Cursor for pagination
138
+ """
139
+ gql = """
140
+ query GetProducts($first: Int!, $query: String, $after: String) {
141
+ products(first: $first, query: $query, after: $after) {
142
+ edges {
143
+ node {
144
+ id
145
+ title
146
+ handle
147
+ status
148
+ totalInventory
149
+ variants(first: 5) {
150
+ edges {
151
+ node {
152
+ id
153
+ title
154
+ price
155
+ inventoryQuantity
156
+ sku
157
+ }
158
+ }
159
+ }
160
+ }
161
+ cursor
162
+ }
163
+ pageInfo {
164
+ hasNextPage
165
+ endCursor
166
+ }
167
+ }
168
+ }
169
+ """
170
+ return self.execute(gql, {"first": first, "query": query, "after": after})
171
+
172
+ def get_orders(
173
+ self,
174
+ first: int = 10,
175
+ query: Optional[str] = None,
176
+ after: Optional[str] = None
177
+ ) -> GraphQLResponse:
178
+ """
179
+ Query orders with pagination.
180
+
181
+ Args:
182
+ first: Number of orders to fetch (max 250)
183
+ query: Optional search query (e.g., "financial_status:paid")
184
+ after: Cursor for pagination
185
+ """
186
+ gql = """
187
+ query GetOrders($first: Int!, $query: String, $after: String) {
188
+ orders(first: $first, query: $query, after: $after) {
189
+ edges {
190
+ node {
191
+ id
192
+ name
193
+ createdAt
194
+ displayFinancialStatus
195
+ displayFulfillmentStatus
196
+ totalPriceSet {
197
+ shopMoney { amount currencyCode }
198
+ }
199
+ customer {
200
+ id
201
+ firstName
202
+ lastName
203
+ }
204
+ lineItems(first: 5) {
205
+ edges {
206
+ node {
207
+ title
208
+ quantity
209
+ }
210
+ }
211
+ }
212
+ }
213
+ cursor
214
+ }
215
+ pageInfo {
216
+ hasNextPage
217
+ endCursor
218
+ }
219
+ }
220
+ }
221
+ """
222
+ return self.execute(gql, {"first": first, "query": query, "after": after})
223
+
224
+ def get_customers(
225
+ self,
226
+ first: int = 10,
227
+ query: Optional[str] = None,
228
+ after: Optional[str] = None
229
+ ) -> GraphQLResponse:
230
+ """
231
+ Query customers with pagination.
232
+
233
+ Args:
234
+ first: Number of customers to fetch (max 250)
235
+ query: Optional search query
236
+ after: Cursor for pagination
237
+ """
238
+ gql = """
239
+ query GetCustomers($first: Int!, $query: String, $after: String) {
240
+ customers(first: $first, query: $query, after: $after) {
241
+ edges {
242
+ node {
243
+ id
244
+ firstName
245
+ lastName
246
+ displayName
247
+ defaultEmailAddress {
248
+ emailAddress
249
+ }
250
+ numberOfOrders
251
+ amountSpent {
252
+ amount
253
+ currencyCode
254
+ }
255
+ }
256
+ cursor
257
+ }
258
+ pageInfo {
259
+ hasNextPage
260
+ endCursor
261
+ }
262
+ }
263
+ }
264
+ """
265
+ return self.execute(gql, {"first": first, "query": query, "after": after})
266
+
267
+ def set_metafields(self, metafields: List[Dict]) -> GraphQLResponse:
268
+ """
269
+ Set metafields on resources.
270
+
271
+ Args:
272
+ metafields: List of metafield inputs, each containing:
273
+ - ownerId: Resource GID
274
+ - namespace: Metafield namespace
275
+ - key: Metafield key
276
+ - value: Metafield value
277
+ - type: Metafield type
278
+ """
279
+ gql = """
280
+ mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
281
+ metafieldsSet(metafields: $metafields) {
282
+ metafields {
283
+ id
284
+ namespace
285
+ key
286
+ value
287
+ }
288
+ userErrors {
289
+ field
290
+ message
291
+ }
292
+ }
293
+ }
294
+ """
295
+ return self.execute(gql, {"metafields": metafields})
296
+
297
+ # ==================== Pagination Helpers ====================
298
+
299
+ def paginate_products(
300
+ self,
301
+ batch_size: int = 50,
302
+ query: Optional[str] = None
303
+ ) -> Generator[Dict, None, None]:
304
+ """
305
+ Generator that yields all products with automatic pagination.
306
+
307
+ Args:
308
+ batch_size: Products per request (max 250)
309
+ query: Optional search query
310
+
311
+ Yields:
312
+ Product dictionaries
313
+ """
314
+ cursor = None
315
+ while True:
316
+ response = self.get_products(first=batch_size, query=query, after=cursor)
317
+
318
+ if not response.is_success or not response.data:
319
+ break
320
+
321
+ products = response.data.get('products', {})
322
+ edges = products.get('edges', [])
323
+
324
+ for edge in edges:
325
+ yield edge['node']
326
+
327
+ page_info = products.get('pageInfo', {})
328
+ if not page_info.get('hasNextPage'):
329
+ break
330
+
331
+ cursor = page_info.get('endCursor')
332
+
333
+ def paginate_orders(
334
+ self,
335
+ batch_size: int = 50,
336
+ query: Optional[str] = None
337
+ ) -> Generator[Dict, None, None]:
338
+ """
339
+ Generator that yields all orders with automatic pagination.
340
+
341
+ Args:
342
+ batch_size: Orders per request (max 250)
343
+ query: Optional search query
344
+
345
+ Yields:
346
+ Order dictionaries
347
+ """
348
+ cursor = None
349
+ while True:
350
+ response = self.get_orders(first=batch_size, query=query, after=cursor)
351
+
352
+ if not response.is_success or not response.data:
353
+ break
354
+
355
+ orders = response.data.get('orders', {})
356
+ edges = orders.get('edges', [])
357
+
358
+ for edge in edges:
359
+ yield edge['node']
360
+
361
+ page_info = orders.get('pageInfo', {})
362
+ if not page_info.get('hasNextPage'):
363
+ break
364
+
365
+ cursor = page_info.get('endCursor')
366
+
367
+
368
+ # ==================== Utility Functions ====================
369
+
370
+ def extract_id(gid: str) -> str:
371
+ """
372
+ Extract numeric ID from Shopify GID.
373
+
374
+ Args:
375
+ gid: Global ID (e.g., 'gid://shopify/Product/123')
376
+
377
+ Returns:
378
+ Numeric ID string (e.g., '123')
379
+ """
380
+ return gid.split('/')[-1] if gid else ''
381
+
382
+
383
+ def build_gid(resource_type: str, id: str) -> str:
384
+ """
385
+ Build Shopify GID from resource type and ID.
386
+
387
+ Args:
388
+ resource_type: Resource type (e.g., 'Product', 'Order')
389
+ id: Numeric ID
390
+
391
+ Returns:
392
+ Global ID (e.g., 'gid://shopify/Product/123')
393
+ """
394
+ return f"gid://shopify/{resource_type}/{id}"
395
+
396
+
397
+ # ==================== Example Usage ====================
398
+
399
+ def main():
400
+ """Example usage of ShopifyGraphQL client."""
401
+ import os
402
+
403
+ # Load from environment
404
+ shop = os.environ.get('SHOP_DOMAIN', 'your-store.myshopify.com')
405
+ token = os.environ.get('SHOPIFY_ACCESS_TOKEN', '')
406
+
407
+ if not token:
408
+ print("Set SHOPIFY_ACCESS_TOKEN environment variable")
409
+ return
410
+
411
+ client = ShopifyGraphQL(shop, token)
412
+
413
+ # Example: Get first 5 products
414
+ print("Fetching products...")
415
+ response = client.get_products(first=5)
416
+
417
+ if response.is_success:
418
+ products = response.data['products']['edges']
419
+ for edge in products:
420
+ product = edge['node']
421
+ print(f" - {product['title']} ({product['status']})")
422
+ print(f"\nQuery cost: {response.query_cost}")
423
+ else:
424
+ print(f"Errors: {response.errors}")
425
+
426
+
427
+ if __name__ == '__main__':
428
+ main()