././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7345598 shopifyapi-12.7.0/0000755000076500000240000000000014712210466013702 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744343.0 shopifyapi-12.7.0/CHANGELOG0000644000076500000240000002724314712210027015115 0ustar00lizkenyibstaff== Unreleased == Version 12.7.0 - Remove requirement to use a predefined API version. Now you can use any valid API version string. ([#737](https://github.com/Shopify/shopify_python_api/pull/737)) == Version 12.6.0 - Update API version with 2024-07 release ([#723](https://github.com/Shopify/shopify_python_api/pull/723)) == Version 12.5.0 - Remove `cgi` import to avoid triggering a `DeprecationWarning` on Python 3.11. - Update API version with 2024-04 release.([710](https://github.com/Shopify/shopify_python_api/pull/710)) == Version 12.4.0 - Update API version with 2023-07, 2023-10, 2024-01 releases ([#694](https://github.com/Shopify/shopify_python_api/pull/694)) == Version 12.3.0 - Update API version with 2023-04 release ([#649](https://github.com/Shopify/shopify_python_api/pull/649)) == Version 12.2.0 - Update API version with 2023-01 release ([#631](https://github.com/Shopify/shopify_python_api/pull/631)) == Version 12.1.0 - Add API version with 2022-10 release == Version 12.0.1 - Allow up to 10 seconds clock skew to avoid `ImmatureSignatureError` ([#609](https://github.com/Shopify/shopify_python_api/pull/609)) == Version 12.0.0 - Update API version with 2022-04 release, remove API version 2021-07 ([#591](https://github.com/Shopify/shopify_python_api/pull/591)) == Version 11.0.0 - Update API version with 2022-04 release - remove API version 2020-10, 2021-01, 2021-04 as they are all unsupported as of 2022-04 == Version 10.0.0 - Update API version with 2022-01 release, remove API version 2020-07 == Version 9.0.0 - Drop Python 2 support ([#549](https://github.com/Shopify/shopify_python_api/pull/549)) - Update API version with 2021-10 release, remove API version 2020-04 ([#548](https://github.com/Shopify/shopify_python_api/pull/548)) == Version 8.4.2 - Update API version with 2021-07 release, remove API version 2020-01 ([#521](https://github.com/Shopify/shopify_python_api/pull/521)) == Version 8.4.1 - Bug fix: `sanitize_shop_domain` now returns `None` rather than `'none.myshopify.com'` if no `shop_domain` arg is passed in ([#499](https://github.com/Shopify/shopify_python_api/pull/499)) == Version 8.4.0 - Revert Feature #441 Dynamic API Versioning ([#495](https://github.com/Shopify/shopify_python_api/pull/495)) == Version 8.3.1 - Fix bug: Add the `shopify/utils` sub-package when building the source distribution ([#493](https://github.com/Shopify/shopify_python_api/pull/493)) == Version 8.3.0 - Add support for [session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens) ([#479](https://github.com/Shopify/shopify_python_api/pull/479)) - Use `session_token.decode_from_header` to obtain a decoded session token from an HTTP Authorization header - Create a `utils` sub-package with a `shop_url` utility file ([#483](https://github.com/Shopify/shopify_python_api/pull/483)) - Use `shop_url.sanitize_shop_domain()` to sanitize shop names given as input - Introduce the `ApiAccess` class to handle access scopes operations for apps - `Session` class now store access_scopes attributes as `ApiAccess` objects - Added support for Fulfillment.update_tracking ([#432](https://github.com/Shopify/shopify_python_api/pull/432)) - Add FulfillmentEvent resource ([#454](https://github.com/Shopify/shopify_python_api/pull/454)) - Fix for being unable to get the len() of a filter ([#456](https://github.com/Shopify/shopify_python_api/pull/456)) - Add ApplicationCredit resource ([#457](https://github.com/Shopify/shopify_python_api/pull/457)) - Add support for retrieving all orders using customer_id ([#466](https://github.com/Shopify/shopify_python_api/pull/466)) == Version 8.2.0 - [Feature] Add support for Dynamic API Versioning. When the library is initialized, it will now make a request to Shopify to fetch a list of the available API versions. ([#441](https://github.com/Shopify/shopify_python_api/pull/441)) == Version 8.1.0 - [Feature] Add support for Shopify Payments resources (#428) == Version 8.0.4 - Release API version 2020-10 - Deprecate API version 2019-10 == Version 8.0.3 - Patch for replacing call to \_build_list() with \_build_collection() in gift_card.py == Version 8.0.2 - Patch for product updating with variants == Version 8.0.1 - release api version 2020-07 - deprecate api version 2019-07 - Add support for FulfillmentOrder resource (#390) == Version 8.0.0 - release api version 2020-04 - deprecate api version 2019-04 == Version 7.0.3 - bug fix for temporary sessions - deprecation fix for regexs == Version 7.0.2 - bug fix for variant updates after the 2019-10 api version == Version 7.0.1 - bug fix for string interpolation == Version 7.0.0 - Made no_iter_next default to True on collection so that by default it only fetches a single page - Passes kwargs to paginated collections so that attributes can be set with find() - Allow case insensitive check for the link header for cursor pagination. == Version 6.0.1 - Made the collection access more consistent so that there is no confusion between a collection and a paginated collection == Version 6.0.0 - Add Cursor pagination support == Version 5.1.2 - Add version 2020-01 to known ApiVersions. This version will not be usable until October 2019. == Version 5.1.1 - Fix initializing API with basic auth URL. == Version 5.1.0 - Added support for GraphQL queries with a GraphQL resource == Version 5.0.1 - Fixing missing class variable causing exception when creating a session without a token == Version 5.0.0 - Added support for Shopify API Versioning == Version 4.0.0 - Added AccessScope resource - Added ApiPermission resource - Added User resource - Added Publication, CollectionPublication and ProductPublication resources - Added Currency resource - Added TenderTransaction resource - Added shopify.Limits class, for retrieving the current status of Shopify rate limiting. - Added support for Refund.calculate - Added support for Location.inventory_levels - Added support for PriceRule batch operations - Removed `cancel()` method for RecurringApplicationCharge resource (use `destroy()` going forward) - Fix for handling array query parameters (e.g. `foo[]=1&foo[]=2`) during HMAC calculation - Fixed Python 3 compatibility with the API console == Version 3.1.0 - Adds InventoryItem resource - Adds InventoryLevel resource - Adds GiftCardAdjustment resource - Fix to properly handle byte data for Asset.attach() == Version 3.0.0 - Added CollectListing resource - Added ResourceFeedback resource - Added StorefrontAccessToken resource - Added ProductListing resource - Removed deprecated ProductSearchEngine resource - Removed deprecated Discount resource - Fixed Python3 compatibility issue with `Image.attach_image()` == Version 2.6.0 - Added support for Marketing Event API through Marketing Event resource == Version 2.5.1 - Fixed an issue preventing creation of Order Risk resources == Version 2.5.0 - Added Price Rule and Discount Code resources == Version 2.4.0 - Add support for report publishing == Version 2.3.0 - Add support for customer#send_invite == Version 2.2.0 - Add support for draft orders == Version 2.1.8 - Added support for `open` method on fulfillments == Version 2.1.7 - Removed all references to the deprecated MD5 `signature` parameter which is no longer provided by Shopify. == Version 2.1.6 - Added Refund resource == Version 2.1.5 - bump pyactiveresource for camelcase bugfix == Version 2.1.4 == Version 2.1.3 - Fixed hmac signature validation for params with delimiters (`&`, `=` or `%`) == Version 2.1.2 - Fixed an issue with unicode strings in params passed to validate_hmac - Added shop domain verification when creating a session == Version 2.1.1 - Added Checkout resource - Updated to pyactiveresource v2.1.1 which includes a test-related bugfix - Changed OAuth validation from MD5 to HMAC-SHA256 == Version 2.1.0 - Added python 3 compatibility - Fixed setting the format attribute on carrier and fulfillment services - Add a specific exception for signature validation failures == Version 2.0.4 - Bug fixes - Added CarrierService resource - Added Property resource to LineItem == Version 2.0.3 - Add Order Risk resource == Version 2.0.2 - Add access to FulfillmentService endpoint - Fix some import bugs == Version 2.0.1 - Package bug fix == Version 2.0.0 - Removed support for legacy auth - Updated to pyactiveresource v2.0.0 which changes the default form to JSON - in Session::request_token params is no longer optional, you must pass all the params and the method will now extract the code - made create_permission_url an instance method, you'll need an instance of session to call this method from now on - Updated session.request_token - Updated Session to better match the ShopifyAPI Ruby gem - Updated the readme to better describe how to use the library - Added support for CustomerSavedSearch (CustomerGroup is deprecated) == Version 1.0.7 - Fix thread local headers to store a copy of the default hash which prevents activate_session in one thread from affecting other threads. == Version 1.0.6 - Fix deserializing and serializing fulfillments which can now contain arrays of strings in the tracking_urls attribute. == Version 1.0.5 - Fix parameter passing for order cancellation. - Fix Product.price_range method for variants with different prices. == Version 1.0.4 - Fixed another bug in Image size methods regex. == Version 1.0.3 - Fix bug in setting format attribute on Webhook instances. - Fixed missing slash in return value of Image size methods - Upgrade pyactiveresource to fix unicode encoding issues == Version 1.0.2 - Made ShopifyResource.clear_session idempotent. == Version 1.0.1 - Use the correct redirect parameter in Session.create_permission_url. Was redirect_url but corrected to redirect_uri. == Version 1.0.0 - Added support for OAuth2. - ShopifyResource.activate_session must now be used with OAuth2 instead of setting ShopifyResource.site directly. - Session.**init** no longer allows params to be passed in as \*\*params - Session.**init** now makes an HTTP request when using OAuth2 if params are specified - Session now exposes the access token through the token instance variable to simplify session saving and resuming == Version 0.4.0 - Using setup.py no longer requires all dependencies - More compatibility fixes for using the latest pyactiveresource - ShopifyResource.activate_session is not recommended over setting site directly for forward compatibility with coming OAuth2 changes. == Version 0.3.1 - Compatibility fixes for using latest (unreleased) pyactiveresource == Version 0.3.0 - Added support for customer search and customer group search. - Resource errors are cleared on save from previous save attempt. - Made the library thread-safe using thread-local connections. == Version 0.2.1 - Fixed a regression that caused a different connection object to be created on each resource. == Version 0.2.0 - Made responses available through the connection object. == Version 0.1.8 - Added ability to add metafields on customers. == Version 0.1.7 - Fixed missing theme_id in return value of Asset.find. == Version 0.1.6 - Fixed attribute setting on Asset objects - Strip path from shop_url to get just the shop's domain. == Version 0.1.5 - Fixed Asset.find() - Fixed Variant.find(id) - Allow running from source directory with PYTHONPATH=./lib == Version 0.1.4 - Fixed a bug in metafields method caused by missing import. - Prefix options can be specified in the attributes dict on creation - Allow count method to be used the same way as find == Version 0.1.3 - Fixed the automatic download of dependencies. - Updated the README instructions. == Version 0.1.2 - Add python 2.5 compatibility == Version 0.1.1 - Make creating a session simpler with django == Version 0.1.0 - ported ShopifyAPI from ruby to python ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/LICENSE0000644000076500000240000000204514605034740014710 0ustar00lizkenyibstaffCopyright (c) 2011 "JadedPixel inc." Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/MANIFEST.in0000644000076500000240000000011714605034740015437 0ustar00lizkenyibstaffinclude LICENSE include CHANGELOG include README.md include bin/shopify_api.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7342472 shopifyapi-12.7.0/PKG-INFO0000644000076500000240000000275114712210466015004 0ustar00lizkenyibstaffMetadata-Version: 2.1 Name: ShopifyAPI Version: 12.7.0 Summary: Shopify API for Python Home-page: https://github.com/Shopify/shopify_python_api Author: Shopify Author-email: developers@shopify.com License: MIT License Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: pyactiveresource>=2.2.2 Requires-Dist: PyJWT>=2.0.0 Requires-Dist: PyYAML>=6.0.1; python_version >= "3.12" Requires-Dist: PyYAML; python_version < "3.12" Requires-Dist: six The ShopifyAPI library allows python developers to programmatically access the admin section of stores using an ActiveResource like interface similar the ruby Shopify API gem. The library makes HTTP requests to Shopify in order to list, create, update, or delete resources (e.g. Order, Product, Collection). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727192681.0 shopifyapi-12.7.0/README.md0000644000076500000240000002614414674557151015204 0ustar00lizkenyibstaff# Shopify API [![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) ![Supported Python Versions](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-brightgreen) [![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/main/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/main/LICENSE) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library ## Usage ### Requirements You should be signed up as a partner on the [Shopify Partners Dashboard](https://www.shopify.com/partners) so that you can create and manage shopify applications. ### Installation To easily install or upgrade to the latest release, use [pip](http://www.pip-installer.org/). ```shell pip install --upgrade ShopifyAPI ``` ### Table of Contents - [Usage](#usage) - [Requirements](#requirements) - [Installation](#installation) - [Table of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Public and Custom Apps](#public-and-custom-apps) - [Private Apps](#private-apps) - [With full session](#with-full-session) - [With temporary session](#with-temporary-session) - [Billing](#billing) - [Advanced Usage](#advanced-usage) - [Prefix options](#prefix-options) - [Console](#console) - [GraphQL](#graphql) - [Using Development Version](#using-development-version) - [Building and installing dev version](#building-and-installing-dev-version) - [Running Tests](#running-tests) - [Relative Cursor Pagination](#relative-cursor-pagination) - [Set up pre-commit locally \[OPTIONAL\]](#set-up-pre-commit-locally-optional) - [Limitations](#limitations) - [Additional Resources](#additional-resources) - [Sample apps built using this library](#sample-apps-built-using-this-library) ### Getting Started #### Public and Custom Apps 1. First create a new application in the [Partners Dashboard](https://www.shopify.com/partners), and retrieve your API Key and API Secret Key. 1. We then need to supply these keys to the Shopify Session Class so that it knows how to authenticate. ```python import shopify shopify.Session.setup(api_key=API_KEY, secret=API_SECRET) ``` 1. In order to access a shop's data, apps need an access token from that specific shop. We need to authenticate with that shop using OAuth, which we can start in the following way: ```python 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(scopes, redirect_uri, state) # redirect to auth_url ``` 1. Once the merchant accepts, the shop redirects the owner to the `redirect_uri` of your application with a parameter named 'code'. This is a temporary token that the app can exchange for a permanent access token. You should compare the state you provided above with the one you received back to ensure the request is correct. Now we can exchange the code for an access_token when you get the request from shopify in your callback handler: ```python session = shopify.Session(shop_url, api_version) access_token = session.request_token(request_params) # request_token will validate hmac and timing attacks # you should save the access token now for future use. ``` 1. Now you're ready to make authorized API requests to your shop!: ```python session = shopify.Session(shop_url, api_version, access_token) shopify.ShopifyResource.activate_session(session) shop = shopify.Shop.current() # Get the current shop product = shopify.Product.find(179761209) # Get a specific product # execute a graphQL call shopify.GraphQL().execute("{ shop { name id } }") ``` Alternatively, you can use temp to initialize a Session and execute a command: ```python with shopify.Session.temp(shop_url, api_version, token): product = shopify.Product.find() ``` 1. It is best practice to clear your session when you're done. A temporary session does this automatically: ```python shopify.ShopifyResource.clear_session() ``` #### Private Apps Private apps are a bit quicker to use because OAuth is not needed. You can create the private app in the Shopify Merchant Admin. You can use the Private App password as your `access_token`: ##### With full session ```python session = shopify.Session(shop_url, api_version, private_app_password) shopify.ShopifyResource.activate_session(session) # ... shopify.ShopifyResource.clear_session() ``` ##### With temporary session ```python with shopify.Session.temp(shop_url, api_version, private_app_password): shopify.GraphQL().execute("{ shop { name id } }") ``` ### Billing _Note: Your application must be public to test the billing process. To test on a development store use the `'test': True` flag_ 1. Create charge after session has been activated ```python 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 so they can approve the charge ``` 1. After approving the charge, the user is redirected to `return_url` with `charge_id` parameter (_Note: This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)_) ```python charge = shopify.ApplicationCharge.find(charge_id) shopify.ApplicationCharge.activate(charge) ``` 1. Check that `activated_charge` status is `active` ```python activated_charge = shopify.ApplicationCharge.find(charge_id) has_been_billed = activated_charge.status == 'active' ``` ### Advanced Usage It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which is a port of rails/ActiveResource to Python and upon which this package relies heavily. Instances of `pyactiveresource` resources map to RESTful resources in the Shopify API. `pyactiveresource` exposes life cycle methods for creating, finding, updating, and deleting resources which are equivalent to the `POST`, `GET`, `PUT`, and `DELETE` HTTP verbs. ```python product = shopify.Product() product.title = "Shopify Logo T-Shirt" product.id # => 292082188312 product.save() # => True shopify.Product.exists(product.id) # => True product = shopify.Product.find(292082188312) # Resource holding our newly created Product object # Inspect attributes with product.attributes product.price = 19.99 product.save() # => True product.destroy() # Delete the resource from the remote server (i.e. Shopify) ``` Here is another example to retrieve a list of open orders using certain parameters: ```python new_orders = shopify.Order.find(status="open", limit="50") ``` ### Prefix options Some resources such as `Fulfillment` are prefixed by a parent resource in the Shopify API (e.g. `orders/450789469/fulfillments/255858046`). In order to interact with these resources, you must specify the identifier of the parent resource in your request. ```python shopify.Fulfillment.find(255858046, order_id=450789469) ``` ### Console This package also includes the `shopify_api.py` script to make it easy to open an interactive console to use the API with a shop. 1. Obtain a private API key and password to use with your shop (step 2 in "Getting Started") 1. Save your default credentials: `shopify_api.py add yourshopname` 1. Start the console for the connection: `shopify_api.py console` 1. To see the full list of commands, type: `shopify_api.py help` ### GraphQL This library also supports Shopify's new [GraphQL API](https://help.shopify.com/en/api/graphql-admin-api). The authentication process is identical. Once your session is activated, simply construct a new graphql client and use `execute` to execute the query. ```python result = shopify.GraphQL().execute('{ shop { name id } }') ``` You can perform more complex operations using the `variables` and `operation_name` parameters of `execute`. For example, this GraphQL document uses a fragment to construct two named queries - one for a single order, and one for multiple orders: ```graphql # ./order_queries.graphql fragment OrderInfo on Order { id name createdAt } query GetOneOrder($order_id: ID!){ node(id: $order_id){ ...OrderInfo } } query GetManyOrders($order_ids: [ID]!){ nodes(ids: $order_ids){ ...OrderInfo } } ``` Now you can choose which operation to execute: ```python # Load the document with both queries document = Path("./order_queries.graphql").read_text() # Specify the named operation to execute, and the parameters for the query result = shopify.GraphQL().execute( query=document, variables={"order_id": "gid://shopify/Order/12345"}, operation_name="GetOneOrder", ) ``` ## Using Development Version #### Building and installing dev version ```shell python setup.py sdist pip install --upgrade dist/ShopifyAPI-*.tar.gz ``` **Note** Use the `bin/shopify_api.py` script when running from the source tree. It will add the lib directory to start of sys.path, so the installed version won't be used. #### Running Tests ```shell pip install setuptools --upgrade python setup.py test ``` ## Relative Cursor Pagination Cursor based pagination support has been added in 6.0.0. ```python import shopify page1 = shopify.Product.find() if page1.has_next_page(): page2 = page1.next_page() # to persist across requests you can use next_page_url and previous_page_url next_url = page1.next_page_url page2 = shopify.Product.find(from_=next_url) ``` ## Set up pre-commit locally [OPTIONAL] [Pre-commit](https://pre-commit.com/) is set up as a GitHub action that runs on pull requests and pushes to the `main` branch. If you want to run pre-commit locally, install it and set up the git hook scripts ```shell pip install -r requirements.txt pre-commit install ``` ## Limitations Currently there is no support for: * asynchronous requests * persistent connections ## Additional Resources * [Partners Dashboard](https://partners.shopify.com) * [developers.shopify.com](https://developers.shopify.com) * [Shopify.dev](https://shopify.dev) * [Ask questions on the Shopify forums](http://ecommerce.shopify.com/c/shopify-apis-and-technology) ### Sample apps built using this library * [Sample Django app](https://github.com/shopify/sample-django-app) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7337174 shopifyapi-12.7.0/ShopifyAPI.egg-info/0000755000076500000240000000000014712210466017347 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744629.0 shopifyapi-12.7.0/ShopifyAPI.egg-info/PKG-INFO0000644000076500000240000000275114712210465020450 0ustar00lizkenyibstaffMetadata-Version: 2.1 Name: ShopifyAPI Version: 12.7.0 Summary: Shopify API for Python Home-page: https://github.com/Shopify/shopify_python_api Author: Shopify Author-email: developers@shopify.com License: MIT License Platform: Any Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Libraries :: Python Modules License-File: LICENSE Requires-Dist: pyactiveresource>=2.2.2 Requires-Dist: PyJWT>=2.0.0 Requires-Dist: PyYAML>=6.0.1; python_version >= "3.12" Requires-Dist: PyYAML; python_version < "3.12" Requires-Dist: six The ShopifyAPI library allows python developers to programmatically access the admin section of stores using an ActiveResource like interface similar the ruby Shopify API gem. The library makes HTTP requests to Shopify in order to list, create, update, or delete resources (e.g. Order, Product, Collection). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744629.0 shopifyapi-12.7.0/ShopifyAPI.egg-info/SOURCES.txt0000644000076500000240000000630314712210465021234 0ustar00lizkenyibstaffCHANGELOG LICENSE MANIFEST.in README.md pyproject.toml setup.py ShopifyAPI.egg-info/PKG-INFO ShopifyAPI.egg-info/SOURCES.txt ShopifyAPI.egg-info/dependency_links.txt ShopifyAPI.egg-info/requires.txt ShopifyAPI.egg-info/top_level.txt bin/shopify_api.py scripts/shopify_api.py shopify/__init__.py shopify/api_access.py shopify/api_version.py shopify/base.py shopify/collection.py shopify/limits.py shopify/mixins.py shopify/session.py shopify/session_token.py shopify/version.py shopify/yamlobjects.py shopify/resources/__init__.py shopify/resources/access_scope.py shopify/resources/address.py shopify/resources/api_permission.py shopify/resources/application_charge.py shopify/resources/application_credit.py shopify/resources/article.py shopify/resources/asset.py shopify/resources/balance.py shopify/resources/billing_address.py shopify/resources/blog.py shopify/resources/carrier_service.py shopify/resources/cart.py shopify/resources/checkout.py shopify/resources/collect.py shopify/resources/collection_listing.py shopify/resources/collection_publication.py shopify/resources/comment.py shopify/resources/country.py shopify/resources/currency.py shopify/resources/custom_collection.py shopify/resources/customer.py shopify/resources/customer_group.py shopify/resources/customer_invite.py shopify/resources/customer_saved_search.py shopify/resources/discount_code.py shopify/resources/discount_code_creation.py shopify/resources/disputes.py shopify/resources/draft_order.py shopify/resources/draft_order_invoice.py shopify/resources/event.py shopify/resources/fulfillment.py shopify/resources/fulfillment_event.py shopify/resources/fulfillment_service.py shopify/resources/gift_card.py shopify/resources/gift_card_adjustment.py shopify/resources/graphql.py shopify/resources/image.py shopify/resources/inventory_item.py shopify/resources/inventory_level.py shopify/resources/line_item.py shopify/resources/location.py shopify/resources/marketing_event.py shopify/resources/metafield.py shopify/resources/note_attribute.py shopify/resources/option.py shopify/resources/order.py shopify/resources/order_risk.py shopify/resources/page.py shopify/resources/payment_details.py shopify/resources/payouts.py shopify/resources/policy.py shopify/resources/price_rule.py shopify/resources/product.py shopify/resources/product_listing.py shopify/resources/product_publication.py shopify/resources/province.py shopify/resources/publication.py shopify/resources/receipt.py shopify/resources/recurring_application_charge.py shopify/resources/redirect.py shopify/resources/refund.py shopify/resources/report.py shopify/resources/resource_feedback.py shopify/resources/rule.py shopify/resources/script_tag.py shopify/resources/shipping_address.py shopify/resources/shipping_line.py shopify/resources/shipping_zone.py shopify/resources/shop.py shopify/resources/smart_collection.py shopify/resources/storefront_access_token.py shopify/resources/tax_line.py shopify/resources/tender_transaction.py shopify/resources/theme.py shopify/resources/transaction.py shopify/resources/transactions.py shopify/resources/usage_charge.py shopify/resources/user.py shopify/resources/variant.py shopify/resources/webhook.py shopify/utils/__init__.py shopify/utils/shop_url.py test/test_helper.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744629.0 shopifyapi-12.7.0/ShopifyAPI.egg-info/dependency_links.txt0000644000076500000240000000000114712210465023414 0ustar00lizkenyibstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744629.0 shopifyapi-12.7.0/ShopifyAPI.egg-info/requires.txt0000644000076500000240000000016714712210465021752 0ustar00lizkenyibstaffpyactiveresource>=2.2.2 PyJWT>=2.0.0 six [:python_version < "3.12"] PyYAML [:python_version >= "3.12"] PyYAML>=6.0.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744629.0 shopifyapi-12.7.0/ShopifyAPI.egg-info/top_level.txt0000644000076500000240000000005014712210465022073 0ustar00lizkenyibstaffshopify shopify/resources shopify/utils ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7126493 shopifyapi-12.7.0/bin/0000755000076500000240000000000014712210466014452 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/bin/shopify_api.py0000755000076500000240000000064014605034740017341 0ustar00lizkenyibstaff#!/usr/bin/env python """shopify_api.py wrapper script for running it the source directory""" import sys import os.path # Use the development rather than installed version project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, project_root) with open(os.path.join(project_root, "scripts", "shopify_api.py")) as f: code = compile(f.read(), f.name, "exec") exec(code) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/pyproject.toml0000644000076500000240000000010614605034740016613 0ustar00lizkenyibstaff[tool.autopep8] max_line_length = 120 [tool.black] line-length = 120 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7128608 shopifyapi-12.7.0/scripts/0000755000076500000240000000000014712210466015371 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/scripts/shopify_api.py0000755000076500000240000002244414605034740020266 0ustar00lizkenyibstaff#!/usr/bin/env python import shopify import code import sys import os import os.path import glob import subprocess import functools import yaml import six from six.moves import input, map def start_interpreter(**variables): # add the current working directory to the sys paths sys.path.append(os.getcwd()) try: from IPython import start_ipython from traitlets.config.loader import Config config = Config(TerminalInteractiveShell={"banner2": "(shopify %s)" % shopify.version.VERSION}) start_ipython(argv=[], user_ns=variables, config=config) except ImportError: console = type("shopify " + shopify.version.VERSION, (code.InteractiveConsole, object), {}) import readline console(variables).interact() class ConfigFileError(Exception): pass def usage(usage_string): """Decorator to add a usage string to a function""" def decorate(func): func.usage = usage_string return func return decorate class TasksMeta(type): _prog = os.path.basename(sys.argv[0]) def __new__(mcs, name, bases, new_attrs): cls = type.__new__(mcs, name, bases, new_attrs) tasks = list(new_attrs.keys()) tasks.append("help") def filter_func(item): return not item.startswith("_") and hasattr(getattr(cls, item), "__call__") tasks = filter(filter_func, tasks) cls._tasks = sorted(tasks) return cls def run_task(cls, task=None, *args): if task in [None, "-h", "--help"]: cls.help() return # Allow unambiguous abbreviations of tasks if task not in cls._tasks: matches = filter(lambda item: item.startswith(task), cls._tasks) list_of_matches = list(matches) if len(list_of_matches) == 1: task = list_of_matches[0] else: sys.stderr.write('Could not find task "%s".\n' % (task)) task_func = getattr(cls, task) task_func(*args) @usage("help [TASK]") def help(cls, task=None): """Describe available tasks or one specific task""" if task is None: usage_list = [] for task in iter(cls._tasks): task_func = getattr(cls, task) usage_string = " %s %s" % (cls._prog, task_func.usage) desc = task_func.__doc__.splitlines()[0] usage_list.append((usage_string, desc)) max_len = functools.reduce(lambda m, item: max(m, len(item[0])), usage_list, 0) print("Tasks:") cols = int(os.environ.get("COLUMNS", 80)) for line, desc in usage_list: task_func = getattr(cls, task) if desc: line = "%s%s # %s" % (line, " " * (max_len - len(line)), desc) if len(line) > cols: line = line[: cols - 3] + "..." print(line) else: task_func = getattr(cls, task) print("Usage:") print(" %s %s" % (cls._prog, task_func.usage)) print("") print(task_func.__doc__) @six.add_metaclass(TasksMeta) class Tasks(object): _shop_config_dir = os.path.join(os.environ["HOME"], ".shopify", "shops") _default_symlink = os.path.join(_shop_config_dir, "default") _default_api_version = "unstable" @classmethod @usage("list") def list(cls): """list available connections""" for c in cls._available_connections(): prefix = " * " if cls._is_default(c) else " " print(prefix + c) @classmethod @usage("add CONNECTION") def add(cls, connection): """create a config file for a connection named CONNECTION""" filename = cls._get_config_filename(connection) if os.path.exists(filename): raise ConfigFileError("There is already a config file at " + filename) else: config = dict(protocol="https") domain = input("Domain? (leave blank for %s.myshopify.com) " % (connection)) if not domain.strip(): domain = "%s.myshopify.com" % (connection) config["domain"] = domain print("") print("open https://%s/admin/apps/private in your browser to generate API credentials" % (domain)) config["api_key"] = input("API key? ") config["password"] = input("Password? ") config["api_version"] = input("API version? (leave blank for %s) " % (cls._default_api_version)) if not config["api_version"].strip(): config["api_version"] = cls._default_api_version if not os.path.isdir(cls._shop_config_dir): os.makedirs(cls._shop_config_dir) with open(filename, "w") as f: f.write(yaml.dump(config, default_flow_style=False, explicit_start="---")) if len(list(cls._available_connections())) == 1: cls.default(connection) @classmethod @usage("remove CONNECTION") def remove(cls, connection): """remove the config file for CONNECTION""" filename = cls._get_config_filename(connection) if os.path.exists(filename): if cls._is_default(connection): os.remove(cls._default_symlink) os.remove(filename) else: cls._no_config_file_error(filename) @classmethod @usage("edit [CONNECTION]") def edit(cls, connection=None): """open the config file for CONNECTION with you default editor""" filename = cls._get_config_filename(connection) if os.path.exists(filename): editor = os.environ.get("EDITOR") if editor: subprocess.call([editor, filename]) else: print("Please set an editor in the EDITOR environment variable") else: cls._no_config_file_error(filename) @classmethod @usage("show [CONNECTION]") def show(cls, connection=None): """output the location and contents of the CONNECTION's config file""" if connection is None: connection = cls._default_connection() filename = cls._get_config_filename(connection) if os.path.exists(filename): print(filename) with open(filename) as f: print(f.read()) else: cls._no_config_file_error(filename) @classmethod @usage("default [CONNECTION]") def default(cls, connection=None): """show the default connection, or make CONNECTION the default""" if connection is not None: target = cls._get_config_filename(connection) if os.path.exists(target): if os.path.exists(cls._default_symlink): os.remove(cls._default_symlink) os.symlink(target, cls._default_symlink) else: cls._no_config_file_error(target) if os.path.exists(cls._default_symlink): print("Default connection is " + cls._default_connection()) else: print("There is no default connection set") @classmethod @usage("console [CONNECTION]") def console(cls, connection=None): """start an API console for CONNECTION""" filename = cls._get_config_filename(connection) if not os.path.exists(filename): cls._no_config_file_error(filename) with open(filename) as f: config = yaml.safe_load(f.read()) print("using %s" % (config["domain"])) session = cls._session_from_config(config) shopify.ShopifyResource.activate_session(session) start_interpreter(shopify=shopify) @classmethod @usage("version") def version(cls): """output the shopify library version""" print(shopify.version.VERSION) @classmethod def _available_connections(cls): return map( lambda item: os.path.splitext(os.path.basename(item))[0], glob.glob(os.path.join(cls._shop_config_dir, "*.yml")), ) @classmethod def _default_connection_target(cls): if not os.path.exists(cls._default_symlink): return None target = os.readlink(cls._default_symlink) return os.path.join(cls._shop_config_dir, target) @classmethod def _default_connection(cls): target = cls._default_connection_target() if not target: return None return os.path.splitext(os.path.basename(target))[0] @classmethod def _get_config_filename(cls, connection): if connection is None: return cls._default_symlink else: return os.path.join(cls._shop_config_dir, connection + ".yml") @classmethod def _session_from_config(cls, config): session = shopify.Session(config.get("domain"), config.get("api_version", cls._default_api_version)) session.protocol = config.get("protocol", "https") session.api_key = config.get("api_key") session.token = config.get("password") return session @classmethod def _is_default(cls, connection): return connection == cls._default_connection() @classmethod def _no_config_file_error(cls, filename): raise ConfigFileError("There is no config file at " + filename) try: Tasks.run_task(*sys.argv[1:]) except ConfigFileError as e: print(e) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7347353 shopifyapi-12.7.0/setup.cfg0000644000076500000240000000004614712210466015523 0ustar00lizkenyibstaff[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727192681.0 shopifyapi-12.7.0/setup.py0000755000076500000240000000354514674557151015442 0ustar00lizkenyibstafffrom setuptools import setup NAME = "ShopifyAPI" exec(open("shopify/version.py").read()) DESCRIPTION = "Shopify API for Python" LONG_DESCRIPTION = """\ The ShopifyAPI library allows python developers to programmatically access the admin section of stores using an ActiveResource like interface similar the ruby Shopify API gem. The library makes HTTP requests to Shopify in order to list, create, update, or delete resources (e.g. Order, Product, Collection).""" setup( name=NAME, version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, author="Shopify", author_email="developers@shopify.com", url="https://github.com/Shopify/shopify_python_api", packages=["shopify", "shopify/resources", "shopify/utils"], scripts=["scripts/shopify_api.py"], license="MIT License", install_requires=[ "pyactiveresource>=2.2.2", "PyJWT >= 2.0.0", "PyYAML>=6.0.1; python_version>='3.12'", "PyYAML; python_version<'3.12'", "six", ], test_suite="test", tests_require=[ "mock>=1.0.1", ], platforms="Any", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", ], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7155704 shopifyapi-12.7.0/shopify/0000755000076500000240000000000014712210466015363 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/__init__.py0000644000076500000240000000042314605034740017473 0ustar00lizkenyibstafffrom shopify.version import VERSION from shopify.session import Session, ValidationException from shopify.resources import * from shopify.limits import Limits from shopify.api_version import * from shopify.api_access import * from shopify.collection import PaginatedIterator ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/api_access.py0000644000076500000240000000373214605034740020034 0ustar00lizkenyibstaffimport re import sys def basestring_type(): if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 return basestring else: return str class ApiAccessError(Exception): pass class ApiAccess: SCOPE_DELIMITER = "," SCOPE_RE = re.compile(r"\A(?Punauthenticated_)?(write|read)_(?P.*)\Z") IMPLIED_SCOPE_RE = re.compile(r"\A(?Punauthenticated_)?write_(?P.*)\Z") def __init__(self, scopes): if isinstance(scopes, basestring_type()): scopes = scopes.split(self.SCOPE_DELIMITER) self.__store_scopes(scopes) def covers(self, api_access): return api_access._compressed_scopes <= self._expanded_scopes def __str__(self): return self.SCOPE_DELIMITER.join(self._compressed_scopes) def __iter__(self): return iter(self._compressed_scopes) def __eq__(self, other): return type(self) == type(other) and self._compressed_scopes == other._compressed_scopes def __store_scopes(self, scopes): sanitized_scopes = frozenset(filter(None, [scope.strip() for scope in scopes])) self.__validate_scopes(sanitized_scopes) implied_scopes = frozenset(self.__implied_scope(scope) for scope in sanitized_scopes) self._compressed_scopes = sanitized_scopes - implied_scopes self._expanded_scopes = sanitized_scopes.union(implied_scopes) def __validate_scopes(self, scopes): for scope in scopes: if not self.SCOPE_RE.match(scope): error_message = "'{s}' is not a valid access scope".format(s=scope) raise ApiAccessError(error_message) def __implied_scope(self, scope): match = self.IMPLIED_SCOPE_RE.match(scope) if match: return "{unauthenticated}read_{resource}".format( unauthenticated=match.group("unauthenticated") or "", resource=match.group("resource"), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744343.0 shopifyapi-12.7.0/shopify/api_version.py0000644000076500000240000000471414712210027020252 0ustar00lizkenyibstaffimport re class InvalidVersionError(Exception): pass class VersionNotFoundError(Exception): pass class ApiVersion(object): versions = {} @classmethod def coerce_to_version(cls, version): try: return cls.versions[version] except KeyError: # Dynamically create a new Release object if version string is not found if Release.FORMAT.match(version): return Release(version) raise VersionNotFoundError @classmethod def define_version(cls, version): cls.versions[version.name] = version return version @classmethod def define_known_versions(cls): cls.define_version(Unstable()) cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) cls.define_version(Release("2022-04")) cls.define_version(Release("2022-07")) cls.define_version(Release("2022-10")) cls.define_version(Release("2023-01")) cls.define_version(Release("2023-04")) cls.define_version(Release("2023-07")) cls.define_version(Release("2023-10")) cls.define_version(Release("2024-01")) cls.define_version(Release("2024-04")) cls.define_version(Release("2024-07")) cls.define_version(Release("2024-10")) @classmethod def clear_defined_versions(cls): cls.versions = {} @property def numeric_version(self): return self._numeric_version @property def name(self): return self._name def api_path(self, site): return site + self._path def __eq__(self, other): if not isinstance(other, type(self)): return False return self.numeric_version == int(other.numeric_version) class Release(ApiVersion): FORMAT = re.compile(r"^\d{4}-\d{2}$") API_PREFIX = "/admin/api" def __init__(self, version_number): if not self.FORMAT.match(version_number): raise InvalidVersionError self._name = version_number self._numeric_version = int(version_number.replace("-", "")) self._path = "%s/%s" % (self.API_PREFIX, version_number) @property def stable(self): return True class Unstable(ApiVersion): def __init__(self): self._name = "unstable" self._numeric_version = 9000000 self._path = "/admin/api/unstable" @property def stable(self): return False ApiVersion.define_known_versions() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/base.py0000644000076500000240000001642014605034740016652 0ustar00lizkenyibstaffimport pyactiveresource.connection from pyactiveresource.activeresource import ActiveResource, ResourceMeta, formats import shopify.yamlobjects import shopify.mixins as mixins import shopify import threading import sys from six.moves import urllib import six from shopify.collection import PaginatedCollection from pyactiveresource.collection import Collection # Store the response from the last request in the connection object class ShopifyConnection(pyactiveresource.connection.Connection): response = None def _open(self, *args, **kwargs): self.response = None try: self.response = super(ShopifyConnection, self)._open(*args, **kwargs) except pyactiveresource.connection.ConnectionError as err: self.response = err.response raise return self.response # Inherit from pyactiveresource's metaclass in order to use ShopifyConnection class ShopifyResourceMeta(ResourceMeta): @property def connection(cls): """HTTP connection for the current thread""" local = cls._threadlocal if not getattr(local, "connection", None): # Make sure these variables are no longer affected by other threads. local.user = cls.user local.password = cls.password local.site = cls.site local.timeout = cls.timeout local.headers = cls.headers local.format = cls.format local.version = cls.version local.url = cls.url if cls.site is None: raise ValueError("No shopify session is active") local.connection = ShopifyConnection(cls.site, cls.user, cls.password, cls.timeout, cls.format) return local.connection def get_user(cls): return getattr(cls._threadlocal, "user", ShopifyResource._user) def set_user(cls, value): cls._threadlocal.connection = None ShopifyResource._user = cls._threadlocal.user = value user = property(get_user, set_user, None, "The username for HTTP Basic Auth.") def get_password(cls): return getattr(cls._threadlocal, "password", ShopifyResource._password) def set_password(cls, value): cls._threadlocal.connection = None ShopifyResource._password = cls._threadlocal.password = value password = property(get_password, set_password, None, "The password for HTTP Basic Auth.") def get_site(cls): return getattr(cls._threadlocal, "site", ShopifyResource._site) def set_site(cls, value): cls._threadlocal.connection = None ShopifyResource._site = cls._threadlocal.site = value if value is not None: parts = urllib.parse.urlparse(value) host = parts.hostname if parts.port: host += ":" + str(parts.port) new_site = urllib.parse.urlunparse((parts.scheme, host, parts.path, "", "", "")) ShopifyResource._site = cls._threadlocal.site = new_site if parts.username: cls.user = urllib.parse.unquote(parts.username) if parts.password: cls.password = urllib.parse.unquote(parts.password) site = property(get_site, set_site, None, "The base REST site to connect to.") def get_timeout(cls): return getattr(cls._threadlocal, "timeout", ShopifyResource._timeout) def set_timeout(cls, value): cls._threadlocal.connection = None ShopifyResource._timeout = cls._threadlocal.timeout = value timeout = property(get_timeout, set_timeout, None, "Socket timeout for HTTP requests") def get_headers(cls): if not hasattr(cls._threadlocal, "headers"): cls._threadlocal.headers = ShopifyResource._headers.copy() return cls._threadlocal.headers def set_headers(cls, value): cls._threadlocal.headers = value headers = property(get_headers, set_headers, None, "The headers sent with HTTP requests") def get_format(cls): return getattr(cls._threadlocal, "format", ShopifyResource._format) def set_format(cls, value): cls._threadlocal.connection = None ShopifyResource._format = cls._threadlocal.format = value format = property(get_format, set_format, None, "Encoding used for request and responses") def get_prefix_source(cls): """Return the prefix source, by default derived from site.""" try: return cls.override_prefix() except AttributeError: if hasattr(cls, "_prefix_source"): return cls.site + cls._prefix_source else: return cls.site def set_prefix_source(cls, value): """Set the prefix source, which will be rendered into the prefix.""" cls._prefix_source = value prefix_source = property(get_prefix_source, set_prefix_source, None, "prefix for lookups for this type of object.") def get_version(cls): if hasattr(cls._threadlocal, "version") or ShopifyResource._version: return getattr(cls._threadlocal, "version", ShopifyResource._version) elif ShopifyResource._site is not None: return ShopifyResource._site.split("/")[-1] def set_version(cls, value): ShopifyResource._version = cls._threadlocal.version = value version = property(get_version, set_version, None, "Shopify Api Version") def get_url(cls): return getattr(cls._threadlocal, "url", ShopifyResource._url) def set_url(cls, value): ShopifyResource._url = cls._threadlocal.url = value url = property(get_url, set_url, None, "Base URL including protocol and shopify domain") @six.add_metaclass(ShopifyResourceMeta) class ShopifyResource(ActiveResource, mixins.Countable): _format = formats.JSONFormat _threadlocal = threading.local() _headers = {"User-Agent": "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0])} _version = None _url = None def __init__(self, attributes=None, prefix_options=None): if attributes is not None and prefix_options is None: prefix_options, attributes = self.__class__._split_options(attributes) return super(ShopifyResource, self).__init__(attributes, prefix_options) def is_new(self): return not self.id def _load_attributes_from_response(self, response): if response.body.strip(): self._update(self.__class__.format.decode(response.body)) @classmethod def activate_session(cls, session): cls.site = session.site cls.url = session.url cls.user = None cls.password = None cls.version = session.api_version.name cls.headers["X-Shopify-Access-Token"] = session.token @classmethod def clear_session(cls): cls.site = None cls.url = None cls.user = None cls.password = None cls.version = None cls.headers.pop("X-Shopify-Access-Token", None) @classmethod def find(cls, id_=None, from_=None, **kwargs): """Checks the resulting collection for pagination metadata.""" collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, **kwargs) if isinstance(collection, Collection) and "headers" in collection.metadata: return PaginatedCollection(collection, metadata={"resource_class": cls}, **kwargs) return collection ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/collection.py0000644000076500000240000001226314605034740020074 0ustar00lizkenyibstafffrom pyactiveresource.collection import Collection class PaginatedCollection(Collection): """ A subclass of Collection which allows cycling through pages of data through cursor-based pagination. :next_page_url contains a url for fetching the next page :previous_page_url contains a url for fetching the previous page You can use next_page_url and previous_page_url to fetch the next page of data by calling Resource.find(from_=page.next_page_url) """ def __init__(self, *args, **kwargs): """If given a Collection object as an argument, inherit its metadata.""" metadata = kwargs.pop("metadata", None) obj = args[0] if isinstance(obj, Collection): if metadata: metadata.update(obj.metadata) else: metadata = obj.metadata super(PaginatedCollection, self).__init__(obj, metadata=metadata) else: super(PaginatedCollection, self).__init__(metadata=metadata or {}, *args, **kwargs) if not ("resource_class" in self.metadata): raise AttributeError('Cursor-based pagination requires a "resource_class" attribute in the metadata.') self.metadata["pagination"] = self.__parse_pagination() self.next_page_url = self.metadata["pagination"].get("next", None) self.previous_page_url = self.metadata["pagination"].get("previous", None) self._next = None self._previous = None self._current_iter = None self._no_iter_next = kwargs.pop("no_iter_next", True) def __parse_pagination(self): if "headers" not in self.metadata: return {} values = self.metadata["headers"].get("Link", self.metadata["headers"].get("link", None)) if values is None: return {} result = {} for value in values.split(", "): link, rel = value.split("; ") result[rel.split('"')[1]] = link[1:-1] return result def has_previous_page(self): """Returns true if the current page has any previous pages before it.""" return bool(self.previous_page_url) def has_next_page(self): """Returns true if the current page has any pages beyond the current position.""" return bool(self.next_page_url) def previous_page(self, no_cache=False): """Returns the previous page of items. Args: no_cache: If true the page will not be cached. Returns: A PaginatedCollection object with the new data set. """ if self._previous: return self._previous elif not self.has_previous_page(): raise IndexError("No previous page") return self.__fetch_page(self.previous_page_url, no_cache) def next_page(self, no_cache=False): """Returns the next page of items. Args: no_cache: If true the page will not be cached. Returns: A PaginatedCollection object with the new data set. """ if self._next: return self._next elif not self.has_next_page(): raise IndexError("No next page") return self.__fetch_page(self.next_page_url, no_cache) def __fetch_page(self, url, no_cache=False): next = self.metadata["resource_class"].find(from_=url) if not no_cache: self._next = next self._next._previous = self next._no_iter_next = self._no_iter_next return next def __iter__(self): """Iterates through all items, also fetching other pages.""" for item in super(PaginatedCollection, self).__iter__(): yield item if self._no_iter_next: return try: if not self._current_iter: self._current_iter = self self._current_iter = self.next_page() for item in self._current_iter: yield item except IndexError: return def __len__(self): """If fetched count all the pages.""" if self._next: count = len(self._next) else: count = 0 return count + super(PaginatedCollection, self).__len__() class PaginatedIterator(object): """ This class implements an iterator over paginated collections which aims to be more memory-efficient by not keeping more than one page in memory at a time. >>> from shopify import Product, PaginatedIterator >>> for page in PaginatedIterator(Product.find()): ... for item in page: ... do_something(item) ... # every page and the page items are iterated """ def __init__(self, collection): if not isinstance(collection, PaginatedCollection): raise TypeError("PaginatedIterator expects a PaginatedCollection instance") self.collection = collection self.collection._no_iter_next = True def __iter__(self): """Iterate over pages, returning one page at a time.""" current_page = self.collection while True: yield current_page try: current_page = current_page.next_page(no_cache=True) except IndexError: return ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/limits.py0000644000076500000240000000311314605034740017234 0ustar00lizkenyibstaffimport shopify class Limits(object): """ API Calls Limit https://help.shopify.com/en/api/getting-started/api-call-limit Conversion of lib/shopify_api/limits.rb """ # num_requests_executed/max_requests # Eg: 1/40 CREDIT_LIMIT_HEADER_PARAM = "X-Shopify-Shop-Api-Call-Limit" @classmethod def response(cls): if not shopify.Shop.connection.response: shopify.Shop.current() return shopify.Shop.connection.response @classmethod def api_credit_limit_param(cls): response = cls.response() _safe_header = getattr(response, "headers", "") if not _safe_header: raise Exception("No shopify headers found") if cls.CREDIT_LIMIT_HEADER_PARAM in response.headers: credits = response.headers[cls.CREDIT_LIMIT_HEADER_PARAM] return credits.split("/") else: raise Exception("No valid api call header found") @classmethod def credit_left(cls): """ How many more API calls can I make? """ return int(cls.credit_limit() - cls.credit_used()) @classmethod def credit_maxed(cls): """ Have I reached my API call limit? """ return bool(cls.credit_left() <= 0) @classmethod def credit_limit(cls): """ How many total API calls can I make? """ return int(cls.api_credit_limit_param()[1]) @classmethod def credit_used(cls): """ How many API calls have I made? """ return int(cls.api_credit_limit_param()[0]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/mixins.py0000644000076500000240000000211414605034740017242 0ustar00lizkenyibstaffimport shopify.resources class Countable(object): @classmethod def count(cls, _options=None, **kwargs): if _options is None: _options = kwargs return int(cls.get("count", **_options)) class Metafields(object): def metafields(self, _options=None, **kwargs): if _options is None: _options = kwargs return shopify.resources.Metafield.find(resource=self.__class__.plural, resource_id=self.id, **_options) def metafields_count(self, _options=None, **kwargs): if _options is None: _options = kwargs return int(self.get("metafields/count", **_options)) def add_metafield(self, metafield): if self.is_new(): raise ValueError("You can only add metafields to a resource that has been saved") metafield._prefix_options = dict(resource=self.__class__.plural, resource_id=self.id) metafield.save() return metafield class Events(object): def events(self): return shopify.resources.Event.find(resource=self.__class__.plural, resource_id=self.id) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1730744629.732755 shopifyapi-12.7.0/shopify/resources/0000755000076500000240000000000014712210466017375 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727192681.0 shopifyapi-12.7.0/shopify/resources/__init__.py0000644000076500000240000000573314674557151021532 0ustar00lizkenyibstafffrom .shop import Shop from .product import Product from .cart import Cart from .checkout import Checkout from .custom_collection import CustomCollection from .collect import Collect from .shipping_address import ShippingAddress from .billing_address import BillingAddress from .line_item import LineItem from .shipping_line import ShippingLine from .note_attribute import NoteAttribute from .address import Address from .option import Option from .payment_details import PaymentDetails from .receipt import Receipt from .rule import Rule from .tax_line import TaxLine from .script_tag import ScriptTag from .application_charge import ApplicationCharge from .application_credit import ApplicationCredit from .recurring_application_charge import RecurringApplicationCharge from .usage_charge import UsageCharge from .asset import Asset from .theme import Theme from .currency import Currency from .customer_saved_search import CustomerSavedSearch from .customer_group import CustomerGroup from .customer_invite import CustomerInvite from .customer import Customer from .event import Event from .webhook import Webhook from .redirect import Redirect from .province import Province from .comment import Comment from .metafield import Metafield from .article import Article from .blog import Blog from .page import Page from .country import Country from .refund import Refund from .fulfillment import Fulfillment, FulfillmentOrders, FulfillmentV2 from .fulfillment_event import FulfillmentEvent from .fulfillment_service import FulfillmentService from .carrier_service import CarrierService from .transaction import Transaction from .tender_transaction import TenderTransaction from .image import Image from .variant import Variant from .order import Order from .balance import Balance from .disputes import Disputes from .payouts import Payouts from .transactions import Transactions from .order_risk import OrderRisk from .policy import Policy from .smart_collection import SmartCollection from .gift_card import GiftCard from .gift_card_adjustment import GiftCardAdjustment from .shipping_zone import ShippingZone from .location import Location from .draft_order import DraftOrder from .draft_order_invoice import DraftOrderInvoice from .report import Report from .price_rule import PriceRule from .discount_code import DiscountCode from .discount_code_creation import DiscountCodeCreation from .marketing_event import MarketingEvent from .collection_listing import CollectionListing from .product_listing import ProductListing from .resource_feedback import ResourceFeedback from .storefront_access_token import StorefrontAccessToken from .inventory_item import InventoryItem from .inventory_level import InventoryLevel from .access_scope import AccessScope from .user import User from .api_permission import ApiPermission from .publication import Publication from .collection_publication import CollectionPublication from .product_publication import ProductPublication from .graphql import GraphQL from ..base import ShopifyResource ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/access_scope.py0000644000076500000240000000022614605034740022401 0ustar00lizkenyibstafffrom ..base import ShopifyResource class AccessScope(ShopifyResource): @classmethod def override_prefix(cls): return "/admin/oauth" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/address.py0000644000076500000240000000011614605034740021372 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Address(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/api_permission.py0000644000076500000240000000036114605034740022770 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ApiPermission(ShopifyResource): @classmethod def delete(cls): cls.connection.delete(cls.site + "/api_permissions/current." + cls.format.extension, cls.headers) destroy = delete ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/application_charge.py0000644000076500000240000000025214605034740023562 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ApplicationCharge(ShopifyResource): def activate(self): self._load_attributes_from_response(self.post("activate")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/application_credit.py0000644000076500000240000000013014605034740023576 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ApplicationCredit(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/article.py0000644000076500000240000000123014605034740021366 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins from .comment import Comment class Article(ShopifyResource, mixins.Metafields, mixins.Events): _prefix_source = "/blogs/$blog_id/" @classmethod def _prefix(cls, options={}): blog_id = options.get("blog_id") if blog_id: return "%s/blogs/%s" % (cls.site, blog_id) else: return cls.site def comments(self): return Comment.find(article_id=self.id) @classmethod def authors(cls, **kwargs): return cls.get("authors", **kwargs) @classmethod def tags(cls, **kwargs): return cls.get("tags", **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/asset.py0000644000076500000240000000506514605034740021074 0ustar00lizkenyibstafffrom ..base import ShopifyResource import base64 class Asset(ShopifyResource): _primary_key = "key" _prefix_source = "/themes/$theme_id/" @classmethod def _prefix(cls, options={}): theme_id = options.get("theme_id") if theme_id: return "%s/themes/%s" % (cls.site, theme_id) else: return cls.site @classmethod def _element_path(cls, id, prefix_options={}, query_options=None): if query_options is None: prefix_options, query_options = cls._split_options(prefix_options) return "%s%s.%s%s" % ( cls._prefix(prefix_options) + "/", cls.plural, cls.format.extension, cls._query_string(query_options), ) @classmethod def find(cls, key=None, **kwargs): """ Find an asset by key E.g. shopify.Asset.find('layout/theme.liquid', theme_id=99) """ if not key: return super(Asset, cls).find(**kwargs) params = {"asset[key]": key} params.update(kwargs) theme_id = params.get("theme_id") path_prefix = "%s/themes/%s" % (cls.site, theme_id) if theme_id else cls.site resource = cls.find_one("%s/assets.%s" % (path_prefix, cls.format.extension), **params) if theme_id and resource: resource._prefix_options["theme_id"] = theme_id return resource def __get_value(self): data = self.attributes.get("value") if data: return data data = self.attributes.get("attachment") if data: return base64.b64decode(data).decode() def __set_value(self, data): self.__wipe_value_attributes() self.attributes["value"] = data value = property(__get_value, __set_value, None, "The asset's value or attachment") def attach(self, data): self.attachment = base64.b64encode(data).decode() def destroy(self): options = {"asset[key]": self.key} options.update(self._prefix_options) return self.__class__.connection.delete(self._element_path(self.key, options), self.__class__.headers) def is_new(self): return False def __setattr__(self, name, value): if name in ("value", "attachment", "src", "source_key"): self.__wipe_value_attributes() return super(Asset, self).__setattr__(name, value) def __wipe_value_attributes(self): for attr in ("value", "attachment", "src", "source_key"): if attr in self.attributes: del self.attributes[attr] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/balance.py0000644000076500000240000000030114605034740021326 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Balance(ShopifyResource, mixins.Metafields): _prefix_source = "/shopify_payments/" _singular = _plural = "balance" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/billing_address.py0000644000076500000240000000012514605034740023072 0ustar00lizkenyibstafffrom ..base import ShopifyResource class BillingAddress(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/blog.py0000644000076500000240000000033314605034740020671 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins import shopify class Blog(ShopifyResource, mixins.Metafields, mixins.Events): def articles(self): return shopify.Article.find(blog_id=self.id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/carrier_service.py0000644000076500000240000000045614605034740023123 0ustar00lizkenyibstafffrom ..base import ShopifyResource class CarrierService(ShopifyResource): def __get_format(self): return self.attributes.get("format") def __set_format(self, data): self.attributes["format"] = data format = property(__get_format, __set_format, None, "Format attribute") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/cart.py0000644000076500000240000000011314605034740020673 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Cart(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/checkout.py0000644000076500000240000000011714605034740021553 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Checkout(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/collect.py0000644000076500000240000000011614605034740021372 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Collect(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/collection_listing.py0000644000076500000240000000030714605034740023633 0ustar00lizkenyibstafffrom ..base import ShopifyResource class CollectionListing(ShopifyResource): _primary_key = "collection_id" def product_ids(cls, **kwargs): return cls.get("product_ids", **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/collection_publication.py0000644000076500000240000000021114605034740024465 0ustar00lizkenyibstafffrom ..base import ShopifyResource class CollectionPublication(ShopifyResource): _prefix_source = "/publications/$publication_id/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/comment.py0000644000076500000240000000100014605034740021400 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Comment(ShopifyResource): def remove(self): self._load_attributes_from_response(self.post("remove")) def spam(self): self._load_attributes_from_response(self.post("spam")) def approve(self): self._load_attributes_from_response(self.post("approve")) def restore(self): self._load_attributes_from_response(self.post("restore")) def not_spam(self): self._load_attributes_from_response(self.post("not_spam")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/country.py0000644000076500000240000000011614605034740021450 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Country(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/currency.py0000644000076500000240000000011714605034740021600 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Currency(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/custom_collection.py0000644000076500000240000000104314605034740023472 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins import shopify class CustomCollection(ShopifyResource, mixins.Metafields, mixins.Events): def products(self): return shopify.Product.find(collection_id=self.id) def add_product(self, product): return shopify.Collect.create({"collection_id": self.id, "product_id": product.id}) def remove_product(self, product): collect = shopify.Collect.find_first(collection_id=self.id, product_id=product.id) if collect: collect.destroy() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/customer.py0000644000076500000240000000201714605034740021610 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins from .customer_invite import CustomerInvite from .order import Order class Customer(ShopifyResource, mixins.Metafields): @classmethod def search(cls, **kwargs): """ Search for customers matching supplied query Args: order: Field and direction to order results by (default: last_order_date DESC) query: Text to search for customers page: Page to show (default: 1) limit: Amount of results (default: 50) (maximum: 250) fields: comma-separated list of fields to include in the response Returns: A Collection of customers. """ return cls._build_collection(cls.get("search", **kwargs)) def send_invite(self, customer_invite=CustomerInvite()): resource = self.post("send_invite", customer_invite.encode()) return CustomerInvite(Customer.format.decode(resource.body)) def orders(self): return Order.find(customer_id=self.id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/customer_group.py0000644000076500000240000000015414605034740023024 0ustar00lizkenyibstafffrom .customer_saved_search import CustomerSavedSearch class CustomerGroup(CustomerSavedSearch): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/customer_invite.py0000644000076500000240000000012514605034740023164 0ustar00lizkenyibstafffrom ..base import ShopifyResource class CustomerInvite(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/customer_saved_search.py0000644000076500000240000000033414605034740024317 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .customer import Customer class CustomerSavedSearch(ShopifyResource): def customers(cls, **kwargs): return Customer._build_collection(cls.get("customers", **kwargs)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/discount_code.py0000644000076500000240000000017614605034740022575 0ustar00lizkenyibstafffrom ..base import ShopifyResource class DiscountCode(ShopifyResource): _prefix_source = "/price_rules/$price_rule_id/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/discount_code_creation.py0000644000076500000240000000100514605034740024451 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .discount_code import DiscountCode class DiscountCodeCreation(ShopifyResource): _prefix_source = "/price_rules/$price_rule_id/" def discount_codes(self): return DiscountCode.find( from_="%s/price_rules/%s/batch/%s/discount_codes.%s" % ( ShopifyResource.site, self._prefix_options["price_rule_id"], self.id, DiscountCodeCreation.format.extension, ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/disputes.py0000644000076500000240000000023614605034740021610 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Disputes(ShopifyResource, mixins.Metafields): _prefix_source = "/shopify_payments/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/draft_order.py0000644000076500000240000000121214605034740022236 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins from .draft_order_invoice import DraftOrderInvoice class DraftOrder(ShopifyResource, mixins.Metafields): def send_invoice(self, draft_order_invoice=DraftOrderInvoice()): resource = self.post("send_invoice", draft_order_invoice.encode()) return DraftOrderInvoice(DraftOrder.format.decode(resource.body)) def complete(self, params={}): if params.get("payment_pending", False): self._load_attributes_from_response(self.put("complete", payment_pending="true")) else: self._load_attributes_from_response(self.put("complete")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/draft_order_invoice.py0000644000076500000240000000013014605034740023750 0ustar00lizkenyibstafffrom ..base import ShopifyResource class DraftOrderInvoice(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/event.py0000644000076500000240000000053614605034740021074 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Event(ShopifyResource): _prefix_source = "/$resource/$resource_id/" @classmethod def _prefix(cls, options={}): resource = options.get("resource") if resource: return "%s/%s/%s" % (cls.site, resource, options["resource_id"]) else: return cls.site ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/fulfillment.py0000644000076500000240000000202014605034740022262 0ustar00lizkenyibstafffrom ..base import ShopifyResource import json class Fulfillment(ShopifyResource): _prefix_source = "/orders/$order_id/" def cancel(self): self._load_attributes_from_response(self.post("cancel")) def complete(self): self._load_attributes_from_response(self.post("complete")) def open(self): self._load_attributes_from_response(self.post("open")) def update_tracking(self, tracking_info, notify_customer): fulfill = FulfillmentV2() fulfill.id = self.id self._load_attributes_from_response(fulfill.update_tracking(tracking_info, notify_customer)) class FulfillmentOrders(ShopifyResource): _prefix_source = "/orders/$order_id/" class FulfillmentV2(ShopifyResource): _singular = "fulfillment" _plural = "fulfillments" def update_tracking(self, tracking_info, notify_customer): body = {"fulfillment": {"tracking_info": tracking_info, "notify_customer": notify_customer}} return self.post("update_tracking", json.dumps(body).encode()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/fulfillment_event.py0000644000076500000240000000170114605034740023470 0ustar00lizkenyibstafffrom ..base import ShopifyResource class FulfillmentEvent(ShopifyResource): _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id/" _singular = "event" _plural = "events" @classmethod def _prefix(cls, options={}): order_id = options.get("order_id") fulfillment_id = options.get("fulfillment_id") event_id = options.get("event_id") return "%s/orders/%s/fulfillments/%s" % (cls.site, order_id, fulfillment_id) def save(self): status = self.attributes["status"] if status not in [ "label_printed", "label_purchased", "attempted_delivery", "ready_for_pickup", "picked_up", "confirmed", "in_transit", "out_for_delivery", "delivered", "failure", ]: raise AttributeError("Invalid status") return super(ShopifyResource, self).save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/fulfillment_service.py0000644000076500000240000000046214605034740024012 0ustar00lizkenyibstafffrom ..base import ShopifyResource class FulfillmentService(ShopifyResource): def __get_format(self): return self.attributes.get("format") def __set_format(self, data): self.attributes["format"] = data format = property(__get_format, __set_format, None, "Format attribute") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/gift_card.py0000644000076500000240000000201614605034740021670 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .gift_card_adjustment import GiftCardAdjustment class GiftCard(ShopifyResource): def disable(self): self._load_attributes_from_response(self.post("disable")) @classmethod def search(cls, **kwargs): """ Search for gift cards matching supplied query Args: order: Field and direction to order results by (default: disabled_at DESC) query: Text to search for gift cards page: Page to show (default: 1) limit: Amount of results (default: 50) (maximum: 250) fields: comma-separated list of fields to include in the response Returns: An array of gift cards. """ return cls._build_collection(cls.get("search", **kwargs)) def add_adjustment(self, adjustment): """ Create a new Gift Card Adjustment """ resource = self.post("adjustments", adjustment.encode()) return GiftCardAdjustment(GiftCard.format.decode(resource.body)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/gift_card_adjustment.py0000644000076500000240000000030114605034740024121 0ustar00lizkenyibstafffrom ..base import ShopifyResource class GiftCardAdjustment(ShopifyResource): _prefix_source = "/admin/gift_cards/$gift_card_id/" _plural = "adjustments" _singular = "adjustment" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/graphql.py0000644000076500000240000000214014605034740021402 0ustar00lizkenyibstaffimport shopify from ..base import ShopifyResource from six.moves import urllib import json class GraphQL: def __init__(self): self.endpoint = shopify.ShopifyResource.get_site() + "/graphql.json" self.headers = shopify.ShopifyResource.get_headers() def merge_headers(self, *headers): merged_headers = {} for header in headers: merged_headers.update(header) return merged_headers def execute(self, query, variables=None, operation_name=None): endpoint = self.endpoint default_headers = {"Accept": "application/json", "Content-Type": "application/json"} headers = self.merge_headers(default_headers, self.headers) data = {"query": query, "variables": variables, "operationName": operation_name} req = urllib.request.Request(self.endpoint, json.dumps(data).encode("utf-8"), headers) try: response = urllib.request.urlopen(req) return response.read().decode("utf-8") except urllib.error.HTTPError as e: print((e.read())) print("") raise e ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/image.py0000644000076500000240000000261714605034740021037 0ustar00lizkenyibstafffrom ..base import ShopifyResource from ..resources import Metafield from six.moves import urllib import base64 import re class Image(ShopifyResource): _prefix_source = "/products/$product_id/" @classmethod def _prefix(cls, options={}): product_id = options.get("product_id") if product_id: return "%s/products/%s" % (cls.site, product_id) else: return cls.site def __getattr__(self, name): if name in ["pico", "icon", "thumb", "small", "compact", "medium", "large", "grande", "original"]: return re.sub(r"/(.*)\.(\w{2,4})", r"/\1_%s.\2" % (name), self.src) else: return super(Image, self).__getattr__(name) def attach_image(self, data, filename=None): self.attributes["attachment"] = base64.b64encode(data).decode() if filename: self.attributes["filename"] = filename def metafields(self): if self.is_new(): return [] query_params = {"metafield[owner_id]": self.id, "metafield[owner_resource]": "product_image"} return Metafield.find( from_="%s/metafields.json?%s" % (ShopifyResource.site, urllib.parse.urlencode(query_params)) ) def save(self): if "product_id" not in self._prefix_options: self._prefix_options["product_id"] = self.product_id return super(ShopifyResource, self).save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/inventory_item.py0000644000076500000240000000012414605034740023017 0ustar00lizkenyibstafffrom ..base import ShopifyResource class InventoryItem(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/inventory_level.py0000644000076500000240000000431114605034740023172 0ustar00lizkenyibstafffrom ..base import ShopifyResource import shopify import json class InventoryLevel(ShopifyResource): def __repr__(self): return "%s(inventory_item_id=%s, location_id=%s)" % (self._singular, self.inventory_item_id, self.location_id) @classmethod def _element_path(cls, prefix_options={}, query_options=None): if query_options is None: prefix_options, query_options = cls._split_options(prefix_options) return "%s%s.%s%s" % ( cls._prefix(prefix_options) + "/", cls.plural, cls.format.extension, cls._query_string(query_options), ) @classmethod def adjust(cls, location_id, inventory_item_id, available_adjustment): body = { "inventory_item_id": inventory_item_id, "location_id": location_id, "available_adjustment": available_adjustment, } resource = cls.post("adjust", body=json.dumps(body).encode()) return InventoryLevel(InventoryLevel.format.decode(resource.body)) @classmethod def connect(cls, location_id, inventory_item_id, relocate_if_necessary=False, **kwargs): body = { "inventory_item_id": inventory_item_id, "location_id": location_id, "relocate_if_necessary": relocate_if_necessary, } resource = cls.post("connect", body=json.dumps(body).encode()) return InventoryLevel(InventoryLevel.format.decode(resource.body)) @classmethod def set(cls, location_id, inventory_item_id, available, disconnect_if_necessary=False, **kwargs): body = { "inventory_item_id": inventory_item_id, "location_id": location_id, "available": available, "disconnect_if_necessary": disconnect_if_necessary, } resource = cls.post("set", body=json.dumps(body).encode()) return InventoryLevel(InventoryLevel.format.decode(resource.body)) def is_new(self): return False def destroy(self): options = {"inventory_item_id": self.inventory_item_id, "location_id": self.location_id} return self.__class__.connection.delete(self._element_path(query_options=options), self.__class__.headers) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/line_item.py0000644000076500000240000000017014605034740021712 0ustar00lizkenyibstafffrom ..base import ShopifyResource class LineItem(ShopifyResource): class Property(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/location.py0000644000076500000240000000046014605034740021557 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .inventory_level import InventoryLevel class Location(ShopifyResource): def inventory_levels(self, **kwargs): return InventoryLevel.find( from_="%s/locations/%s/inventory_levels.json" % (ShopifyResource.site, self.id), **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/marketing_event.py0000644000076500000240000000041314605034740023127 0ustar00lizkenyibstaffimport json from ..base import ShopifyResource class MarketingEvent(ShopifyResource): def add_engagements(self, engagements): engagements_json = json.dumps({"engagements": engagements}) return self.post("engagements", engagements_json.encode()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/metafield.py0000644000076500000240000000054214605034740021702 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Metafield(ShopifyResource): _prefix_source = "/$resource/$resource_id/" @classmethod def _prefix(cls, options={}): resource = options.get("resource") if resource: return "%s/%s/%s" % (cls.site, resource, options["resource_id"]) else: return cls.site ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/note_attribute.py0000644000076500000240000000012414605034740022774 0ustar00lizkenyibstafffrom ..base import ShopifyResource class NoteAttribute(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/option.py0000644000076500000240000000011514605034740021254 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Option(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/order.py0000644000076500000240000000164114605034740021064 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins from .transaction import Transaction class Order(ShopifyResource, mixins.Metafields, mixins.Events): _prefix_source = "/customers/$customer_id/" @classmethod def _prefix(cls, options={}): customer_id = options.get("customer_id") if customer_id: return "%s/customers/%s" % (cls.site, customer_id) else: return cls.site def close(self): self._load_attributes_from_response(self.post("close")) def open(self): self._load_attributes_from_response(self.post("open")) def cancel(self, **kwargs): self._load_attributes_from_response(self.post("cancel", **kwargs)) def transactions(self): return Transaction.find(order_id=self.id) def capture(self, amount=""): return Transaction.create({"amount": amount, "kind": "capture", "order_id": self.id}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/order_risk.py0000644000076500000240000000023614605034740022113 0ustar00lizkenyibstafffrom ..base import ShopifyResource class OrderRisk(ShopifyResource): _prefix_source = "/orders/$order_id/" _singular = "risk" _plural = "risks" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/page.py0000644000076500000240000000021014605034740020654 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Page(ShopifyResource, mixins.Metafields, mixins.Events): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/payment_details.py0000644000076500000240000000012514605034740023127 0ustar00lizkenyibstafffrom ..base import ShopifyResource class PaymentDetails(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/payouts.py0000644000076500000240000000023514605034740021453 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Payouts(ShopifyResource, mixins.Metafields): _prefix_source = "/shopify_payments/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/policy.py0000644000076500000240000000023114605034740021242 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins import shopify class Policy(ShopifyResource, mixins.Metafields, mixins.Events): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/price_rule.py0000644000076500000240000000162014605034740022077 0ustar00lizkenyibstaffimport json from ..base import ShopifyResource from .discount_code import DiscountCode from .discount_code_creation import DiscountCodeCreation class PriceRule(ShopifyResource): def add_discount_code(self, discount_code=DiscountCode()): resource = self.post("discount_codes", discount_code.encode()) return DiscountCode(PriceRule.format.decode(resource.body)) def discount_codes(self): return DiscountCode.find(price_rule_id=self.id) def create_batch(self, codes=[]): codes_json = json.dumps({"discount_codes": codes}) response = self.post("batch", codes_json.encode()) return DiscountCodeCreation(PriceRule.format.decode(response.body)) def find_batch(self, batch_id): return DiscountCodeCreation.find_one( "%s/price_rules/%s/batch/%s.%s" % (ShopifyResource.site, self.id, batch_id, PriceRule.format.extension) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/product.py0000644000076500000240000000307514605034740021434 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins import shopify class Product(ShopifyResource, mixins.Metafields, mixins.Events): def price_range(self): prices = [float(variant.price) for variant in self.variants] f = "%0.2f" min_price = min(prices) max_price = max(prices) if min_price != max_price: return "%s - %s" % (f % min_price, f % max_price) else: return f % min_price def collections(self): return shopify.CustomCollection.find(product_id=self.id) def smart_collections(self): return shopify.SmartCollection.find(product_id=self.id) def add_to_collection(self, collection): return collection.add_product(self) def remove_from_collection(self, collection): return collection.remove_product(self) def add_variant(self, variant): variant.attributes["product_id"] = self.id return variant.save() def save(self): start_api_version = "201910" api_version = ShopifyResource.version if api_version and (api_version.strip("-") >= start_api_version) and api_version != "unstable": if "variants" in self.attributes: for variant in self.variants: if "inventory_quantity" in variant.attributes: del variant.attributes["inventory_quantity"] if "old_inventory_quantity" in variant.attributes: del variant.attributes["old_inventory_quantity"] return super(ShopifyResource, self).save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/product_listing.py0000644000076500000240000000032214605034740023155 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ProductListing(ShopifyResource): _primary_key = "product_id" @classmethod def product_ids(cls, **kwargs): return cls.get("product_ids", **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/product_publication.py0000644000076500000240000000020614605034740024016 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ProductPublication(ShopifyResource): _prefix_source = "/publications/$publication_id/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/province.py0000644000076500000240000000017314605034740021575 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Province(ShopifyResource): _prefix_source = "/admin/countries/$country_id/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/publication.py0000644000076500000240000000012214605034740022253 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Publication(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/receipt.py0000644000076500000240000000011614605034740021400 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Receipt(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/recurring_application_charge.py0000644000076500000240000000153714605034740025651 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .usage_charge import UsageCharge def _get_first_by_status(resources, status): for resource in resources: if resource.status == status: return resource return None class RecurringApplicationCharge(ShopifyResource): def usage_charges(self): return UsageCharge.find(recurring_application_charge_id=self.id) def customize(self, **kwargs): self._load_attributes_from_response(self.put("customize", recurring_application_charge=kwargs)) @classmethod def current(cls): """ Returns first RecurringApplicationCharge object with status=active. If not found, None will be returned. """ return _get_first_by_status(cls.find(), "active") def activate(self): self._load_attributes_from_response(self.post("activate")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/redirect.py0000644000076500000240000000011714605034740021547 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Redirect(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/refund.py0000644000076500000240000000206014605034740021230 0ustar00lizkenyibstaffimport json from ..base import ShopifyResource class Refund(ShopifyResource): _prefix_source = "/orders/$order_id/" @classmethod def calculate(cls, order_id, shipping=None, refund_line_items=None): """ Calculates refund transactions based on line items and shipping. When you want to create a refund, you should first use the calculate endpoint to generate accurate refund transactions. Args: order_id: Order ID for which the Refund has to created. shipping: Specify how much shipping to refund. refund_line_items: A list of line item IDs and quantities to refund. Returns: Unsaved refund record """ data = {} if shipping: data["shipping"] = shipping data["refund_line_items"] = refund_line_items or [] body = {"refund": data} resource = cls.post("calculate", order_id=order_id, body=json.dumps(body).encode()) return cls(cls.format.decode(resource.body), prefix_options={"order_id": order_id}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/report.py0000644000076500000240000000011514605034740021257 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Report(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/resource_feedback.py0000644000076500000240000000057714605034740023413 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ResourceFeedback(ShopifyResource): _prefix_source = "/products/$product_id/" _plural = "resource_feedback" @classmethod def _prefix(cls, options={}): product_id = options.get("product_id") if product_id: return "%s/products/%s" % (cls.site, product_id) else: return cls.site ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/rule.py0000644000076500000240000000011314605034740020711 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Rule(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/script_tag.py0000644000076500000240000000012014605034740022077 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ScriptTag(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/shipping_address.py0000644000076500000240000000012614605034740023274 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ShippingAddress(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/shipping_line.py0000644000076500000240000000012314605034740022573 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ShippingLine(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/shipping_zone.py0000644000076500000240000000012314605034740022617 0ustar00lizkenyibstafffrom ..base import ShopifyResource class ShippingZone(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/shop.py0000644000076500000240000000105414605034740020720 0ustar00lizkenyibstafffrom ..base import ShopifyResource from .metafield import Metafield from .event import Event class Shop(ShopifyResource): @classmethod def current(cls): return cls.find_one(cls.site + "/shop." + cls.format.extension) def metafields(self): return Metafield.find() def add_metafield(self, metafield): if self.is_new(): raise ValueError("You can only add metafields to a resource that has been saved") metafield.save() return metafield def events(self): return Event.find() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/smart_collection.py0000644000076500000240000000035414605034740023312 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins import shopify class SmartCollection(ShopifyResource, mixins.Metafields, mixins.Events): def products(self): return shopify.Product.find(collection_id=self.id) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/storefront_access_token.py0000644000076500000240000000013414605034740024673 0ustar00lizkenyibstafffrom ..base import ShopifyResource class StorefrontAccessToken(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/tax_line.py0000644000076500000240000000011614605034740021550 0ustar00lizkenyibstafffrom ..base import ShopifyResource class TaxLine(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/tender_transaction.py0000644000076500000240000000013014605034740023627 0ustar00lizkenyibstafffrom ..base import ShopifyResource class TenderTransaction(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/theme.py0000644000076500000240000000011414605034740021045 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Theme(ShopifyResource): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/transaction.py0000644000076500000240000000016314605034740022274 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Transaction(ShopifyResource): _prefix_source = "/orders/$order_id/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/transactions.py0000644000076500000240000000025214605034740022456 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Transactions(ShopifyResource, mixins.Metafields): _prefix_source = "/shopify_payments/balance/" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/usage_charge.py0000644000076500000240000000075214605034740022370 0ustar00lizkenyibstafffrom ..base import ShopifyResource class UsageCharge(ShopifyResource): _prefix_source = "/recurring_application_charge/$recurring_application_charge_id/" @classmethod def _prefix(cls, options={}): recurring_application_charge_id = options.get("recurring_application_charge_id") if recurring_application_charge_id: return "%s/recurring_application_charges/%s" % (cls.site, recurring_application_charge_id) else: return cls.site ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/user.py0000644000076500000240000000022114605034740020720 0ustar00lizkenyibstafffrom ..base import ShopifyResource class User(ShopifyResource): @classmethod def current(cls): return User(cls.get("current")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/variant.py0000644000076500000240000000175114605034740021417 0ustar00lizkenyibstafffrom ..base import ShopifyResource from shopify import mixins class Variant(ShopifyResource, mixins.Metafields): _prefix_source = "/products/$product_id/" @classmethod def _prefix(cls, options={}): product_id = options.get("product_id") if product_id: return "%s/products/%s" % (cls.site, product_id) else: return cls.site def save(self): if "product_id" not in self._prefix_options: self._prefix_options["product_id"] = self.product_id start_api_version = "201910" api_version = ShopifyResource.version if api_version and (api_version.strip("-") >= start_api_version) and api_version != "unstable": if "inventory_quantity" in self.attributes: del self.attributes["inventory_quantity"] if "old_inventory_quantity" in self.attributes: del self.attributes["old_inventory_quantity"] return super(ShopifyResource, self).save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/resources/webhook.py0000644000076500000240000000044714605034740021412 0ustar00lizkenyibstafffrom ..base import ShopifyResource class Webhook(ShopifyResource): def __get_format(self): return self.attributes.get("format") def __set_format(self, data): self.attributes["format"] = data format = property(__get_format, __set_format, None, "Format attribute") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727203258.0 shopifyapi-12.7.0/shopify/session.py0000644000076500000240000001373514674603672017445 0ustar00lizkenyibstaffimport time import hmac import json from hashlib import sha256 try: import simplejson as json except ImportError: import json import re from contextlib import contextmanager from six.moves import urllib from shopify.api_access import ApiAccess from shopify.api_version import ApiVersion, Release, Unstable import six class ValidationException(Exception): pass class Session(object): api_key = None secret = None protocol = "https" myshopify_domain = "myshopify.com" port = None @classmethod def setup(cls, **kwargs): for k, v in six.iteritems(kwargs): setattr(cls, k, v) @classmethod @contextmanager def temp(cls, domain, version, token): import shopify original_domain = shopify.ShopifyResource.url original_token = shopify.ShopifyResource.get_headers().get("X-Shopify-Access-Token") original_version = shopify.ShopifyResource.get_version() or version original_session = shopify.Session(original_domain, original_version, original_token) session = Session(domain, version, token) shopify.ShopifyResource.activate_session(session) yield shopify.ShopifyResource.activate_session(original_session) def __init__(self, shop_url, version=None, token=None, access_scopes=None): self.url = self.__prepare_url(shop_url) self.token = token self.version = ApiVersion.coerce_to_version(version) self.access_scopes = access_scopes return def create_permission_url(self, scope, redirect_uri, state=None): query_params = dict(client_id=self.api_key, scope=",".join(scope), redirect_uri=redirect_uri) if state: query_params["state"] = state return "https://%s/admin/oauth/authorize?%s" % (self.url, urllib.parse.urlencode(query_params)) def request_token(self, params): if self.token: return self.token if not self.validate_params(params): raise ValidationException("Invalid HMAC: Possibly malicious login") code = params["code"] url = "https://%s/admin/oauth/access_token?" % self.url query_params = dict(client_id=self.api_key, client_secret=self.secret, code=code) request = urllib.request.Request(url, urllib.parse.urlencode(query_params).encode("utf-8")) response = urllib.request.urlopen(request) if response.code == 200: json_payload = json.loads(response.read().decode("utf-8")) self.token = json_payload["access_token"] self.access_scopes = json_payload["scope"] return self.token else: raise Exception(response.msg) @property def api_version(self): return self.version @property def site(self): return self.version.api_path("%s://%s" % (self.protocol, self.url)) @property def valid(self): return self.url is not None and self.token is not None @property def access_scopes(self): return self._access_scopes @access_scopes.setter def access_scopes(self, scopes): if scopes is None or type(scopes) == ApiAccess: self._access_scopes = scopes else: self._access_scopes = ApiAccess(scopes) @classmethod def __prepare_url(cls, url): if not url or (url.strip() == ""): return None url = re.sub("^https?://", "", url) shop = urllib.parse.urlparse("https://" + url).hostname if shop is None: return None idx = shop.find(".") if idx != -1: shop = shop[0:idx] if len(shop) == 0: return None shop += "." + cls.myshopify_domain if cls.port: shop += ":" + str(cls.port) return shop @classmethod def validate_params(cls, params): # Avoid replay attacks by making sure the request # isn't more than a day old. one_day = 24 * 60 * 60 if int(params.get("timestamp", 0)) < time.time() - one_day: return False return cls.validate_hmac(params) @classmethod def validate_hmac(cls, params): if "hmac" not in params: return False hmac_calculated = cls.calculate_hmac(params).encode("utf-8") hmac_to_verify = params["hmac"].encode("utf-8") # Try to use compare_digest() to reduce vulnerability to timing attacks. # If it's not available, just fall back to regular string comparison. try: return hmac.compare_digest(hmac_calculated, hmac_to_verify) except AttributeError: return hmac_calculated == hmac_to_verify @classmethod def calculate_hmac(cls, params): """ Calculate the HMAC of the given parameters in line with Shopify's rules for OAuth authentication. See http://docs.shopify.com/api/authentication/oauth#verification. """ encoded_params = cls.__encoded_params_for_signature(params) # Generate the hex digest for the sorted parameters using the secret. return hmac.new(cls.secret.encode(), encoded_params.encode(), sha256).hexdigest() @classmethod def __encoded_params_for_signature(cls, params): """ Sort and combine query parameters into a single string, excluding those that should be removed and joining with '&' """ def encoded_pairs(params): for k, v in six.iteritems(params): if k == "hmac": continue if k.endswith("[]"): # foo[]=1&foo[]=2 has to be transformed as foo=["1", "2"] note the whitespace after comma k = k.rstrip("[]") v = json.dumps(list(map(str, v))) # escape delimiters to avoid tampering k = str(k).replace("%", "%25").replace("=", "%3D") v = str(v).replace("%", "%25") yield "{0}={1}".format(k, v).replace("&", "%26") return "&".join(sorted(encoded_pairs(params))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/session_token.py0000644000076500000240000000442214605034740020622 0ustar00lizkenyibstaffimport jwt import re import six import sys from shopify.utils import shop_url if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 from urlparse import urljoin else: from urllib.parse import urljoin ALGORITHM = "HS256" PREFIX = "Bearer " REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] LEEWAY_SECONDS = 10 class SessionTokenError(Exception): pass class InvalidIssuerError(SessionTokenError): pass class MismatchedHostsError(SessionTokenError): pass class TokenAuthenticationError(SessionTokenError): pass def decode_from_header(authorization_header, api_key, secret): session_token = _extract_session_token(authorization_header) decoded_payload = _decode_session_token(session_token, api_key, secret) _validate_issuer(decoded_payload) return decoded_payload def _extract_session_token(authorization_header): if not authorization_header.startswith(PREFIX): raise TokenAuthenticationError("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token") return authorization_header[len(PREFIX) :] def _decode_session_token(session_token, api_key, secret): try: return jwt.decode( session_token, secret, audience=api_key, algorithms=[ALGORITHM], # AppBridge frequently sends future `nbf`, and it causes `ImmatureSignatureError`. # Accept few seconds clock skew to avoid this error. leeway=LEEWAY_SECONDS, options={"require": REQUIRED_FIELDS}, ) except jwt.exceptions.PyJWTError as exception: six.raise_from(SessionTokenError(str(exception)), exception) def _validate_issuer(decoded_payload): _validate_issuer_hostname(decoded_payload) _validate_issuer_and_dest_match(decoded_payload) def _validate_issuer_hostname(decoded_payload): issuer_root = urljoin(decoded_payload["iss"], "/") if not shop_url.sanitize_shop_domain(issuer_root): raise InvalidIssuerError("Invalid issuer") def _validate_issuer_and_dest_match(decoded_payload): issuer_root = urljoin(decoded_payload["iss"], "/") dest_root = urljoin(decoded_payload["dest"], "/") if issuer_root != dest_root: raise MismatchedHostsError("The issuer and destination do not match") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7331274 shopifyapi-12.7.0/shopify/utils/0000755000076500000240000000000014712210466016523 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/utils/__init__.py0000644000076500000240000000000014605034740020622 0ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/utils/shop_url.py0000644000076500000240000000127314605034740020733 0ustar00lizkenyibstaffimport re import sys if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 from urlparse import urlparse else: from urllib.parse import urlparse HOSTNAME_PATTERN = r"[a-z0-9][a-z0-9-]*[a-z0-9]" def sanitize_shop_domain(shop_domain, myshopify_domain="myshopify.com"): name = str(shop_domain or "").lower().strip() if myshopify_domain not in name and "." not in name: name += ".{domain}".format(domain=myshopify_domain) name = re.sub(r"https?://", "", name) uri = urlparse("http://{hostname}".format(hostname=name)) if re.match(r"{h}\.{d}$".format(h=HOSTNAME_PATTERN, d=re.escape(myshopify_domain)), uri.netloc): return uri.netloc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730744343.0 shopifyapi-12.7.0/shopify/version.py0000644000076500000240000000002314712210027017406 0ustar00lizkenyibstaffVERSION = "12.7.0" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/shopify/yamlobjects.py0000644000076500000240000000125114605034740020250 0ustar00lizkenyibstafftry: # Shopify serializes receipts in YAML format, and yaml.safe_load will # not automatically load custom types because of security purpose, # so create safe loaders for types returned from Shopify here. # # The YAMLObject metaclass will automatically add these classes to # the list of constructors for yaml.safe_load to use. import yaml class YAMLHashWithIndifferentAccess(yaml.YAMLObject): yaml_tag = "!map:ActiveSupport::HashWithIndifferentAccess" yaml_loader = yaml.SafeLoader @classmethod def from_yaml(cls, loader, node): return loader.construct_mapping(node, cls) except ImportError: pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730744629.7333078 shopifyapi-12.7.0/test/0000755000076500000240000000000014712210466014661 5ustar00lizkenyibstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1712601568.0 shopifyapi-12.7.0/test/test_helper.py0000644000076500000240000000377014605034740017560 0ustar00lizkenyibstaffimport os import sys import unittest from pyactiveresource.activeresource import ActiveResource from pyactiveresource.testing import http_fake import shopify class TestCase(unittest.TestCase): def setUp(self): ActiveResource.site = None ActiveResource.headers = None shopify.ShopifyResource.clear_session() shopify.ShopifyResource.site = "https://this-is-my-test-show.myshopify.com/admin/api/unstable" shopify.ShopifyResource.password = None shopify.ShopifyResource.user = None http_fake.initialize() self.http = http_fake.TestHandler self.http.set_response(Exception("Bad request")) self.http.site = "https://this-is-my-test-show.myshopify.com" def load_fixture(self, name, format="json"): with open(os.path.dirname(__file__) + "/fixtures/%s.%s" % (name, format), "rb") as f: return f.read() def fake(self, endpoint, **kwargs): body = kwargs.pop("body", None) or self.load_fixture(endpoint) format = kwargs.pop("format", "json") method = kwargs.pop("method", "GET") prefix = kwargs.pop("prefix", "/admin/api/unstable") if "extension" in kwargs and not kwargs["extension"]: extension = "" else: extension = ".%s" % (kwargs.pop("extension", "json")) url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension) try: url = kwargs["url"] except KeyError: pass headers = {} if kwargs.pop("has_user_agent", True): userAgent = "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0]) headers["User-agent"] = userAgent try: headers.update(kwargs["headers"]) except KeyError: pass code = kwargs.pop("code", 200) self.http.respond_to( method, url, headers, body=body, code=code, response_headers=kwargs.pop("response_headers", None) )