Shopify Python API Library

repository·main·Indexed 23 days ago

https://github.com/shopify/shopify_python_api

A Python library for interacting with the Shopify Admin API, supporting both REST and GraphQL interfaces. It provides session management, OAuth flows for public apps, resource mapping via pyactiveresource, and utilities for managing application billing, session tokens, and API access scopes.

Tokens
4.3K
Snippets
13
Records
16
Agent score
81%

What's inside shopify_python_api

  1. Get started with Public and Custom Apps

    main

    Public and Custom apps require OAuth to obtain an access token from a specific shop.

    1. Setup Credentials: Initialize the shopify.Session with your API Key and API Secret.
    2. Generate Auth URL: Create a session and use create_permission_url to generate the URL where you will redirect the merchant.
    3. Exchange Code for Token: In your callback handler, use session.request_token(request_params) to exchange the temporary code for a permanent access_token.
    4. Activate Session: Use shopify.ShopifyResource.activate_session(session) to make authorized requests.
    5. Cleanup: It is best practice to call shopify.ShopifyResource.clear_session() when finished.
    import shopify
    import binascii
    import os
    
    # 1. Setup credentials
    shopify.Session.setup(api_key='API_KEY', secret='API_SECRET')
    
    # 2. Generate Auth URL
    shop_url = "SHOP_NAME.myshopify.com"
    api_version = '2024-07'
    state = binascii.b2a_hex(os.urandom(15)).decode("utf-8")
    redirect_uri = "http://myapp.com/auth/shopify/callback"
    scopes = ['read_products', 'read_orders']
    
    newSession = shopify.Session(shop_url, api_version)
    auth_url = newSession.create_permission_url(redirect_uri, scopes, state)
    # Redirect user to auth_url
    
    # 3. Exchange code for token (in callback handler)
    # request_params contains the 'code' and 'state' from the redirect
    session = shopify.Session(shop_url, api_version)
    access_token = session.request_token(request_params)
    
    # 4. Make requests
    session = shopify.Session(shop_url, api_version, access_token)
    shopify.ShopifyResource.activate_session(session)
    
    shop = shopify.Shop.current()
    product = shopify.Product.find(179761209)
    
    # 5. Cleanup
    shopify.ShopifyResource.clear_session()
  2. Protect app routes using a `session_token` decorator

    main

    To protect your app's views or routes, you can create a decorator that uses session_token.decode_from_header to validate the presence of a valid session token in the request headers. If the token is invalid, it will raise a session_token.SessionTokenError.

    In the event of a decoding error, you should catch session_token.SessionTokenError and return an unauthorized response (e.g., a 401 status code).

    from shopify import session_token
    
    
    def session_token_required(func):
        def wrapper(*args, **kwargs):
            request = args[0]  # Or flask.request if you use Flask
            try:
                decoded_session_token = session_token.decode_from_header(
                    authorization_header = request.headers.get('Authorization'),
                    api_key = SHOPIFY_API_KEY,
                    secret = SHOPIFY_API_SECRET
                )
                with shopify_session(decoded_session_token):
                    return func(*args, **kwargs)
            except session_token.SessionTokenError as e:
                # Log the error here
                return unauthorized_401_response()
    
        return wrapper
    
    
    def shopify_session(decoded_session_token):
        shopify_domain = decoded_session_token.get("dest")
        access_token = get_offline_access_token_by_shop_domain(shopify_domain)
    
        return shopify.Session.temp(shopify_domain, SHOPIFY_API_VERSION, access_token)
    
    
    @session_token_required  # Requests to /products require session tokens
    def products(request):
        products = shopify.Product.find()
        ...
  3. Handle changes in app access scopes using ApiAccess

    main

    When your app's required scopes change, you can use ApiAccess to detect if the currently granted scopes in your database match your app's expected scopes. If they do not match, you should redirect the merchant to the OAuth flow to update their permissions.

    Example pattern using a decorator:

    from shopify import ApiAccess
    
    def oauth_on_access_scopes_mismatch(func):
      def wrapper(*args, **kwargs):
        shop_domain = get_shop_query_parameter(request) # shop query param when loading app
        current_shop_scopes = ApiAccess(ShopStore.get_record(shopify_domain = shop_domain).access_scopes)
        expected_access_scopes = ApiAccess(SHOPIFY_API_SCOPES)
    
        if current_shop_scopes != expected_access_scopes:
          return redirect_to_login() # redirect to OAuth to update access scopes granted
    
        return func(*args, **kwargs)
    
      return wrapper
    from shopify import ApiAccess
    
    
    def oauth_on_access_scopes_mismatch(func):
      def wrapper(*args, **kwargs):
        shop_domain = get_shop_query_parameter(request) # shop query param when loading app
        current_shop_scopes = ApiAccess(ShopStore.get_record(shopify_domain = shop_domain).access_scopes)
        expected_access_scopes = ApiAccess(SHOPIFY_API_SCOPES)
    
        if current_shop_scopes != expected_access_scopes:
          return redirect_to_login() # redirect to OAuth to update access scopes granted
    
        return func(*args, **kwargs)
    
      return wrapper
  4. Manage Application Billing

    main

    To charge a merchant, create an ApplicationCharge.

    1. Create Charge: Use shopify.ApplicationCharge.create with a dictionary containing name, price, return_url, and optionally 'test': True for development stores.
    2. Redirect: Redirect the user to application_charge.confirmation_url.
    3. Activate: After the user approves, they are redirected to your return_url with a charge_id. Use shopify.ApplicationCharge.find(charge_id) and then shopify.ApplicationCharge.activate(charge) to finalize.
    4. Verify: Check charge.status == 'active' to confirm billing.
    # 1. Create charge
    application_charge = shopify.ApplicationCharge.create({
        'name': 'My public app',
        'price': 123,
        'test': True,
        'return_url': 'https://domain.com/approve'
    })
    # Redirect user to application_charge.confirmation_url
    
    # 2. Activate charge (after redirect with charge_id)
    charge = shopify.ApplicationCharge.find(charge_id)
    shopify.ApplicationCharge.activate(charge)
    
    # 3. Verify
    activated_charge = shopify.ApplicationCharge.find(charge_id)
    if activated_charge.status == 'active':
        print("Billed successfully")
  5. Get started with Private Apps

    main

    Private apps do not require OAuth. You can use your Private App password directly as the access_token.

    Using a full session:

    session = shopify.Session(shop_url, api_version, private_app_password)
    shopify.ShopifyResource.activate_session(session)
    # ... perform operations
    shopify.ShopifyResource.clear_session()

    Using a temporary session (recommended):

    with shopify.Session.temp(shop_url, api_version, private_app_password):
        shopify.GraphQL().execute("{ shop { name id } }")
    # Temporary session example
    with shopify.Session.temp(shop_url, api_version, private_app_password):
        shopify.GraphQL().execute("{ shop { name id } }")
  6. Set up pre-commit locally

    main

    If you are contributing to this project and want to run the same linting and formatting checks used in GitHub Actions, you can set up pre-commit locally. This requires installing the project requirements and then initializing the git hook scripts.

    pip install -r requirements.txt
    pre-commit install
  7. Execute GraphQL queries

    main

    The library supports the Shopify GraphQL API. Once a session is activated, use shopify.GraphQL().execute() to run queries.

    You can pass a single query string, or provide a full GraphQL document with variables and an operation_name to execute specific named queries within a document.

    # Simple query
    result = shopify.GraphQL().execute('{ shop { name id } }')
    
    # Complex query with variables and operation name
    from pathlib import Path
    document = Path("./order_queries.graphql").read_text()
    
    result = shopify.GraphQL().execute(
        query=document,
        variables={"order_id": "gid://shopify/Order/12345"},
        operation_name="GetOneOrder",
    )
  8. Use the REST Admin API (Legacy)

    main

    The library uses pyactiveresource to map RESTful resources. Note: REST API examples will be deprecated in 2025.

    • Create: Instantiate a resource, set attributes, and call .save().
    • Read: Use .find(id) or .find(params) to retrieve resources.
    • Update: Modify attributes on an existing resource and call .save().
    • Delete: Call .destroy() on a resource.
    • Prefixes: For resources prefixed by a parent (e.g., Fulfillment), pass the parent ID as the first argument to .find().

    Example of finding a prefixed resource:

    shopify.Fulfillment.find(255858046, order_id=450789467)
    # Create and save
    product = shopify.Product()
    product.title = "Shopify Logo T-Shirt"
    product.save()
    
    # Find and update
    product = shopify.Product.find(292082188312)
    product.price = 19.99
    product.save()
    
    # Delete
    product.destroy()
    
    # Find with parameters
    new_orders = shopify.Order.find(status="open", limit="50")
  9. Compare ApiAccess objects for equality and coverage

    main

    Use ApiAccess objects to manage and validate permissions:

    • Equality (==): Checks if two sets of scopes grant exactly the same API access. Note that the order of scopes does not affect equality.
    • Coverage (.covers()): Checks if one set of scopes (superset_access) includes all the permissions required by another set (subset_access).
    expected_api_access = ApiAccess(["read_products", "write_orders"])
    actual_api_access = ApiAccess(["read_products", "read_orders", "write_orders"])
    non_equal_api_access = ApiAccess(["read_products", "write_orders", "read_themes"])
    
    # Checking for API access equality
    actual_api_access == expected_api_access # True
    non_equal_api_access == expected_api_access # False
    
    # Checking if ApiAccess covers the access of another
    superset_access = ApiAccess(["write_products", "write_orders", "read_themes"])
    subset_access = ApiAccess(["read_products", "write_orders"])
    
    superset_access.covers(subset_access) # True
  10. Use Relative Cursor Pagination

    main

    For cursor-based pagination, use the methods provided on the resource returned by .find().

    1. Check if more pages exist using .has_next_page().
    2. Retrieve the next page using .next_page().
    3. To persist pagination across different requests, use the next_page_url property and pass it to .find(from_=next_url).
    import shopify
    
    page1 = shopify.Product.find()
    if page1.has_next_page():
      page2 = page1.next_page()
    
    # To persist across requests:
    next_url = page1.next_page_url
    page2 = shopify.Product.find(from_=next_url)