Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Shopify client #1

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Python Tests

on:
push:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.8' # Specify your Python version here
Anton-Shutik marked this conversation as resolved.
Show resolved Hide resolved

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]

- name: Run tests
run: pytest .
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*#*
*.pyc
.env
.env.BAK
tmp/
src/

*.log
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,101 @@
# shopify-client
Python client for Shopify REST & GraphQL API
## Installation

```bash
pip install -e git+https://github.com/groveco/shopify-client@main#egg=shopify_client
```

## Usage

### REST API

```python
from shopify_client import ShopifyClient

# Initialize the client
client = ShopifyClient(api_url='your_api_url', api_token='your_token', api_version='your_api_version')

# Get a list of products
products = client.products.all()

# Get only specific fields
products = client.products.all(fields="id,title")

# Get limited amount of products
products = client.products.all(limit=20)

# Use pagination
for page in client.products.all(paginate=True, limit=100)
print(page)

# Get specific product by id
product = client.products.get(resource_id=1234)

# List product metafields for product id
metafields = client.products.metafields.all(resource_id=1234)

# Get speficic metafield by id
metafield = client.products.metafields.all(resource_id=1234, sub_resource_id=5678)

# Create a product
data = {"product":{"title":"Burton Custom Freestyle 151","body_html":"<strong>Good snowboard!</strong>","vendor":"Burton","product_type":"Snowboard","status":"draft"}}
product = client.products.create(json=data)

# Update product
product = client.products.create(resource_id=1234, json=data)

# Delete product
deleted = client.products.delete(resource=1234)

# Count of products
count = client.products.count()

# Cancel order
order = client.orders.cancel(resource_id=1234)

# Close order
order = client.orders.close(resource_id=1234)

```

### GraphQL API

```python
# List products
query = '''
query products($page_size: Int = 100) {
products(first: $page_size) {
nodes {
id
title
}
}
}
'''
response = client.graphql.query(query)

# Limit page size
response = client.graphql.query(query, variables={"page_size": 20})

# Use pagination.
# Note that "pageIngo" block with at least "hasNextPage" & "startCursor" is required
# $cursor value should be passed as "after" parameter
query = '''
query products($page_size: Int = 100, $cursor: String) {
products(first: $page_size, after: $cursor) {
nodes {
id
title
}
pageInfo {
hasNextPage
startCursor
}
}
}
'''
for page in client.graphql.query_paginated(query)
print(page)
```

44 changes: 44 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from setuptools import setup, find_packages

setup(
name="shopify-client",
version="0.1.0",
description="Python client for Shopify REST and GraphQL API",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
author="Anton Shutsik",
author_email="[email protected]",
url="https://github.com/groveco/shopify-client.git",
packages=["shopify_client"],
install_requires=[
"requests>=2.25.1",
],
extras_require={
"dev": [
"pytest",
"pytest-cov",
"pytest-mock",
"flake8",
"black",
"sphinx",
"pre-commit"
]
},
python_requires=">=3.7",
classifiers=[
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
Anton-Shutik marked this conversation as resolved.
Show resolved Hide resolved
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
],
project_urls={
"Documentation": "https://github.com/groveco/shopify-client",
"Source": "https://github.com/groveco/shopify-client",
"Tracker": "https://github.com/groveco/shopify-client/issues",
},
)
140 changes: 140 additions & 0 deletions shopify_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import logging
import time
from urllib.parse import urljoin

import requests

from .endpoint import DraftOrdersEndpoint, Endpoint, OrdersEndpoint
from .graphql import GraphQL

logger = logging.getLogger(__name__)

SHOPIFY_API_VERSION = "2024-10"


class ShopifyClient(requests.Session):

def __init__(self, api_url, api_token, api_version=SHOPIFY_API_VERSION):
super().__init__()
self.api_url = api_url
self.api_version = api_version
self.headers.update({"X-Shopify-Access-Token": api_token, "Content-Type": "application/json"})

# Access
self.storefront_access_tokens = Endpoint(client=self, endpoint="storefront_access_tokens")

# Billing
self.application_charges = Endpoint(client=self, endpoint="application_charges")
self.application_credits = Endpoint(client=self, endpoint="application_credits")
self.recurring_application_charges = Endpoint(client=self, endpoint="recurring_application_charges")

# Customers
self.customers = Endpoint(client=self, endpoint="customers", metafields=True)
# self.customer_addresses = ShopifyEndpoint(client=self, endpoint="customer_addresses")

# Discounts
self.price_rules = Endpoint(client=self, endpoint="price_rules")
self.discount_codes = Endpoint(client=self, endpoint="discount_codes")

# Events
self.events = Endpoint(client=self, endpoint="events")

# Gift Cards
self.gift_cards = Endpoint(client=self, endpoint="gift_cards")

# Inventory
self.inventory_items = Endpoint(client=self, endpoint="inventory_items")
self.inventory_levels = Endpoint(client=self, endpoint="inventory_levels")
self.locations = Endpoint(client=self, endpoint="locations", metafields=True)

# Marketing Event
self.marketing_events = Endpoint(client=self, endpoint="marketing_events")

# Mobile Support
self.mobile_platform_applications = Endpoint(client=self, endpoint="mobile_platform_applications")

# Online Store
self.articles = Endpoint(client=self, endpoint="articles", metafields=True)
self.blogs = Endpoint(client=self, endpoint="blogs", metafields=True)
self.pages = Endpoint(client=self, endpoint="pages", metafields=True)

# Orders
self.checkouts = Endpoint(client=self, endpoint="checkouts")
self.draft_orders = DraftOrdersEndpoint(client=self, endpoint="draft_orders", metafields=True)
self.orders = OrdersEndpoint(client=self, endpoint="orders")

# Plus
self.users = Endpoint(client=self, endpoint="users")

# Products
self.collects = Endpoint(client=self, endpoint="collects")
self.collections = Endpoint(client=self, endpoint="collections", metafields=True)
self.custom_collections = Endpoint(client=self, endpoint="custom_collections")
self.products = Endpoint(client=self, endpoint="products", metafields=True)
self.products.images = Endpoint(client=self, endpoint="products", sub_endpoint="images")
self.product_images = Endpoint(client=self, endpoint="product_images", metafields=True)
self.smart_collections = Endpoint(client=self, endpoint="smart_collections", metafields=True)
self.variants = Endpoint(client=self, endpoint="variants", metafields=True)

# Sales Channels
self.collections_listings = Endpoint(client=self, endpoint="collections_listings")
self.checkouts = Endpoint(client=self, endpoint="checkouts")
self.product_listings = Endpoint(client=self, endpoint="product_listings")
self.resource_feedback = Endpoint(client=self, endpoint="resource_feedback")

# Shipping and Fulfillment
self.assigned_fulfillment_orders = Endpoint(client=self, endpoint="assigned_fulfillment_orders")
# TODO: Implement Fulfillment
self.fulfillment_orders = Endpoint(client=self, endpoint="fulfillment_orders")
self.carrier_services = Endpoint(client=self, endpoint="carrier_services")
self.fulfillments = Endpoint(client=self, endpoint="fulfillments")
self.fulfillment_services = Endpoint(client=self, endpoint="fulfillment_services")

# Shopify Payments
self.shopify_payments = Endpoint(client=self, endpoint="shopify_payments")

# Store Properties
self.countries = Endpoint(client=self, endpoint="countries")
self.currencies = Endpoint(client=self, endpoint="currencies")
self.policies = Endpoint(client=self, endpoint="policies")
self.shipping_zones = Endpoint(client=self, endpoint="shipping_zones")
self.shop = Endpoint(client=self, endpoint="shop", metafields=True)

# Tender Transactions
self.tender_transactions = Endpoint(client=self, endpoint="tender_transactions")

# Webhooks
self.webhooks = Endpoint(client=self, endpoint="webhooks")

# GraphQL
self.graphql = GraphQL(client=self)

def rate_limit(response, *args, **kwargs):
max_retry_count = 5
retry_count = int(response.request.headers.get("X-Retry-Count", 0))

if response.status_code == 429 and retry_count < max_retry_count:
retry_after = int(response.headers.get("retry-after", 4))
logger.warning(f"Shopify service exceeds API call limit; will retry request in {retry_after} seconds")
time.sleep(retry_after)
response.request.headers["X-Retry-Count"] = retry_count + 1
new_response = response.connection.send(response.request)
new_response.history.append(response)
return rate_limit(new_response, *args, **kwargs)

return response

self.hooks["response"].append(rate_limit)

def request(self, method, url, *args, **kwargs):
response = super().request(method, urljoin(f"{self.api_url}/admin/api/{self.api_version}/", url), *args, **kwargs)
logger.info(f"Requesting {method} {url}: {response.status_code}")
return response

def parse_response(self, response):
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
logger.warning(f"Failed to execute request: {response.text}")
raise e
return response.json()
Loading