diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
new file mode 100644
index 00000000..5a32bc62
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
@@ -0,0 +1,39 @@
+---
+name: '๐ Bug Report'
+about: Something isn't working
+labels: "Type: Bug ๐"
+---
+
+# Issue summary
+
+Write a short description of the issue here โ
+
+
+## Expected behavior
+
+What do you think should happen?
+
+
+## Actual behavior
+
+What actually happens?
+
+Tip: include an error message (in a ` ` tag) if your issue is related to an error
+
+
+## Steps to reproduce the problem
+
+1.
+2.
+3.
+
+## Reduced test case
+
+The best way to get your bug fixed is to provide a [reduced test case](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Reducing_testcases).
+
+
+---
+
+## Checklist
+
+- [ ] I have described this issue in a way that is actionable (if possible)
diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.md b/.github/ISSUE_TEMPLATE/ENHANCEMENT.md
new file mode 100644
index 00000000..8d333e75
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.md
@@ -0,0 +1,26 @@
+---
+name: '๐ Enhancement'
+about: Enhancement to our codebase that isn't a adding or changing a feature
+labels: "Type: Enhancement ๐"
+---
+
+## Overview/summary
+
+...
+
+## Motivation
+
+> What inspired this enhancement?
+
+...
+
+### Area
+
+- [ ] Add any relevant `Area: ` labels to this issue
+
+
+---
+
+## Checklist
+
+- [ ] I have described this enhancement in a way that is actionable (if possible)
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
new file mode 100644
index 00000000..ebe76b01
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
@@ -0,0 +1,30 @@
+---
+name: '๐ Feature Request'
+about: Suggest a new feature, or changes to an existing one
+labels: "Type: Feature Request :raised_hands:"
+---
+
+## Overview
+
+...
+
+## Type
+
+- [ ] New feature
+- [ ] Changes to existing features
+
+## Motivation
+
+> What inspired this feature request? What problems were you facing?
+
+...
+
+### Area
+
+- [ ] Add any relevant `Area: ` labels to this issue
+
+---
+
+## Checklist
+
+- [ ] I have described this feature request in a way that is actionable (if possible)
diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md
new file mode 100644
index 00000000..1e7927ed
--- /dev/null
+++ b/.github/api_update_reminder.md
@@ -0,0 +1,25 @@
+---
+title: A new release of the Shopify API is due soon
+labels: automated
+---
+
+This is an automated reminder for the maintainers that a new Stable release of the Shopify API is due soon, so the library needs to be updated.
+
+A new library release should be prepared to:
+* add the upcoming Stable release
+* remove the oldest Stable release which will no longer be supported.
+
+The PR should be created as a **draft** but not yet be merged.
+
+The release schedule can be found at https://shopify.dev/concepts/about-apis/versioning
+
+Review the changelog and consider if anything in the library needs to change:
+
+https://shopify.dev/changelog
+
+Test against the upcoming release by using the Release Candidate.
+
+Another reminder issue will be created on the date of the next release.
+When that happens, test again using the now Stable API version, and aim to release an update of the library within one week.
+
+Thank you!
diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md
new file mode 100644
index 00000000..47ddaaf5
--- /dev/null
+++ b/.github/api_update_reminder_on_release.md
@@ -0,0 +1,19 @@
+---
+title: A new release of the Shopify API occurred
+labels: automated
+---
+
+This is an automated reminder for the maintainers that a new Stable release of the Shopify API is scheduled for today
+at 12pm Eastern Time, so a new release of the library is now due.
+
+A draft PR should already exist for this.
+
+Review the changelog again and consider if anything in the library needs to change:
+
+https://shopify.dev/changelog
+
+Test against the new release by using the Stable version just released.
+
+Aim to release an update of the library within one week.
+
+Thank you!
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..4af31a55
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ time: "00:00"
+ timezone: "UTC"
+ reviewers:
+ - Shopify/core-build-learn
+ labels:
+ - "Pip upgrades"
+ open-pull-requests-limit: 100
+ registries: '*'
diff --git a/.github/probots.yml b/.github/probots.yml
deleted file mode 100644
index 1491d275..00000000
--- a/.github/probots.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-enabled:
- - cla
diff --git a/.github/workflows/api_update_reminder.yml b/.github/workflows/api_update_reminder.yml
new file mode 100644
index 00000000..b4cfb456
--- /dev/null
+++ b/.github/workflows/api_update_reminder.yml
@@ -0,0 +1,16 @@
+on:
+ workflow_dispatch: ~
+ schedule:
+ - cron: "0 0 1 3,6,9,12 *" # At 00:00 on 1st of March, June, September, and December
+
+name: API update reminder
+jobs:
+ reminder:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: JasonEtco/create-an-issue@v2.4.0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ filename: .github/api_update_reminder.md
diff --git a/.github/workflows/api_update_reminder_on_release.yml b/.github/workflows/api_update_reminder_on_release.yml
new file mode 100644
index 00000000..5e3053f9
--- /dev/null
+++ b/.github/workflows/api_update_reminder_on_release.yml
@@ -0,0 +1,16 @@
+on:
+ workflow_dispatch: ~
+ schedule:
+ - cron: "0 0 1 1,4,7,10 *" # At 00:00 on 1st of January, April, July, and October
+
+name: API update reminder on release
+jobs:
+ reminder:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: JasonEtco/create-an-issue@v2.4.0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ filename: .github/api_update_reminder_on_release.md
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 04c10fb7..c0cd5ab9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,28 +1,30 @@
name: CI
-
-on:
- push:
-
-
+on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
name: Python ${{ matrix.version }}
- strategy:
+ strategy:
matrix:
- version: [2.7, 3.6, 3.8]
+ version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
- steps:
- - uses: actions/checkout@v2
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.version }}
- name: Install Dependencies
- run: |
+ run: |
python -m pip install --upgrade pip
- pip install pytest mock
- python setup.py install
+ pip install pytest mock pytest-cov setuptools
+ python setup.py install
+ pytest --cov=./ --cov-report=xml
- name: Run Tests
run: python -m pytest -v
-
+ - name: upload coverage to Codecov
+ uses: codecov/codecov-action@v1
+ with:
+ name: codecov-umbrella
+ fail_ci_if_error: false
diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml
new file mode 100644
index 00000000..2c3a4042
--- /dev/null
+++ b/.github/workflows/cla.yml
@@ -0,0 +1,22 @@
+name: Contributor License Agreement (CLA)
+
+on:
+ pull_request_target:
+ types: [opened, synchronize]
+ issue_comment:
+ types: [created]
+
+jobs:
+ cla:
+ runs-on: ubuntu-latest
+ if: |
+ (github.event.issue.pull_request
+ && !github.event.issue.pull_request.merged_at
+ && contains(github.event.comment.body, 'signed')
+ )
+ || (github.event.pull_request && !github.event.pull_request.merged)
+ steps:
+ - uses: Shopify/shopify-cla-action@v1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ cla-token: ${{ secrets.CLA_TOKEN }}
diff --git a/.github/workflows/close-waiting-for-response-issues.yml b/.github/workflows/close-waiting-for-response-issues.yml
new file mode 100644
index 00000000..ffd7a382
--- /dev/null
+++ b/.github/workflows/close-waiting-for-response-issues.yml
@@ -0,0 +1,20 @@
+name: Close Waiting for Response Issues
+on:
+ schedule:
+ - cron: "30 1 * * *"
+ workflow_dispatch:
+jobs:
+ check-need-info:
+ runs-on: ubuntu-latest
+ steps:
+ - name: close-issues
+ uses: actions-cool/issues-helper@v3
+ with:
+ actions: 'close-issues'
+ token: ${{ secrets.GITHUB_TOKEN }}
+ labels: 'Waiting for Response'
+ inactive-day: 7
+ body: |
+ We are closing this issue because we did not hear back regarding additional details we needed to resolve this issue. If the issue persists and you are able to provide the missing clarification we need, feel free to respond and reopen this issue.
+
+ We appreciate your understanding as we try to manage our number of open issues.
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 00000000..f92848d7
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,17 @@
+name: pre-commit
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Setup
+ uses: actions/setup-python@v4
+ - name: Pre-commit
+ uses: pre-commit/action@v2.0.0
diff --git a/.github/workflows/remove-labels-on-activity.yml b/.github/workflows/remove-labels-on-activity.yml
new file mode 100644
index 00000000..948801e7
--- /dev/null
+++ b/.github/workflows/remove-labels-on-activity.yml
@@ -0,0 +1,15 @@
+name: Remove Stale or Waiting Labels
+on:
+ issue_comment:
+ types: [created]
+ workflow_dispatch:
+jobs:
+ remove-labels-on-activity:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: actions-ecosystem/action-remove-labels@v1
+ if: contains(github.event.issue.labels.*.name, 'Waiting for Response')
+ with:
+ labels: |
+ Waiting for Response
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 00000000..2d0a3ec7
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,32 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: "30 1 * * *"
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - uses: actions/stale@v5
+ with:
+ days-before-issue-stale: 60
+ days-before-issue-close: 14
+ stale-issue-label: "Stale"
+ stale-issue-message: >
+ This issue is stale because it has been open for 60 days with no activity. It will be closed if no further action occurs in 14 days.
+ close-issue-message: |
+ We are closing this issue because it has been inactive for a few months.
+ This probably means that it is not reproducible or it has been fixed in a newer version.
+ If itโs an enhancement and hasnโt been taken on since it was submitted, then it seems other issues have taken priority.
+
+ If you still encounter this issue with the latest stable version, please reopen using the issue template. You can also contribute directly by submitting a pull requestโ see the [CONTRIBUTING.md](https://github.com/Shopify/shopify_python_api/blob/main/CONTRIBUTING.md) file for guidelines
+
+ Thank you!
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ exempt-issue-labels: "feature request"
+ close-issue-reason: "not_planned"
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..f3500257
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,16 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/psf/black
+ rev: 24.10.0
+ hooks:
+ - id: black
+ - repo: https://github.com/PyCQA/pylint
+ rev: v3.3.3
+ hooks:
+ - id: pylint
diff --git a/CHANGELOG b/CHANGELOG
index c5ce222d..e9910c2e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,298 +1,409 @@
+== Unreleased
+
+- Remove requirement to provide scopes to Permission URL, as it should be omitted if defined with the TOML file.
+
+== 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))
+
+- [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
+
+- 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.
+
+- 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
+
+- 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
+
+- Add Cursor pagination support
== Version 5.1.2
-* Add version 2020-01 to known ApiVersions. This version will not be usable until October 2019.
+
+- 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.
+
+- Fix initializing API with basic auth URL.
== Version 5.1.0
-* Added support for GraphQL queries with a GraphQL resource
+
+- 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
+
+- Fixing missing class variable causing exception when creating a session without a token
== Version 5.0.0
-* Added support for Shopify API Versioning
+
+- 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
+
+- 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()
+
+- 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()`
+
+- 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
+
+- Added support for Marketing Event API through Marketing Event resource
== Version 2.5.1
-* Fixed an issue preventing creation of Order Risk resources
+
+- Fixed an issue preventing creation of Order Risk resources
== Version 2.5.0
-* Added Price Rule and Discount Code resources
+
+- Added Price Rule and Discount Code resources
== Version 2.4.0
-* Add support for report publishing
+
+- Add support for report publishing
== Version 2.3.0
-* Add support for customer#send_invite
+
+- Add support for customer#send_invite
== Version 2.2.0
-* Add support for draft orders
+
+- Add support for draft orders
== Version 2.1.8
-* Added support for `open` method on fulfillments
+
+- 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.
+- Removed all references to the deprecated MD5 `signature` parameter which is no longer provided by Shopify.
== Version 2.1.6
-* Added Refund resource
+- Added Refund resource
== Version 2.1.5
-* bump pyactiveresource for camelcase bugfix
+- bump pyactiveresource for camelcase bugfix
== Version 2.1.4
== Version 2.1.3
-* Fixed hmac signature validation for params with delimiters (`&`, `=` or `%`)
+- 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
+- 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
+- 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
+- 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
+- Bug fixes
+- Added CarrierService resource
+- Added Property resource to LineItem
== Version 2.0.3
-* Add Order Risk resource
+- Add Order Risk resource
== Version 2.0.2
-* Add access to FulfillmentService endpoint
-* Fix some import bugs
+- Add access to FulfillmentService endpoint
+- Fix some import bugs
== Version 2.0.1
-* Package bug fix
+- 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
+- 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
+- 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)
+- 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.
+- 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.
+- 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.
+- 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.
+- 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
+- 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.
+- 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.
+- 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
+- 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.
+- 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
+- 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.
+- 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.
+- 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.
+- Made responses available through the connection object.
== Version 0.1.8
-* Added ability to add metafields on customers.
+- Added ability to add metafields on customers.
== Version 0.1.7
-* Fixed missing theme_id in return value of Asset.find.
+- 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.
+- 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
+- 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
+- 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.
+- Fixed the automatic download of dependencies.
+- Updated the README instructions.
== Version 0.1.2
-* Add python 2.5 compatibility
+- Add python 2.5 compatibility
== Version 0.1.1
-* Make creating a session simpler with django
+- Make creating a session simpler with django
== Version 0.1.0
-* ported ShopifyAPI from ruby to python
+- ported ShopifyAPI from ruby to python
diff --git a/README.md b/README.md
index 435cdb6a..cadda24e 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,17 @@
[](https://github.com/Shopify/shopify_python_api/actions)
[](https://badge.fury.io/py/ShopifyAPI)
+
+[](https://codecov.io/gh/Shopify/shopify_python_api)
+[](https://github.com/Shopify/shopify_python_api/blob/main/LICENSE)
+[](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://partners.shopify.com) so that you can create and manage shopify applications.
-
-#### Python version
-This library requires Python 3.8 or lower.
+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
@@ -21,30 +22,60 @@ To easily install or upgrade to the latest release, use [pip](http://www.pip-ins
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://partners.shopify.com/apps/new), and retreive your API Key and API Secret Key.
+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 = '2020-10'
+ api_version = '2024-07'
state = binascii.b2a_hex(os.urandom(15)).decode("utf-8")
redirect_uri = "http://myapp.com/auth/shopify/callback"
+ # `scope` should be omitted if provided by app's TOML
scopes = ['read_products', 'read_orders']
newSession = shopify.Session(shop_url, api_version)
- auth_url = newSession.create_permission_url(scopes, redirect_uri, state)
+ # `scope` should be omitted if provided by app's TOML
+ auth_url = newSession.create_permission_url(redirect_uri, scopes, 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 recieved 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:
+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)
@@ -58,10 +89,11 @@ pip install --upgrade ShopifyAPI
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
+ # Note: REST API examples will be deprecated in 2025
+ shop = shopify.Shop.current() # Get the current shop
+ product = shopify.Product.find(179761209) # Get a specific product
- # execute a graphQL call
+ # GraphQL API example
shopify.GraphQL().execute("{ shop { name id } }")
```
@@ -121,6 +153,13 @@ _Note: Your application must be public to test the billing process. To test on a
```
### Advanced Usage
+
+> **โ ๏ธ Note**: As of October 1, 2024, the REST Admin API is legacy:
+> - Public apps must migrate to GraphQL by February 2025
+> - Custom apps must migrate to GraphQL by April 2025
+>
+> For migration guidance, see [Shopify's migration guide](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model)
+
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.
@@ -128,6 +167,7 @@ Instances of `pyactiveresource` resources map to RESTful resources in the Shopif
`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
+# Note: REST API examples will be deprecated in 2025
product = shopify.Product()
product.title = "Shopify Logo T-Shirt"
product.id # => 292082188312
@@ -142,11 +182,18 @@ 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
+# Note: This REST API example will be deprecated in the future
shopify.Fulfillment.find(255858046, order_id=450789469)
```
@@ -161,10 +208,52 @@ This package also includes the `shopify_api.py` script to make it easy to open a
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.
+> **Note**: Shopify recommends using GraphQL API for new development as REST API will be deprecated.
+> See [Migration Guide](https://shopify.dev/docs/apps/build/graphql/migrate/new-product-model) for more details.
+
```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
@@ -185,7 +274,7 @@ 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()
@@ -197,6 +286,13 @@ 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:
@@ -208,4 +304,7 @@ Currently there is no support for:
* [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)
+* [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)
diff --git a/RELEASING b/RELEASING
index f84fa314..a15667a0 100644
--- a/RELEASING
+++ b/RELEASING
@@ -15,6 +15,6 @@ Releasing shopify_python_api
git tag -m "Release X.Y.Z" vX.Y.Z
7. Push the changes to github
- git push --tags origin master
+ git push --tags origin main
8. Shipit!
diff --git a/SECURITY.md b/SECURITY.md
index 2d13b5e1..2a0e9c48 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -4,7 +4,7 @@
### New features
-New features will only be added to the master branch and will not be made available in point releases.
+New features will only be added to the main branch and will not be made available in point releases.
### Bug fixes
diff --git a/bin/shopify_api.py b/bin/shopify_api.py
index e8abb535..c0930deb 100755
--- a/bin/shopify_api.py
+++ b/bin/shopify_api.py
@@ -8,6 +8,6 @@
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')
+with open(os.path.join(project_root, "scripts", "shopify_api.py")) as f:
+ code = compile(f.read(), f.name, "exec")
exec(code)
diff --git a/dev.yml b/dev.yml
index 6c37588a..84a23200 100644
--- a/dev.yml
+++ b/dev.yml
@@ -4,7 +4,7 @@ name: shopify-python-api
type: python
up:
- - python: 3.8.0
+ - python: 3.8.13
- pip:
- requirements.txt
diff --git a/docs/api-access.md b/docs/api-access.md
new file mode 100644
index 00000000..52424a24
--- /dev/null
+++ b/docs/api-access.md
@@ -0,0 +1,73 @@
+# Handling access scope operations
+
+#### Table of contents
+
+- [Common ApiAccess operations](#common-apiaccess-operations)
+- [Using ApiAccess to handle changes in app access scopes](#using-apiaccess-to-handle-changes-in-app-access-scopes)
+
+There are common operations that are used for managing [access scopes](https://shopify.dev/docs/admin-api/access-scopes) in apps. Such operations include serializing, deserializing and normalizing scopes. Other operations can include checking whether two sets of scopes grant the same API access or whether one set covers the access granted by another set.
+
+To encapsulate the access granted by access scopes, you can use the `ApiAccess` value object.
+
+## Common ApiAccess operations
+
+### Constructing an ApiAccess
+
+```python
+api_access = ApiAccess(["read_products", "write_orders"]) # List of access scopes
+another_api_access = ApiAccess("read_products, write_products, unauthenticated_read_themes") # String of comma-delimited access scopes
+```
+
+### Serializing ApiAccess
+
+```python
+api_access = ApiAccess(["read_products", "write_orders", "unauthenticated_read_themes"])
+
+access_scopes_list = list(api_access) # ["read_products", "write_orders", "unauthenticated_read_themes"]
+comma_delimited_access_scopes = str(api_access) # "read_products,write_orders,unauthenticated_read_themes"
+```
+
+### Comparing ApiAccess objects
+
+#### Checking for API access equality
+
+```python
+expected_api_access = ApiAccess(["read_products", "write_orders"])
+
+actual_api_access = ApiAccess(["read_products", "read_orders", "write_orders"])
+non_equal_api_access = ApiAccess(["read_products", "write_orders", "read_themes"])
+
+actual_api_access == expected_api_access # True
+non_equal_api_access == expected_api_access # False
+```
+
+#### Checking if ApiAccess covers the access of another
+
+```python
+superset_access = ApiAccess(["write_products", "write_orders", "read_themes"])
+subset_access = ApiAccess(["read_products", "write_orders"])
+
+superset_access.covers(subset_access) # True
+```
+
+## Using ApiAccess to handle changes in app access scopes
+
+If your app has changes in the access scopes it requests, you can use the `ApiAccess` object to determine whether the merchant needs to go through OAuth based on the scopes currently granted. A sample decorator shows how this can be achieved when loading an app.
+
+```python
+from shopify import ApiAccess
+
+
+def oauth_on_access_scopes_mismatch(func):
+ def wrapper(*args, **kwargs):
+ shop_domain = get_shop_query_parameter(request) # shop query param when loading app
+ current_shop_scopes = ApiAccess(ShopStore.get_record(shopify_domain = shop_domain).access_scopes)
+ expected_access_scopes = ApiAccess(SHOPIFY_API_SCOPES)
+
+ if current_shop_scopes != expected_access_scopes:
+ return redirect_to_login() # redirect to OAuth to update access scopes granted
+
+ return func(*args, **kwargs)
+
+ return wrapper
+```
diff --git a/docs/session-tokens.md b/docs/session-tokens.md
new file mode 100644
index 00000000..7349cd3a
--- /dev/null
+++ b/docs/session-tokens.md
@@ -0,0 +1,54 @@
+# Session tokens
+
+The Shopify Python API library provides helper methods to decode [session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens). You can use the `decode_from_header` function to extract and decode a session token from an HTTP Authorization header.
+
+## Basic usage
+
+```python
+from shopify import session_token
+
+decoded_payload = session_token.decode_from_header(
+ authorization_header=your_auth_request_header,
+ api_key=your_api_key,
+ secret=your_api_secret,
+)
+```
+
+## Create a decorator using `session_token`
+
+Here's a sample decorator that protects your app views/routes by requiring the presence of valid session tokens as part of a request's headers.
+
+```python
+from shopify import session_token
+
+
+def session_token_required(func):
+ def wrapper(*args, **kwargs):
+ request = args[0] # Or flask.request if you use Flask
+ try:
+ decoded_session_token = session_token.decode_from_header(
+ authorization_header = request.headers.get('Authorization'),
+ api_key = SHOPIFY_API_KEY,
+ secret = SHOPIFY_API_SECRET
+ )
+ with shopify_session(decoded_session_token):
+ return func(*args, **kwargs)
+ except session_token.SessionTokenError as e:
+ # Log the error here
+ return unauthorized_401_response()
+
+ return wrapper
+
+
+def shopify_session(decoded_session_token):
+ shopify_domain = decoded_session_token.get("dest")
+ access_token = get_offline_access_token_by_shop_domain(shopify_domain)
+
+ return shopify.Session.temp(shopify_domain, SHOPIFY_API_VERSION, access_token)
+
+
+@session_token_required # Requests to /products require session tokens
+def products(request):
+ products = shopify.Product.find()
+ ...
+```
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 00000000..97de81bf
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,443 @@
+# This Pylint rcfile contains a best-effort configuration to uphold the
+# best-practices and style described in the Google Python style guide:
+# https://google.github.io/styleguide/pyguide.html
+#
+# Its canonical open-source location is:
+# https://google.github.io/styleguide/pylintrc
+
+[MASTER]
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=third_party
+
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
+ignore-patterns=
+
+# Pickle collected data for later comparisons.
+persistent=no
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Use multiple processes to speed up Pylint.
+jobs=4
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=INFERENCE
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=abstract-method,
+ apply-builtin,
+ arguments-differ,
+ attribute-defined-outside-init,
+ backtick,
+ bad-option-value,
+ basestring-builtin,
+ buffer-builtin,
+ c-extension-no-member,
+ consider-using-enumerate,
+ cmp-builtin,
+ cmp-method,
+ coerce-builtin,
+ coerce-method,
+ delslice-method,
+ div-method,
+ duplicate-code,
+ eq-without-hash,
+ execfile-builtin,
+ file-builtin,
+ filter-builtin-not-iterating,
+ fixme,
+ getslice-method,
+ global-statement,
+ hex-method,
+ idiv-method,
+ implicit-str-concat-in-sequence,
+ import-error,
+ import-self,
+ import-star-module-level,
+ inconsistent-return-statements,
+ input-builtin,
+ intern-builtin,
+ invalid-str-codec,
+ locally-disabled,
+ long-builtin,
+ long-suffix,
+ map-builtin-not-iterating,
+ misplaced-comparison-constant,
+ missing-class-docstring,
+ missing-function-docstring,
+ missing-module-docstring,
+ metaclass-assignment,
+ next-method-called,
+ next-method-defined,
+ no-absolute-import,
+ no-else-break,
+ no-else-continue,
+ no-else-raise,
+ no-else-return,
+ no-init, # added
+ no-member,
+ no-name-in-module,
+ no-self-use,
+ nonzero-method,
+ oct-method,
+ old-division,
+ old-ne-operator,
+ old-octal-literal,
+ old-raise-syntax,
+ parameter-unpacking,
+ print-statement,
+ raising-string,
+ range-builtin-not-iterating,
+ raw_input-builtin,
+ rdiv-method,
+ reduce-builtin,
+ relative-import,
+ reload-builtin,
+ round-builtin,
+ setslice-method,
+ signature-differs,
+ standarderror-builtin,
+ suppressed-message,
+ sys-max-int,
+ too-few-public-methods,
+ too-many-ancestors,
+ too-many-arguments,
+ too-many-boolean-expressions,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-locals,
+ too-many-nested-blocks,
+ too-many-public-methods,
+ too-many-return-statements,
+ too-many-statements,
+ trailing-newlines,
+ unichr-builtin,
+ unicode-builtin,
+ unnecessary-pass,
+ unpacking-in-except,
+ useless-else-on-loop,
+ useless-object-inheritance,
+ useless-suppression,
+ using-cmp-argument,
+ wrong-import-order,
+ xrange-builtin,
+ zip-builtin-not-iterating,
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]". This option is deprecated
+# and it will be removed in Pylint 2.0.
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[BASIC]
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=main,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
+
+# Regular expression matching correct function names
+function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct constant names
+const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct attribute names
+attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
+
+# Regular expression matching correct argument names
+argument-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=^[a-z][a-z0-9_]*$
+
+# Regular expression matching correct class names
+class-rgx=^_?[A-Z][a-zA-Z0-9]*$
+
+# Regular expression matching correct module names
+module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
+
+# Regular expression matching correct method names
+method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=10
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
+# lines made too long by directives to pytype.
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=(?x)(
+ ^\s*(\#\ )??$|
+ ^\s*(from\s+\S+\s+)?import\s+.+$)
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=yes
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=
+
+# Maximum number of lines in a module
+max-module-lines=99999
+
+# String used as indentation unit. The internal Google style guide mandates 2
+# spaces. Google's externaly-published style guide says 4, consistent with
+# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google
+# projects (like TensorFlow).
+indent-string=' '
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=TODO
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=yes
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging,absl.logging,tensorflow.io.logging
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,
+ TERMIOS,
+ Bastion,
+ rexec,
+ sets
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant, absl
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls,
+ class_
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=StandardError,
+ Exception,
+ BaseException
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..02c844ae
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[tool.autopep8]
+max_line_length = 120
+
+[tool.black]
+line-length = 120
diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py
index 60f48065..bab35f15 100755
--- a/scripts/shopify_api.py
+++ b/scripts/shopify_api.py
@@ -12,23 +12,38 @@
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())
- console = type('shopify ' + shopify.version.VERSION, (code.InteractiveConsole, object), {})
- import readline
- console(variables).interact()
+ 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])
@@ -37,23 +52,26 @@ def __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']:
+ if task in [None, "-h", "--help"]:
cls.help()
return
- # Allow unambigious abbreviations of tasks
+ # Allow unambiguous abbreviations of tasks
if task not in cls._tasks:
matches = filter(lambda item: item.startswith(task), cls._tasks)
- if len(matches) == 1:
- task = matches[0]
+ 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))
@@ -78,7 +96,7 @@ def help(cls, task=None):
if desc:
line = "%s%s # %s" % (line, " " * (max_len - len(line)), desc)
if len(line) > cols:
- line = line[:cols - 3] + "..."
+ line = line[: cols - 3] + "..."
print(line)
else:
task_func = getattr(cls, task)
@@ -110,22 +128,22 @@ def add(cls, connection):
if os.path.exists(filename):
raise ConfigFileError("There is already a config file at " + filename)
else:
- config = dict(protocol='https')
+ config = {"protocol": "https"}
domain = input("Domain? (leave blank for %s.myshopify.com) " % (connection))
if not domain.strip():
domain = "%s.myshopify.com" % (connection)
- config['domain'] = domain
+ 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
+ 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:
+ 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)
@@ -205,14 +223,16 @@ def console(cls, connection=None):
@classmethod
@usage("version")
- def version(cls, connection=None):
+ 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")))
+ 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):
@@ -221,7 +241,6 @@ def _default_connection_target(cls):
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()
@@ -252,6 +271,7 @@ def _is_default(cls, connection):
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:
diff --git a/setup.py b/setup.py
index 47334b33..eb23ab08 100755
--- a/setup.py
+++ b/setup.py
@@ -1,47 +1,53 @@
from setuptools import setup
-NAME='ShopifyAPI'
-exec(open('shopify/version.py').read())
-DESCRIPTION='Shopify API for Python'
-LONG_DESCRIPTION="""\
+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'],
- scripts=['scripts/shopify_api.py'],
- license='MIT License',
- install_requires=[
- 'pyactiveresource>=2.2.0',
- 'PyYAML',
- '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 :: 2',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Topic :: Software Development',
- 'Topic :: Software Development :: Libraries',
- 'Topic :: Software Development :: Libraries :: Python Modules']
- )
+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",
+ ],
+)
diff --git a/shipit.yml b/shipit.pypi.yml
similarity index 100%
rename from shipit.yml
rename to shipit.pypi.yml
diff --git a/shopify/__init__.py b/shopify/__init__.py
index b10d9a48..d3f53de9 100644
--- a/shopify/__init__.py
+++ b/shopify/__init__.py
@@ -3,4 +3,5 @@
from shopify.resources import *
from shopify.limits import Limits
from shopify.api_version import *
+from shopify.api_access import *
from shopify.collection import PaginatedIterator
diff --git a/shopify/api_access.py b/shopify/api_access.py
new file mode 100644
index 00000000..19b80671
--- /dev/null
+++ b/shopify/api_access.py
@@ -0,0 +1,58 @@
+import 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"),
+ )
diff --git a/shopify/api_version.py b/shopify/api_version.py
index ce3d609a..32276668 100644
--- a/shopify/api_version.py
+++ b/shopify/api_version.py
@@ -1,13 +1,14 @@
import re
-import json
-from six.moves.urllib import request
+
class InvalidVersionError(Exception):
pass
+
class VersionNotFoundError(Exception):
pass
+
class ApiVersion(object):
versions = {}
@@ -16,6 +17,9 @@ 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
@@ -25,15 +29,20 @@ def define_version(cls, version):
@classmethod
def define_known_versions(cls):
- req = request.urlopen("https://app.shopify.com/services/apis.json")
- data = json.loads(req.read().decode("utf-8"))
- for api in data['apis']:
- if api['handle'] == 'admin':
- for release in api['versions']:
- if release['handle'] == 'unstable':
- cls.define_version(Unstable())
- else:
- cls.define_version(Release(release['handle']))
+ 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):
@@ -57,15 +66,15 @@ def __eq__(self, other):
class Release(ApiVersion):
- FORMAT = re.compile(r'^\d{4}-\d{2}$')
- API_PREFIX = '/admin/api'
+ 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)
+ self._numeric_version = int(version_number.replace("-", ""))
+ self._path = "%s/%s" % (self.API_PREFIX, version_number)
@property
def stable(self):
@@ -74,9 +83,9 @@ def stable(self):
class Unstable(ApiVersion):
def __init__(self):
- self._name = 'unstable'
+ self._name = "unstable"
self._numeric_version = 9000000
- self._path = '/admin/api/unstable'
+ self._path = "/admin/api/unstable"
@property
def stable(self):
diff --git a/shopify/base.py b/shopify/base.py
index f2d9eb06..449e288b 100644
--- a/shopify/base.py
+++ b/shopify/base.py
@@ -17,10 +17,6 @@
class ShopifyConnection(pyactiveresource.connection.Connection):
response = None
- def __init__(self, site, user=None, password=None, timeout=None,
- format=formats.JSONFormat):
- super(ShopifyConnection, self).__init__(site, user, password, timeout, format)
-
def _open(self, *args, **kwargs):
self.response = None
try:
@@ -30,16 +26,16 @@ def _open(self, *args, **kwargs):
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):
+ 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
@@ -51,32 +47,29 @@ def connection(cls):
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)
+ 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)
+ 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.")
+ user = property(get_user, set_user, None, "The username for HTTP Basic Auth.")
def get_password(cls):
- return getattr(cls._threadlocal, 'password', ShopifyResource._password)
+ 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.")
+ password = property(get_password, set_password, None, "The password for HTTP Basic Auth.")
def get_site(cls):
- return getattr(cls._threadlocal, 'site', ShopifyResource._site)
+ return getattr(cls._threadlocal, "site", ShopifyResource._site)
def set_site(cls, value):
cls._threadlocal.connection = None
@@ -86,53 +79,49 @@ def set_site(cls, value):
host = parts.hostname
if parts.port:
host += ":" + str(parts.port)
- new_site = urllib.parse.urlunparse((parts.scheme, host, parts.path, '', '', ''))
+ 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.')
+ 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)
+ 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')
+ timeout = property(get_timeout, set_timeout, None, "Socket timeout for HTTP requests")
def get_headers(cls):
- if not hasattr(cls._threadlocal, 'headers'):
+ 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')
+ headers = property(get_headers, set_headers, None, "The headers sent with HTTP requests")
def get_format(cls):
- return getattr(cls._threadlocal, 'format', ShopifyResource._format)
+ 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')
+ 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'):
+ if hasattr(cls, "_prefix_source"):
return cls.site + cls._prefix_source
else:
return cls.site
@@ -141,37 +130,33 @@ 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.')
+ 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)
+ 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]
+ 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')
+ version = property(get_version, set_version, None, "Shopify Api Version")
def get_url(cls):
- return getattr(cls._threadlocal, 'url', ShopifyResource._url)
+ 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')
+ 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])}
+ _headers = {"User-Agent": "ShopifyPythonAPI/%s Python/%s" % (shopify.VERSION, sys.version.split(" ", 1)[0])}
_version = None
_url = None
@@ -194,7 +179,7 @@ def activate_session(cls, session):
cls.user = None
cls.password = None
cls.version = session.api_version.name
- cls.headers['X-Shopify-Access-Token'] = session.token
+ cls.headers["X-Shopify-Access-Token"] = session.token
@classmethod
def clear_session(cls):
@@ -203,7 +188,7 @@ def clear_session(cls):
cls.user = None
cls.password = None
cls.version = None
- cls.headers.pop('X-Shopify-Access-Token', None)
+ cls.headers.pop("X-Shopify-Access-Token", None)
@classmethod
def find(cls, id_=None, from_=None, **kwargs):
diff --git a/shopify/collection.py b/shopify/collection.py
index b254218f..62728eb9 100644
--- a/shopify/collection.py
+++ b/shopify/collection.py
@@ -1,6 +1,5 @@
from pyactiveresource.collection import Collection
-from six.moves.urllib.parse import urlparse, parse_qs
-import cgi
+
class PaginatedCollection(Collection):
"""
@@ -29,11 +28,11 @@ def __init__(self, *args, **kwargs):
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.")
+ 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_page_url = self.metadata["pagination"].get("next", None)
+ self.previous_page_url = self.metadata["pagination"].get("previous", None)
self._next = None
self._previous = None
@@ -55,13 +54,11 @@ def __parse_pagination(self):
return result
def has_previous_page(self):
- """Returns true if the current page has any previous pages before it.
- """
+ """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.
- """
+ """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):
@@ -141,6 +138,7 @@ class PaginatedIterator(object):
...
# every page and the page items are iterated
"""
+
def __init__(self, collection):
if not isinstance(collection, PaginatedCollection):
raise TypeError("PaginatedIterator expects a PaginatedCollection instance")
diff --git a/shopify/limits.py b/shopify/limits.py
index 711e498d..0246c793 100644
--- a/shopify/limits.py
+++ b/shopify/limits.py
@@ -8,9 +8,10 @@ class Limits(object):
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'
+ CREDIT_LIMIT_HEADER_PARAM = "X-Shopify-Shop-Api-Call-Limit"
@classmethod
def response(cls):
@@ -21,14 +22,14 @@ def response(cls):
@classmethod
def api_credit_limit_param(cls):
response = cls.response()
- _safe_header = getattr(response, "headers", '')
+ _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('/')
+ return credits.split("/")
else:
raise Exception("No valid api call header found")
diff --git a/shopify/mixins.py b/shopify/mixins.py
index c7806a0c..5a13ca3a 100644
--- a/shopify/mixins.py
+++ b/shopify/mixins.py
@@ -1,7 +1,7 @@
import shopify.resources
-class Countable(object):
+class Countable(object):
@classmethod
def count(cls, _options=None, **kwargs):
if _options is None:
@@ -10,7 +10,6 @@ def count(cls, _options=None, **kwargs):
class Metafields(object):
-
def metafields(self, _options=None, **kwargs):
if _options is None:
_options = kwargs
@@ -25,12 +24,11 @@ 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._prefix_options = {"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)
diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py
index 803c5351..0d420b38 100644
--- a/shopify/resources/__init__.py
+++ b/shopify/resources/__init__.py
@@ -17,6 +17,7 @@
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
@@ -37,7 +38,7 @@
from .page import Page
from .country import Country
from .refund import Refund
-from .fulfillment import Fulfillment, FulfillmentOrders
+from .fulfillment import Fulfillment, FulfillmentOrders, FulfillmentV2
from .fulfillment_event import FulfillmentEvent
from .fulfillment_service import FulfillmentService
from .carrier_service import CarrierService
diff --git a/shopify/resources/api_permission.py b/shopify/resources/api_permission.py
index 0f4f9fc6..1c936451 100644
--- a/shopify/resources/api_permission.py
+++ b/shopify/resources/api_permission.py
@@ -1,9 +1,9 @@
from ..base import ShopifyResource
-class ApiPermission(ShopifyResource):
+class ApiPermission(ShopifyResource):
@classmethod
def delete(cls):
- cls.connection.delete(cls.site + '/api_permissions/current.' + cls.format.extension, cls.headers)
+ cls.connection.delete(cls.site + "/api_permissions/current." + cls.format.extension, cls.headers)
destroy = delete
diff --git a/shopify/resources/application_charge.py b/shopify/resources/application_charge.py
index df19e18c..6ed62f77 100644
--- a/shopify/resources/application_charge.py
+++ b/shopify/resources/application_charge.py
@@ -2,6 +2,5 @@
class ApplicationCharge(ShopifyResource):
-
def activate(self):
self._load_attributes_from_response(self.post("activate"))
diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py
new file mode 100644
index 00000000..ecc12fa0
--- /dev/null
+++ b/shopify/resources/application_credit.py
@@ -0,0 +1,5 @@
+from ..base import ShopifyResource
+
+
+class ApplicationCredit(ShopifyResource):
+ pass
diff --git a/shopify/resources/article.py b/shopify/resources/article.py
index 5c6132c8..2b061a3e 100644
--- a/shopify/resources/article.py
+++ b/shopify/resources/article.py
@@ -19,8 +19,8 @@ def comments(self):
@classmethod
def authors(cls, **kwargs):
- return cls.get('authors', **kwargs)
+ return cls.get("authors", **kwargs)
@classmethod
def tags(cls, **kwargs):
- return cls.get('tags', **kwargs)
+ return cls.get("tags", **kwargs)
diff --git a/shopify/resources/asset.py b/shopify/resources/asset.py
index 899e026f..d5156a5a 100644
--- a/shopify/resources/asset.py
+++ b/shopify/resources/asset.py
@@ -18,8 +18,12 @@ def _prefix(cls, options={}):
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))
+ 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):
diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py
index 819a8ea5..aefa87ab 100644
--- a/shopify/resources/balance.py
+++ b/shopify/resources/balance.py
@@ -4,4 +4,4 @@
class Balance(ShopifyResource, mixins.Metafields):
_prefix_source = "/shopify_payments/"
- _singular = _plural = 'balance'
+ _singular = _plural = "balance"
diff --git a/shopify/resources/blog.py b/shopify/resources/blog.py
index d623f011..e88b26b1 100644
--- a/shopify/resources/blog.py
+++ b/shopify/resources/blog.py
@@ -4,6 +4,5 @@
class Blog(ShopifyResource, mixins.Metafields, mixins.Events):
-
def articles(self):
return shopify.Article.find(blog_id=self.id)
diff --git a/shopify/resources/collection_listing.py b/shopify/resources/collection_listing.py
index 7bd25107..00567489 100644
--- a/shopify/resources/collection_listing.py
+++ b/shopify/resources/collection_listing.py
@@ -1,7 +1,8 @@
from ..base import ShopifyResource
+
class CollectionListing(ShopifyResource):
_primary_key = "collection_id"
def product_ids(cls, **kwargs):
- return cls.get('product_ids', **kwargs)
+ return cls.get("product_ids", **kwargs)
diff --git a/shopify/resources/comment.py b/shopify/resources/comment.py
index 0015eb74..110afd61 100644
--- a/shopify/resources/comment.py
+++ b/shopify/resources/comment.py
@@ -2,7 +2,6 @@
class Comment(ShopifyResource):
-
def remove(self):
self._load_attributes_from_response(self.post("remove"))
diff --git a/shopify/resources/custom_collection.py b/shopify/resources/custom_collection.py
index 87ffae1a..85bcbc4a 100644
--- a/shopify/resources/custom_collection.py
+++ b/shopify/resources/custom_collection.py
@@ -4,12 +4,11 @@
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})
+ 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)
diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py
index ca4a51f2..ab989e84 100644
--- a/shopify/resources/customer.py
+++ b/shopify/resources/customer.py
@@ -1,10 +1,10 @@
from ..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):
"""
@@ -21,6 +21,9 @@ def search(cls, **kwargs):
"""
return cls._build_collection(cls.get("search", **kwargs))
- def send_invite(self, customer_invite = CustomerInvite()):
+ 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)
diff --git a/shopify/resources/customer_saved_search.py b/shopify/resources/customer_saved_search.py
index 78f54a76..dbe97251 100644
--- a/shopify/resources/customer_saved_search.py
+++ b/shopify/resources/customer_saved_search.py
@@ -3,6 +3,5 @@
class CustomerSavedSearch(ShopifyResource):
-
def customers(cls, **kwargs):
return Customer._build_collection(cls.get("customers", **kwargs))
diff --git a/shopify/resources/discount_code.py b/shopify/resources/discount_code.py
index 936f254c..e2559e3e 100644
--- a/shopify/resources/discount_code.py
+++ b/shopify/resources/discount_code.py
@@ -1,4 +1,5 @@
from ..base import ShopifyResource
+
class DiscountCode(ShopifyResource):
_prefix_source = "/price_rules/$price_rule_id/"
diff --git a/shopify/resources/discount_code_creation.py b/shopify/resources/discount_code_creation.py
index fa689ce0..e72de283 100644
--- a/shopify/resources/discount_code_creation.py
+++ b/shopify/resources/discount_code_creation.py
@@ -1,12 +1,17 @@
from ..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))
+ 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,
+ )
+ )
diff --git a/shopify/resources/draft_order.py b/shopify/resources/draft_order.py
index b8492d36..878cb7af 100644
--- a/shopify/resources/draft_order.py
+++ b/shopify/resources/draft_order.py
@@ -4,12 +4,12 @@
class DraftOrder(ShopifyResource, mixins.Metafields):
- def send_invoice(self, draft_order_invoice = DraftOrderInvoice()):
+ 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'))
+ 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"))
+ self._load_attributes_from_response(self.put("complete"))
diff --git a/shopify/resources/event.py b/shopify/resources/event.py
index 0fc1e6ff..f3268e13 100644
--- a/shopify/resources/event.py
+++ b/shopify/resources/event.py
@@ -1,5 +1,6 @@
from ..base import ShopifyResource
+
class Event(ShopifyResource):
_prefix_source = "/$resource/$resource_id/"
diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py
index 39277acc..fcf74863 100644
--- a/shopify/resources/fulfillment.py
+++ b/shopify/resources/fulfillment.py
@@ -23,15 +23,11 @@ def update_tracking(self, tracking_info, notify_customer):
class FulfillmentOrders(ShopifyResource):
_prefix_source = "/orders/$order_id/"
+
class FulfillmentV2(ShopifyResource):
- _singular = 'fulfillment'
- _plural = 'fulfillments'
-
+ _singular = "fulfillment"
+ _plural = "fulfillments"
+
def update_tracking(self, tracking_info, notify_customer):
- body = {
- "fulfillment": {
- "tracking_info": tracking_info,
- "notify_customer": notify_customer
- }
- }
+ body = {"fulfillment": {"tracking_info": tracking_info, "notify_customer": notify_customer}}
return self.post("update_tracking", json.dumps(body).encode())
diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py
index 886e4598..fbd2ece7 100644
--- a/shopify/resources/fulfillment_event.py
+++ b/shopify/resources/fulfillment_event.py
@@ -1,21 +1,32 @@
from ..base import ShopifyResource
+
class FulfillmentEvent(ShopifyResource):
_prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id/"
- _singular = 'event'
- _plural = 'events'
+ _singular = "event"
+ _plural = "events"
@classmethod
def _prefix(cls, options={}):
order_id = options.get("order_id")
- fulfillment_id = options.get('fulfillment_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)
+ 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()
+ 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()
diff --git a/shopify/resources/gift_card.py b/shopify/resources/gift_card.py
index 6067c7f3..c1918c68 100644
--- a/shopify/resources/gift_card.py
+++ b/shopify/resources/gift_card.py
@@ -3,7 +3,6 @@
class GiftCard(ShopifyResource):
-
def disable(self):
self._load_attributes_from_response(self.post("disable"))
diff --git a/shopify/resources/gift_card_adjustment.py b/shopify/resources/gift_card_adjustment.py
index c17af431..2314cdb6 100644
--- a/shopify/resources/gift_card_adjustment.py
+++ b/shopify/resources/gift_card_adjustment.py
@@ -3,5 +3,5 @@
class GiftCardAdjustment(ShopifyResource):
_prefix_source = "/admin/gift_cards/$gift_card_id/"
- _plural = 'adjustments'
- _singular = 'adjustment'
+ _plural = "adjustments"
+ _singular = "adjustment"
diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py
index b518be31..33525ef1 100644
--- a/shopify/resources/graphql.py
+++ b/shopify/resources/graphql.py
@@ -3,10 +3,10 @@
from six.moves import urllib
import json
-class GraphQL():
+class GraphQL:
def __init__(self):
- self.endpoint = (shopify.ShopifyResource.get_site() + "/graphql.json")
+ self.endpoint = shopify.ShopifyResource.get_site() + "/graphql.json"
self.headers = shopify.ShopifyResource.get_headers()
def merge_headers(self, *headers):
@@ -15,19 +15,18 @@ def merge_headers(self, *headers):
merged_headers.update(header)
return merged_headers
- def execute(self, query, variables=None):
+ def execute(self, query, variables=None, operation_name=None):
endpoint = self.endpoint
- default_headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
+ default_headers = {"Accept": "application/json", "Content-Type": "application/json"}
headers = self.merge_headers(default_headers, self.headers)
- data = {'query': query,
- 'variables': variables}
+ data = {"query": query, "variables": variables, "operationName": operation_name}
- req = urllib.request.Request(self.endpoint, json.dumps(data).encode('utf-8'), headers)
+ 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')
+ return response.read().decode("utf-8")
except urllib.error.HTTPError as e:
print((e.read()))
- print('')
+ print("")
raise e
diff --git a/shopify/resources/image.py b/shopify/resources/image.py
index fc45ccc8..1a4d13fb 100644
--- a/shopify/resources/image.py
+++ b/shopify/resources/image.py
@@ -30,10 +30,12 @@ def attach_image(self, data, filename=None):
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)))
+ 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
+ if "product_id" not in self._prefix_options:
+ self._prefix_options["product_id"] = self.product_id
return super(ShopifyResource, self).save()
diff --git a/shopify/resources/inventory_level.py b/shopify/resources/inventory_level.py
index 8f24c723..5b7f4b0a 100644
--- a/shopify/resources/inventory_level.py
+++ b/shopify/resources/inventory_level.py
@@ -4,47 +4,50 @@
class InventoryLevel(ShopifyResource):
-
def __repr__(self):
- return '%s(inventory_item_id=%s, location_id=%s)' % (self._singular, self.inventory_item_id, self.location_id)
+ 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))
+ 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
+ "inventory_item_id": inventory_item_id,
+ "location_id": location_id,
+ "available_adjustment": available_adjustment,
}
- resource = cls.post('adjust', body=json.dumps(body).encode())
+ 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,
+ "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())
+ 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,
+ "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())
+ resource = cls.post("set", body=json.dumps(body).encode())
return InventoryLevel(InventoryLevel.format.decode(resource.body))
def is_new(self):
diff --git a/shopify/resources/line_item.py b/shopify/resources/line_item.py
index cac2ceed..c701c90f 100644
--- a/shopify/resources/line_item.py
+++ b/shopify/resources/line_item.py
@@ -2,5 +2,5 @@
class LineItem(ShopifyResource):
- class Property(ShopifyResource):
- pass
+ class Property(ShopifyResource):
+ pass
diff --git a/shopify/resources/location.py b/shopify/resources/location.py
index c9424bc7..51e7ecdd 100644
--- a/shopify/resources/location.py
+++ b/shopify/resources/location.py
@@ -1,7 +1,9 @@
from ..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)
+ return InventoryLevel.find(
+ from_="%s/locations/%s/inventory_levels.json" % (ShopifyResource.site, self.id), **kwargs
+ )
diff --git a/shopify/resources/marketing_event.py b/shopify/resources/marketing_event.py
index 9e07deb8..6b629449 100644
--- a/shopify/resources/marketing_event.py
+++ b/shopify/resources/marketing_event.py
@@ -1,7 +1,8 @@
import 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())
+ engagements_json = json.dumps({"engagements": engagements})
+ return self.post("engagements", engagements_json.encode())
diff --git a/shopify/resources/order.py b/shopify/resources/order.py
index 4e780b0a..2e31a8c3 100644
--- a/shopify/resources/order.py
+++ b/shopify/resources/order.py
@@ -4,6 +4,15 @@
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"))
diff --git a/shopify/resources/order_risk.py b/shopify/resources/order_risk.py
index 4fb6c5d5..fdcfa1f3 100644
--- a/shopify/resources/order_risk.py
+++ b/shopify/resources/order_risk.py
@@ -1,6 +1,7 @@
from ..base import ShopifyResource
+
class OrderRisk(ShopifyResource):
- _prefix_source = "/orders/$order_id/"
- _singular = "risk"
- _plural = "risks"
+ _prefix_source = "/orders/$order_id/"
+ _singular = "risk"
+ _plural = "risks"
diff --git a/shopify/resources/policy.py b/shopify/resources/policy.py
index caac98b9..d97fcc2e 100644
--- a/shopify/resources/policy.py
+++ b/shopify/resources/policy.py
@@ -2,5 +2,6 @@
from shopify import mixins
import shopify
+
class Policy(ShopifyResource, mixins.Metafields, mixins.Events):
- pass
+ pass
diff --git a/shopify/resources/price_rule.py b/shopify/resources/price_rule.py
index a80d592a..41fe3e04 100644
--- a/shopify/resources/price_rule.py
+++ b/shopify/resources/price_rule.py
@@ -3,20 +3,22 @@
from .discount_code import DiscountCode
from .discount_code_creation import DiscountCodeCreation
+
class PriceRule(ShopifyResource):
- def add_discount_code(self, discount_code = DiscountCode()):
+ 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})
+ 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))
+ return DiscountCodeCreation.find_one(
+ "%s/price_rules/%s/batch/%s.%s" % (ShopifyResource.site, self.id, batch_id, PriceRule.format.extension)
+ )
diff --git a/shopify/resources/product.py b/shopify/resources/product.py
index b8bc3c70..cc16e3e3 100644
--- a/shopify/resources/product.py
+++ b/shopify/resources/product.py
@@ -4,7 +4,6 @@
class Product(ShopifyResource, mixins.Metafields, mixins.Events):
-
def price_range(self):
prices = [float(variant.price) for variant in self.variants]
f = "%0.2f"
@@ -28,18 +27,17 @@ def remove_from_collection(self, collection):
return collection.remove_product(self)
def add_variant(self, variant):
- variant.attributes['product_id'] = self.id
+ variant.attributes["product_id"] = self.id
return variant.save()
def save(self):
- start_api_version = '201910'
+ 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:
+ 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']
+ 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()
diff --git a/shopify/resources/product_listing.py b/shopify/resources/product_listing.py
index cd11ac2e..3e59d6c1 100644
--- a/shopify/resources/product_listing.py
+++ b/shopify/resources/product_listing.py
@@ -1,8 +1,9 @@
from ..base import ShopifyResource
+
class ProductListing(ShopifyResource):
_primary_key = "product_id"
@classmethod
def product_ids(cls, **kwargs):
- return cls.get('product_ids', **kwargs)
+ return cls.get("product_ids", **kwargs)
diff --git a/shopify/resources/recurring_application_charge.py b/shopify/resources/recurring_application_charge.py
index d7b0d9ed..c94cac2d 100644
--- a/shopify/resources/recurring_application_charge.py
+++ b/shopify/resources/recurring_application_charge.py
@@ -1,6 +1,7 @@
from ..base import ShopifyResource
from .usage_charge import UsageCharge
+
def _get_first_by_status(resources, status):
for resource in resources:
if resource.status == status:
@@ -9,12 +10,11 @@ def _get_first_by_status(resources, status):
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))
+ self._load_attributes_from_response(self.put("customize", recurring_application_charge=kwargs))
@classmethod
def current(cls):
diff --git a/shopify/resources/refund.py b/shopify/resources/refund.py
index 6a62b513..124036b3 100644
--- a/shopify/resources/refund.py
+++ b/shopify/resources/refund.py
@@ -22,13 +22,8 @@ def calculate(cls, order_id, shipping=None, refund_line_items=None):
"""
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}
- )
+ 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})
diff --git a/shopify/resources/shop.py b/shopify/resources/shop.py
index 7d4a73da..4d447366 100644
--- a/shopify/resources/shop.py
+++ b/shopify/resources/shop.py
@@ -4,7 +4,6 @@
class Shop(ShopifyResource):
-
@classmethod
def current(cls):
return cls.find_one(cls.site + "/shop." + cls.format.extension)
diff --git a/shopify/resources/smart_collection.py b/shopify/resources/smart_collection.py
index efea6347..802c3a7e 100644
--- a/shopify/resources/smart_collection.py
+++ b/shopify/resources/smart_collection.py
@@ -4,6 +4,5 @@
class SmartCollection(ShopifyResource, mixins.Metafields, mixins.Events):
-
def products(self):
return shopify.Product.find(collection_id=self.id)
diff --git a/shopify/resources/tender_transaction.py b/shopify/resources/tender_transaction.py
index 86912755..0999ab6e 100644
--- a/shopify/resources/tender_transaction.py
+++ b/shopify/resources/tender_transaction.py
@@ -1,4 +1,5 @@
from ..base import ShopifyResource
+
class TenderTransaction(ShopifyResource):
pass
diff --git a/shopify/resources/usage_charge.py b/shopify/resources/usage_charge.py
index 707730e0..bd5cd757 100644
--- a/shopify/resources/usage_charge.py
+++ b/shopify/resources/usage_charge.py
@@ -1,5 +1,6 @@
from ..base import ShopifyResource
+
class UsageCharge(ShopifyResource):
_prefix_source = "/recurring_application_charge/$recurring_application_charge_id/"
diff --git a/shopify/resources/user.py b/shopify/resources/user.py
index 1b2a21f6..a1b50cb5 100644
--- a/shopify/resources/user.py
+++ b/shopify/resources/user.py
@@ -2,7 +2,6 @@
class User(ShopifyResource):
-
@classmethod
def current(cls):
- return User(cls.get('current'))
+ return User(cls.get("current"))
diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py
index c0a38e6c..743b071b 100644
--- a/shopify/resources/variant.py
+++ b/shopify/resources/variant.py
@@ -14,16 +14,15 @@ def _prefix(cls, options={}):
return cls.site
def save(self):
- if 'product_id' not in self._prefix_options:
- self._prefix_options['product_id'] = self.product_id
+ if "product_id" not in self._prefix_options:
+ self._prefix_options["product_id"] = self.product_id
- start_api_version = '201910'
+ 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']
+ 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()
diff --git a/shopify/resources/webhook.py b/shopify/resources/webhook.py
index 452934f3..ba8a7f28 100644
--- a/shopify/resources/webhook.py
+++ b/shopify/resources/webhook.py
@@ -2,7 +2,6 @@
class Webhook(ShopifyResource):
-
def __get_format(self):
return self.attributes.get("format")
diff --git a/shopify/session.py b/shopify/session.py
index 0490e27c..561faacf 100644
--- a/shopify/session.py
+++ b/shopify/session.py
@@ -2,6 +2,7 @@
import hmac
import json
from hashlib import sha256
+
try:
import simplejson as json
except ImportError:
@@ -9,17 +10,20 @@
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'
+ protocol = "https"
+ myshopify_domain = "myshopify.com"
port = None
@classmethod
@@ -31,8 +35,9 @@ def setup(cls, **kwargs):
@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_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)
@@ -41,15 +46,20 @@ def temp(cls, domain, version, token):
yield
shopify.ShopifyResource.activate_session(original_session)
- def __init__(self, shop_url, version=None, token=None):
+ 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
+ def create_permission_url(self, redirect_uri, scope=None, state=None):
+ query_params = {"client_id": self.api_key, "redirect_uri": redirect_uri}
+ # `scope` should be omitted if provided by app's TOML
+ if scope:
+ query_params["scope"] = ",".join(scope)
+ 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):
@@ -57,17 +67,20 @@ def request_token(self, params):
return self.token
if not self.validate_params(params):
- raise ValidationException('Invalid HMAC: Possibly malicious login')
+ raise ValidationException("Invalid HMAC: Possibly malicious login")
- code = params['code']
+ 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'))
+ query_params = {"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:
- self.token = json.loads(response.read().decode('utf-8'))['access_token']
+ 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)
@@ -84,6 +97,17 @@ def site(self):
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() == ""):
@@ -107,18 +131,18 @@ 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:
+ 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:
+ if "hmac" not in params:
return False
- hmac_calculated = cls.calculate_hmac(params).encode('utf-8')
- hmac_to_verify = params['hmac'].encode('utf-8')
+ 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.
@@ -142,19 +166,20 @@ 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':
+ 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('[]')
+ 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")
+ yield "{0}={1}".format(k, v).replace("&", "%26")
return "&".join(sorted(encoded_pairs(params)))
diff --git a/shopify/session_token.py b/shopify/session_token.py
new file mode 100644
index 00000000..91a4970b
--- /dev/null
+++ b/shopify/session_token.py
@@ -0,0 +1,84 @@
+import 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")
diff --git a/shopify/utils/__init__.py b/shopify/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/shopify/utils/shop_url.py b/shopify/utils/shop_url.py
new file mode 100644
index 00000000..76e088a7
--- /dev/null
+++ b/shopify/utils/shop_url.py
@@ -0,0 +1,20 @@
+import 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
diff --git a/shopify/version.py b/shopify/version.py
index 1010d782..dfb0b4e4 100644
--- a/shopify/version.py
+++ b/shopify/version.py
@@ -1 +1 @@
-VERSION = '8.2.0'
+VERSION = "12.7.1"
diff --git a/shopify/yamlobjects.py b/shopify/yamlobjects.py
index d4e7cd3a..c7438c42 100644
--- a/shopify/yamlobjects.py
+++ b/shopify/yamlobjects.py
@@ -8,11 +8,12 @@
import yaml
class YAMLHashWithIndifferentAccess(yaml.YAMLObject):
- yaml_tag = '!map:ActiveSupport::HashWithIndifferentAccess'
+ 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
diff --git a/test/access_scope_test.py b/test/access_scope_test.py
index fa231a78..e931263a 100644
--- a/test/access_scope_test.py
+++ b/test/access_scope_test.py
@@ -1,11 +1,10 @@
import shopify
from test.test_helper import TestCase
-class AccessScopeTest(TestCase):
- def test_find_should_return_all_access_scopes(self):
- self.fake('oauth/access_scopes', body=self.load_fixture('access_scopes'),
- prefix='/admin')
- scopes = shopify.AccessScope.find()
- self.assertEqual(3, len(scopes))
- self.assertEqual('read_products', scopes[0].handle)
+class AccessScopeTest(TestCase):
+ def test_find_should_return_all_access_scopes(self):
+ self.fake("oauth/access_scopes", body=self.load_fixture("access_scopes"), prefix="/admin")
+ scopes = shopify.AccessScope.find()
+ self.assertEqual(3, len(scopes))
+ self.assertEqual("read_products", scopes[0].handle)
diff --git a/test/api_access_test.py b/test/api_access_test.py
new file mode 100644
index 00000000..21866a09
--- /dev/null
+++ b/test/api_access_test.py
@@ -0,0 +1,153 @@
+from shopify import ApiAccess, ApiAccessError
+from test.test_helper import TestCase
+
+
+class ApiAccessTest(TestCase):
+ def test_creating_scopes_from_a_string_works_with_a_comma_separated_list(self):
+ deserialized_read_products_write_orders = ApiAccess("read_products,write_orders")
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_creating_api_access_from_invalid_scopes_raises(self):
+ with self.assertRaises(ApiAccessError) as cm:
+ api_access = ApiAccess("bad_scope, read_orders,write_orders")
+
+ self.assertEqual("'bad_scope' is not a valid access scope", str(cm.exception))
+
+ def test_returns_list_of_reduced_scopes(self):
+ api_access = ApiAccess("read_products, read_orders,write_orders")
+ expected_scopes = set(["read_products", "write_orders"])
+ scopes = list(api_access)
+
+ self.assertEqual(expected_scopes, set(scopes))
+
+ def test_write_is_the_same_access_as_read_write_on_the_same_resource(self):
+ read_write_orders = ApiAccess(["read_orders", "write_orders"])
+ write_orders = ApiAccess("write_orders")
+
+ self.assertEqual(write_orders, read_write_orders)
+
+ def test_write_is_the_same_access_as_read_write_on_the_same_unauthenticated_resource(self):
+ unauthenticated_read_write_orders = ApiAccess(["unauthenticated_read_orders", "unauthenticated_write_orders"])
+ unauthenticated_write_orders = ApiAccess("unauthenticated_write_orders")
+
+ self.assertEqual(unauthenticated_write_orders, unauthenticated_read_write_orders)
+
+ def test_read_is_not_the_same_as_read_write_on_the_same_resource(self):
+ read_orders = ApiAccess("read_orders")
+ read_write_orders = ApiAccess(["write_orders", "read_orders"])
+
+ self.assertNotEqual(read_write_orders, read_orders)
+
+ def test_two_different_resources_are_not_equal(self):
+ read_orders = ApiAccess("read_orders")
+ read_products = ApiAccess("read_products")
+
+ self.assertNotEqual(read_orders, read_products)
+
+ def test_two_identical_scopes_are_equal(self):
+ read_orders = ApiAccess("read_orders")
+ read_orders_identical = ApiAccess("read_orders")
+
+ self.assertEqual(read_orders, read_orders_identical)
+
+ def test_unauthenticated_is_not_implied_by_authenticated_access(self):
+ unauthenticated_orders = ApiAccess("unauthenticated_read_orders")
+ authenticated_read_orders = ApiAccess("read_orders")
+ authenticated_write_orders = ApiAccess("write_orders")
+
+ self.assertNotEqual(unauthenticated_orders, authenticated_read_orders)
+ self.assertNotEqual(unauthenticated_orders, authenticated_write_orders)
+
+ def test_scopes_covers_is_truthy_for_same_scopes(self):
+ read_orders = ApiAccess("read_orders")
+ read_orders_identical = ApiAccess("read_orders")
+
+ self.assertTrue(read_orders.covers(read_orders_identical))
+
+ def test_covers_is_falsy_for_different_scopes(self):
+ read_orders = ApiAccess("read_orders")
+ read_products = ApiAccess("read_products")
+
+ self.assertFalse(read_orders.covers(read_products))
+
+ def test_covers_is_truthy_for_read_when_the_set_has_read_write(self):
+ write_products = ApiAccess("write_products")
+ read_products = ApiAccess("read_products")
+
+ self.assertTrue(write_products.covers(read_products))
+
+ def test_covers_is_truthy_for_read_when_the_set_has_read_write_for_that_resource_and_others(self):
+ write_products_and_orders = ApiAccess(["write_products", "write_orders"])
+ read_orders = ApiAccess("read_orders")
+
+ self.assertTrue(write_products_and_orders.covers(read_orders))
+
+ def test_covers_is_truthy_for_write_when_the_set_has_read_write_for_that_resource_and_others(self):
+ write_products_and_orders = ApiAccess(["write_products", "write_orders"])
+ write_orders = ApiAccess("write_orders")
+
+ self.assertTrue(write_products_and_orders.covers(write_orders))
+
+ def test_covers_is_truthy_for_subset_of_scopes(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_orders_products = ApiAccess(["write_orders", "read_products"])
+
+ self.assertTrue(write_products_orders_customers.covers(write_orders_products))
+
+ def test_covers_is_falsy_for_sets_of_scopes_that_have_no_common_elements(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_images_read_content = ApiAccess(["write_images", "read_content"])
+
+ self.assertFalse(write_products_orders_customers.covers(write_images_read_content))
+
+ def test_covers_is_falsy_for_sets_of_scopes_that_have_only_some_common_access(self):
+ write_products_orders_customers = ApiAccess(["write_products", "write_orders", "write_customers"])
+ write_products_read_content = ApiAccess(["write_products", "read_content"])
+
+ self.assertFalse(write_products_orders_customers.covers(write_products_read_content))
+
+ def test_duplicate_scopes_resolve_to_one_scope(self):
+ read_orders_duplicated = ApiAccess(["read_orders", "read_orders", "read_orders", "read_orders"])
+ read_orders = ApiAccess("read_orders")
+
+ self.assertEqual(read_orders, read_orders_duplicated)
+
+ def test_to_s_outputs_scopes_as_a_comma_separated_list_without_implied_read_scopes(self):
+ serialized_read_products_write_orders = "read_products,write_orders"
+ read_products_write_orders = ApiAccess(["read_products", "read_orders", "write_orders"])
+
+ self.assertIn("read_products", str(read_products_write_orders))
+ self.assertIn("write_orders", str(read_products_write_orders))
+
+ def test_to_a_outputs_scopes_as_an_array_of_strings_without_implied_read_scopes(self):
+ serialized_read_products_write_orders = ["write_orders", "read_products"]
+ read_products_write_orders = ApiAccess(["read_products", "read_orders", "write_orders"])
+
+ self.assertEqual(set(serialized_read_products_write_orders), set(list(read_products_write_orders)))
+
+ def test_creating_scopes_removes_extra_whitespace_from_scope_name_and_blank_scope_names(self):
+ deserialized_read_products_write_orders = ApiAccess([" read_products", " ", "write_orders "])
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_creating_scopes_from_a_string_works_with_a_comma_separated_list(self):
+ deserialized_read_products_write_orders = ApiAccess("read_products,write_orders")
+ serialized_read_products_write_orders = str(deserialized_read_products_write_orders)
+ expected_read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(expected_read_products_write_orders, ApiAccess(serialized_read_products_write_orders))
+
+ def test_using_to_s_from_one_scopes_to_construct_another_will_be_equal(self):
+ read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(read_products_write_orders, ApiAccess(str(read_products_write_orders)))
+
+ def test_using_to_a_from_one_scopes_to_construct_another_will_be_equal(self):
+ read_products_write_orders = ApiAccess(["read_products", "write_orders"])
+
+ self.assertEqual(read_products_write_orders, ApiAccess(list(read_products_write_orders)))
diff --git a/test/api_permission_test.py b/test/api_permission_test.py
index 541e060b..1e93ee74 100644
--- a/test/api_permission_test.py
+++ b/test/api_permission_test.py
@@ -1,14 +1,9 @@
import shopify
from test.test_helper import TestCase
-class ApiPermissionTest(TestCase):
+class ApiPermissionTest(TestCase):
def test_delete_api_permission(self):
- self.fake(
- 'api_permissions/current',
- method='DELETE',
- code=200,
- body='{}'
- )
+ self.fake("api_permissions/current", method="DELETE", code=200, body="{}")
shopify.ApiPermission.delete()
diff --git a/test/api_version_test.py b/test/api_version_test.py
index 9e514f85..9dce8cb2 100644
--- a/test/api_version_test.py
+++ b/test/api_version_test.py
@@ -12,34 +12,50 @@ def tearDown(self):
shopify.ApiVersion.define_known_versions()
def test_unstable_api_path_returns_correct_url(self):
- self.assertEqual('https://fakeshop.myshopify.com/admin/api/unstable',
- shopify.Unstable().api_path('https://fakeshop.myshopify.com'))
+ self.assertEqual(
+ "https://fakeshop.myshopify.com/admin/api/unstable",
+ shopify.Unstable().api_path("https://fakeshop.myshopify.com"),
+ )
def test_coerce_to_version_returns_known_versions(self):
v1 = shopify.Unstable()
- v2 = shopify.ApiVersion.define_version(shopify.Release('2019-01'))
+ v2 = shopify.ApiVersion.define_version(shopify.Release("2019-01"))
self.assertNotEqual(v1, None)
- self.assertEqual(v1, shopify.ApiVersion.coerce_to_version('unstable'))
- self.assertEqual(v2, shopify.ApiVersion.coerce_to_version('2019-01'))
+ self.assertEqual(v1, shopify.ApiVersion.coerce_to_version("unstable"))
+ self.assertEqual(v2, shopify.ApiVersion.coerce_to_version("2019-01"))
def test_coerce_to_version_raises_with_string_that_does_not_match_known_version(self):
with self.assertRaises(shopify.VersionNotFoundError):
- shopify.ApiVersion.coerce_to_version('crazy-name')
+ shopify.ApiVersion.coerce_to_version("crazy-name")
+ def test_coerce_to_version_creates_new_release_on_the_fly(self):
+ new_version = "2025-01"
+ coerced_version = shopify.ApiVersion.coerce_to_version(new_version)
+
+ self.assertIsInstance(coerced_version, shopify.Release)
+ self.assertEqual(coerced_version.name, new_version)
+ self.assertEqual(
+ coerced_version.api_path("https://test.myshopify.com"),
+ f"https://test.myshopify.com/admin/api/{new_version}",
+ )
+
+ # Verify that the new version is not added to the known versions
+ self.assertNotIn(new_version, shopify.ApiVersion.versions)
-class ReleaseTest(TestCase):
+class ReleaseTest(TestCase):
def test_raises_if_format_invalid(self):
with self.assertRaises(shopify.InvalidVersionError):
- shopify.Release('crazy-name')
+ shopify.Release("crazy-name")
def test_release_api_path_returns_correct_url(self):
- self.assertEqual('https://fakeshop.myshopify.com/admin/api/2019-04',
- shopify.Release('2019-04').api_path('https://fakeshop.myshopify.com'))
+ self.assertEqual(
+ "https://fakeshop.myshopify.com/admin/api/2019-04",
+ shopify.Release("2019-04").api_path("https://fakeshop.myshopify.com"),
+ )
def test_two_release_versions_with_same_number_are_equal(self):
- version1 = shopify.Release('2019-01')
- version2 = shopify.Release('2019-01')
+ version1 = shopify.Release("2019-01")
+ version2 = shopify.Release("2019-01")
self.assertEqual(version1, version2)
-
diff --git a/test/application_credit_test.py b/test/application_credit_test.py
new file mode 100644
index 00000000..23656b60
--- /dev/null
+++ b/test/application_credit_test.py
@@ -0,0 +1,33 @@
+import shopify
+import json
+from test.test_helper import TestCase
+
+
+class ApplicationCreditTest(TestCase):
+ def test_get_application_credit(self):
+ self.fake("application_credits/445365009", method="GET", body=self.load_fixture("application_credit"), code=200)
+ application_credit = shopify.ApplicationCredit.find(445365009)
+ self.assertEqual("5.00", application_credit.amount)
+
+ def test_get_all_application_credits(self):
+ self.fake("application_credits", method="GET", body=self.load_fixture("application_credits"), code=200)
+ application_credits = shopify.ApplicationCredit.find()
+ self.assertEqual(1, len(application_credits))
+ self.assertEqual(445365009, application_credits[0].id)
+
+ def test_create_application_credit(self):
+ self.fake(
+ "application_credits",
+ method="POST",
+ body=self.load_fixture("application_credit"),
+ headers={"Content-type": "application/json"},
+ code=201,
+ )
+
+ application_credit = shopify.ApplicationCredit.create(
+ {"description": "application credit for refund", "amount": 5.0}
+ )
+
+ expected_body = {"application_credit": {"description": "application credit for refund", "amount": 5.0}}
+
+ self.assertEqual(expected_body, json.loads(self.http.request.data.decode("utf-8")))
diff --git a/test/article_test.py b/test/article_test.py
index 58e29b02..5ee53bbe 100644
--- a/test/article_test.py
+++ b/test/article_test.py
@@ -1,65 +1,75 @@
import shopify
from test.test_helper import TestCase
-class ArticleTest(TestCase):
+class ArticleTest(TestCase):
def test_create_article(self):
- self.fake("blogs/1008414260/articles", method='POST', body=self.load_fixture('article'), headers={'Content-type': 'application/json'})
- article = shopify.Article({'blog_id':1008414260})
+ self.fake(
+ "blogs/1008414260/articles",
+ method="POST",
+ body=self.load_fixture("article"),
+ headers={"Content-type": "application/json"},
+ )
+ article = shopify.Article({"blog_id": 1008414260})
article.save()
self.assertEqual("First Post", article.title)
def test_get_article(self):
- self.fake('articles/6242736', method='GET', body=self.load_fixture('article'))
+ self.fake("articles/6242736", method="GET", body=self.load_fixture("article"))
article = shopify.Article.find(6242736)
self.assertEqual("First Post", article.title)
def test_update_article(self):
- self.fake('articles/6242736', method='GET', body=self.load_fixture('article'))
+ self.fake("articles/6242736", method="GET", body=self.load_fixture("article"))
article = shopify.Article.find(6242736)
- self.fake('articles/6242736', method='PUT', body=self.load_fixture('article'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "articles/6242736",
+ method="PUT",
+ body=self.load_fixture("article"),
+ headers={"Content-type": "application/json"},
+ )
article.save()
def test_get_articles(self):
- self.fake("articles", method='GET', body=self.load_fixture('articles'))
+ self.fake("articles", method="GET", body=self.load_fixture("articles"))
articles = shopify.Article.find()
self.assertEqual(3, len(articles))
def test_get_articles_namespaced(self):
- self.fake("blogs/1008414260/articles", method='GET', body=self.load_fixture('articles'))
+ self.fake("blogs/1008414260/articles", method="GET", body=self.load_fixture("articles"))
articles = shopify.Article.find(blog_id=1008414260)
self.assertEqual(3, len(articles))
def test_get_article_namespaced(self):
- self.fake("blogs/1008414260/articles/6242736", method='GET', body=self.load_fixture('article'))
+ self.fake("blogs/1008414260/articles/6242736", method="GET", body=self.load_fixture("article"))
article = shopify.Article.find(6242736, blog_id=1008414260)
self.assertEqual("First Post", article.title)
def test_get_authors(self):
- self.fake("articles/authors", method='GET', body=self.load_fixture('authors'))
+ self.fake("articles/authors", method="GET", body=self.load_fixture("authors"))
authors = shopify.Article.authors()
self.assertEqual("Shopify", authors[0])
self.assertEqual("development shop", authors[-1])
def test_get_authors_for_blog_id(self):
- self.fake("blogs/1008414260/articles/authors", method='GET', body=self.load_fixture('authors'))
+ self.fake("blogs/1008414260/articles/authors", method="GET", body=self.load_fixture("authors"))
authors = shopify.Article.authors(blog_id=1008414260)
self.assertEqual(3, len(authors))
def test_get_tags(self):
- self.fake("articles/tags", method='GET', body=self.load_fixture('tags'))
+ self.fake("articles/tags", method="GET", body=self.load_fixture("tags"))
tags = shopify.Article.tags()
self.assertEqual("consequuntur", tags[0])
self.assertEqual("repellendus", tags[-1])
def test_get_tags_for_blog_id(self):
- self.fake("blogs/1008414260/articles/tags", method='GET', body=self.load_fixture('tags'))
+ self.fake("blogs/1008414260/articles/tags", method="GET", body=self.load_fixture("tags"))
tags = shopify.Article.tags(blog_id=1008414260)
self.assertEqual("consequuntur", tags[0])
self.assertEqual("repellendus", tags[-1])
def test_get_popular_tags(self):
- self.fake("articles/tags.json?limit=1&popular=1", extension=False, method='GET', body=self.load_fixture('tags'))
+ self.fake("articles/tags.json?limit=1&popular=1", extension=False, method="GET", body=self.load_fixture("tags"))
tags = shopify.Article.tags(popular=1, limit=1)
self.assertEqual(3, len(tags))
diff --git a/test/asset_test.py b/test/asset_test.py
index c7c7b153..19606a4b 100644
--- a/test/asset_test.py
+++ b/test/asset_test.py
@@ -3,51 +3,88 @@
import shopify
from test.test_helper import TestCase
-class AssetTest(TestCase):
+class AssetTest(TestCase):
def test_get_assets(self):
- self.fake("assets", method='GET', body=self.load_fixture('assets'))
+ self.fake("assets", method="GET", body=self.load_fixture("assets"))
v = shopify.Asset.find()
def test_get_asset(self):
- self.fake("assets.json?asset%5Bkey%5D=templates%2Findex.liquid", extension=False, method='GET', body=self.load_fixture('asset'))
- v = shopify.Asset.find('templates/index.liquid')
+ self.fake(
+ "assets.json?asset%5Bkey%5D=templates%2Findex.liquid",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid")
def test_update_asset(self):
- self.fake("assets.json?asset%5Bkey%5D=templates%2Findex.liquid", extension=False, method='GET', body=self.load_fixture('asset'))
- v = shopify.Asset.find('templates/index.liquid')
+ self.fake(
+ "assets.json?asset%5Bkey%5D=templates%2Findex.liquid",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid")
- self.fake("assets", method='PUT', body=self.load_fixture('asset'), headers={'Content-type': 'application/json'})
+ self.fake("assets", method="PUT", body=self.load_fixture("asset"), headers={"Content-type": "application/json"})
v.save()
def test_get_assets_namespaced(self):
- self.fake("themes/1/assets", method='GET', body=self.load_fixture('assets'))
- v = shopify.Asset.find(theme_id = 1)
+ self.fake("themes/1/assets", method="GET", body=self.load_fixture("assets"))
+ v = shopify.Asset.find(theme_id=1)
def test_get_asset_namespaced(self):
- self.fake("themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1", extension=False, method='GET', body=self.load_fixture('asset'))
- v = shopify.Asset.find('templates/index.liquid', theme_id=1)
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
def test_update_asset_namespaced(self):
- self.fake("themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1", extension=False, method='GET', body=self.load_fixture('asset'))
- v = shopify.Asset.find('templates/index.liquid', theme_id=1)
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
- self.fake("themes/1/assets", method='PUT', body=self.load_fixture('asset'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "themes/1/assets",
+ method="PUT",
+ body=self.load_fixture("asset"),
+ headers={"Content-type": "application/json"},
+ )
v.save()
def test_delete_asset_namespaced(self):
- self.fake("themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1", extension=False, method='GET', body=self.load_fixture('asset'))
- v = shopify.Asset.find('templates/index.liquid', theme_id=1)
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid&theme_id=1",
+ extension=False,
+ method="GET",
+ body=self.load_fixture("asset"),
+ )
+ v = shopify.Asset.find("templates/index.liquid", theme_id=1)
- self.fake("themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid", extension=False, method='DELETE', body="{}")
+ self.fake(
+ "themes/1/assets.json?asset%5Bkey%5D=templates%2Findex.liquid", extension=False, method="DELETE", body="{}"
+ )
v.destroy()
def test_attach(self):
- self.fake("themes/1/assets", method='PUT', body=self.load_fixture('asset'), headers={'Content-type': 'application/json'})
- attachment = b'dGVzdCBiaW5hcnkgZGF0YTogAAE='
- key = 'assets/test.jpeg'
+ self.fake(
+ "themes/1/assets",
+ method="PUT",
+ body=self.load_fixture("asset"),
+ headers={"Content-type": "application/json"},
+ )
+ attachment = b"dGVzdCBiaW5hcnkgZGF0YTogAAE="
+ key = "assets/test.jpeg"
theme_id = 1
- asset = shopify.Asset({'key': key, 'theme_id': theme_id})
+ asset = shopify.Asset({"key": key, "theme_id": theme_id})
asset.attach(attachment)
asset.save()
- self.assertEqual(base64.b64encode(attachment).decode(), asset.attributes['attachment'])
+ self.assertEqual(base64.b64encode(attachment).decode(), asset.attributes["attachment"])
diff --git a/test/balance_test.py b/test/balance_test.py
index 71636716..2cb26b08 100644
--- a/test/balance_test.py
+++ b/test/balance_test.py
@@ -3,9 +3,9 @@
class BalanceTest(TestCase):
- prefix = '/admin/api/unstable/shopify_payments'
+ prefix = "/admin/api/unstable/shopify_payments"
def test_get_balance(self):
- self.fake('balance', method='GET', prefix=self.prefix, body=self.load_fixture('balance'))
+ self.fake("balance", method="GET", prefix=self.prefix, body=self.load_fixture("balance"))
balance = shopify.Balance.find()
self.assertGreater(len(balance), 0)
diff --git a/test/base_test.py b/test/base_test.py
index 88478570..5cc19a60 100644
--- a/test/base_test.py
+++ b/test/base_test.py
@@ -4,22 +4,19 @@
from mock import patch
import threading
-class BaseTest(TestCase):
+class BaseTest(TestCase):
@classmethod
def setUpClass(self):
shopify.ApiVersion.define_known_versions()
- shopify.ApiVersion.define_version(shopify.Release('2019-04'))
- self.session1 = shopify.Session('shop1.myshopify.com', 'unstable', 'token1')
- self.session2 = shopify.Session('shop2.myshopify.com', '2019-04', 'token2')
+ shopify.ApiVersion.define_version(shopify.Release("2019-04"))
+ self.session1 = shopify.Session("shop1.myshopify.com", "unstable", "token1")
+ self.session2 = shopify.Session("shop2.myshopify.com", "2019-04", "token2")
@classmethod
def tearDownClass(self):
shopify.ApiVersion.clear_defined_versions()
- def setUp(self):
- super(BaseTest, self).setUp()
-
def tearDown(self):
shopify.ShopifyResource.clear_session()
@@ -27,21 +24,21 @@ def test_activate_session_should_set_site_and_headers_for_given_session(self):
shopify.ShopifyResource.activate_session(self.session1)
self.assertIsNone(ActiveResource.site)
- self.assertEqual('https://shop1.myshopify.com/admin/api/unstable', shopify.ShopifyResource.site)
- self.assertEqual('https://shop1.myshopify.com/admin/api/unstable', shopify.Shop.site)
+ self.assertEqual("https://shop1.myshopify.com/admin/api/unstable", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop1.myshopify.com/admin/api/unstable", shopify.Shop.site)
self.assertIsNone(ActiveResource.headers)
- self.assertEqual('token1', shopify.ShopifyResource.headers['X-Shopify-Access-Token'])
- self.assertEqual('token1', shopify.Shop.headers['X-Shopify-Access-Token'])
+ self.assertEqual("token1", shopify.ShopifyResource.headers["X-Shopify-Access-Token"])
+ self.assertEqual("token1", shopify.Shop.headers["X-Shopify-Access-Token"])
def test_activate_session_should_set_site_given_version(self):
shopify.ShopifyResource.activate_session(self.session2)
self.assertIsNone(ActiveResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin/api/2019-04', shopify.ShopifyResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin/api/2019-04', shopify.Shop.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.Shop.site)
self.assertIsNone(ActiveResource.headers)
- def test_clear_session_should_clear_site_and_headers_from_Base(self):
+ def test_clear_session_should_clear_site_and_headers_from_base(self):
shopify.ShopifyResource.activate_session(self.session1)
shopify.ShopifyResource.clear_session()
@@ -50,66 +47,68 @@ def test_clear_session_should_clear_site_and_headers_from_Base(self):
self.assertIsNone(shopify.Shop.site)
self.assertIsNone(ActiveResource.headers)
- self.assertFalse('X-Shopify-Access-Token' in shopify.ShopifyResource.headers)
- self.assertFalse('X-Shopify-Access-Token' in shopify.Shop.headers)
+ self.assertFalse("X-Shopify-Access-Token" in shopify.ShopifyResource.headers)
+ self.assertFalse("X-Shopify-Access-Token" in shopify.Shop.headers)
- def test_activate_session_with_one_session_then_clearing_and_activating_with_another_session_shoul_request_to_correct_shop(self):
+ def test_activate_session_with_one_session_then_clearing_and_activating_with_another_session_shoul_request_to_correct_shop(
+ self,
+ ):
shopify.ShopifyResource.activate_session(self.session1)
shopify.ShopifyResource.clear_session()
shopify.ShopifyResource.activate_session(self.session2)
self.assertIsNone(ActiveResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin/api/2019-04', shopify.ShopifyResource.site)
- self.assertEqual('https://shop2.myshopify.com/admin/api/2019-04', shopify.Shop.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
+ self.assertEqual("https://shop2.myshopify.com/admin/api/2019-04", shopify.Shop.site)
self.assertIsNone(ActiveResource.headers)
- self.assertEqual('token2', shopify.ShopifyResource.headers['X-Shopify-Access-Token'])
- self.assertEqual('token2', shopify.Shop.headers['X-Shopify-Access-Token'])
+ self.assertEqual("token2", shopify.ShopifyResource.headers["X-Shopify-Access-Token"])
+ self.assertEqual("token2", shopify.Shop.headers["X-Shopify-Access-Token"])
def test_delete_should_send_custom_headers_with_request(self):
shopify.ShopifyResource.activate_session(self.session1)
- org_headers=shopify.ShopifyResource.headers
- shopify.ShopifyResource.set_headers({'X-Custom': 'abc'})
+ org_headers = shopify.ShopifyResource.headers
+ shopify.ShopifyResource.set_headers({"X-Custom": "abc"})
- with patch('shopify.ShopifyResource.connection.delete') as mock:
- url = shopify.ShopifyResource._custom_method_collection_url('1', {})
- shopify.ShopifyResource.delete('1')
- mock.assert_called_with(url, {'X-Custom': 'abc'})
+ with patch("shopify.ShopifyResource.connection.delete") as mock:
+ url = shopify.ShopifyResource._custom_method_collection_url("1", {})
+ shopify.ShopifyResource.delete("1")
+ mock.assert_called_with(url, {"X-Custom": "abc"})
shopify.ShopifyResource.set_headers(org_headers)
def test_headers_includes_user_agent(self):
- self.assertTrue('User-Agent' in shopify.ShopifyResource.headers)
- t = threading.Thread(target=lambda: self.assertTrue('User-Agent' in shopify.ShopifyResource.headers))
+ self.assertTrue("User-Agent" in shopify.ShopifyResource.headers)
+ t = threading.Thread(target=lambda: self.assertTrue("User-Agent" in shopify.ShopifyResource.headers))
t.start()
t.join()
def test_headers_is_thread_safe(self):
def testFunc():
- shopify.ShopifyResource.headers['X-Custom'] = 'abc'
- self.assertTrue('X-Custom' in shopify.ShopifyResource.headers)
+ shopify.ShopifyResource.headers["X-Custom"] = "abc"
+ self.assertTrue("X-Custom" in shopify.ShopifyResource.headers)
t1 = threading.Thread(target=testFunc)
t1.start()
t1.join()
- t2 = threading.Thread(target=lambda: self.assertFalse('X-Custom' in shopify.ShopifyResource.headers))
+ t2 = threading.Thread(target=lambda: self.assertFalse("X-Custom" in shopify.ShopifyResource.headers))
t2.start()
t2.join()
def test_setting_with_user_and_pass_strips_them(self):
shopify.ShopifyResource.clear_session()
self.fake(
- 'shop',
- url='https://this-is-my-test-show.myshopify.com/admin/shop.json',
- method='GET',
- body=self.load_fixture('shop'),
- headers={'Authorization': u'Basic dXNlcjpwYXNz'}
+ "shop",
+ url="https://this-is-my-test-show.myshopify.com/admin/shop.json",
+ method="GET",
+ body=self.load_fixture("shop"),
+ headers={"Authorization": "Basic dXNlcjpwYXNz"},
)
- API_KEY = 'user'
- PASSWORD = 'pass'
+ API_KEY = "user"
+ PASSWORD = "pass"
shop_url = "https://%s:%s@this-is-my-test-show.myshopify.com/admin" % (API_KEY, PASSWORD)
shopify.ShopifyResource.set_site(shop_url)
res = shopify.Shop.current()
- self.assertEqual('Apple Computers', res.name)
+ self.assertEqual("Apple Computers", res.name)
diff --git a/test/blog_test.py b/test/blog_test.py
index 728f2e18..e3a912f0 100644
--- a/test/blog_test.py
+++ b/test/blog_test.py
@@ -1,9 +1,15 @@
import shopify
from test.test_helper import TestCase
+
class BlogTest(TestCase):
-
def test_blog_creation(self):
- self.fake('blogs', method='POST', code=202, body=self.load_fixture('blog'), headers={'Content-type': 'application/json'})
- blog = shopify.Blog.create({'title': "Test Blog"})
+ self.fake(
+ "blogs",
+ method="POST",
+ code=202,
+ body=self.load_fixture("blog"),
+ headers={"Content-type": "application/json"},
+ )
+ blog = shopify.Blog.create({"title": "Test Blog"})
self.assertEqual("Test Blog", blog.title)
diff --git a/test/carrier_service_test.py b/test/carrier_service_test.py
index fbd96180..a5aea4f6 100644
--- a/test/carrier_service_test.py
+++ b/test/carrier_service_test.py
@@ -1,15 +1,21 @@
import shopify
from test.test_helper import TestCase
+
class CarrierServiceTest(TestCase):
def test_create_new_carrier_service(self):
- self.fake("carrier_services", method='POST', body=self.load_fixture('carrier_service'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "carrier_services",
+ method="POST",
+ body=self.load_fixture("carrier_service"),
+ headers={"Content-type": "application/json"},
+ )
- carrier_service = shopify.CarrierService.create({'name': "Some Postal Service"})
+ carrier_service = shopify.CarrierService.create({"name": "Some Postal Service"})
self.assertEqual("Some Postal Service", carrier_service.name)
def test_get_carrier_service(self):
- self.fake("carrier_services/123456", method='GET', body=self.load_fixture('carrier_service'))
+ self.fake("carrier_services/123456", method="GET", body=self.load_fixture("carrier_service"))
carrier_service = shopify.CarrierService.find(123456)
self.assertEqual("Some Postal Service", carrier_service.name)
@@ -17,4 +23,4 @@ def test_get_carrier_service(self):
def test_set_format_attribute(self):
carrier_service = shopify.CarrierService()
carrier_service.format = "json"
- self.assertEqual("json", carrier_service.attributes['format'])
+ self.assertEqual("json", carrier_service.attributes["format"])
diff --git a/test/cart_test.py b/test/cart_test.py
index 3391dc9a..67836330 100644
--- a/test/cart_test.py
+++ b/test/cart_test.py
@@ -1,13 +1,13 @@
import shopify
from test.test_helper import TestCase
+
class CartTest(TestCase):
-
- def test_all_should_return_all_carts(self):
- self.fake('carts')
- carts = shopify.Cart.find()
- self.assertEqual(2, len(carts))
- self.assertEqual(2, carts[0].id)
- self.assertEqual("3eed8183d4281db6ea82ee2b8f23e9cc", carts[0].token)
- self.assertEqual(1, len(carts[0].line_items))
- self.assertEqual('test', carts[0].line_items[0].title)
+ def test_all_should_return_all_carts(self):
+ self.fake("carts")
+ carts = shopify.Cart.find()
+ self.assertEqual(2, len(carts))
+ self.assertEqual(2, carts[0].id)
+ self.assertEqual("3eed8183d4281db6ea82ee2b8f23e9cc", carts[0].token)
+ self.assertEqual(1, len(carts[0].line_items))
+ self.assertEqual("test", carts[0].line_items[0].title)
diff --git a/test/checkout_test.py b/test/checkout_test.py
index 508a3d9f..5b21d560 100644
--- a/test/checkout_test.py
+++ b/test/checkout_test.py
@@ -1,12 +1,12 @@
import shopify
from test.test_helper import TestCase
+
class CheckoutTest(TestCase):
-
- def test_all_should_return_all_checkouts(self):
- self.fake('checkouts')
- checkouts = shopify.Checkout.find()
- self.assertEqual(1, len(checkouts))
- self.assertEqual(450789469, checkouts[0].id)
- self.assertEqual("2a1ace52255252df566af0faaedfbfa7", checkouts[0].token)
- self.assertEqual(2, len(checkouts[0].line_items))
+ def test_all_should_return_all_checkouts(self):
+ self.fake("checkouts")
+ checkouts = shopify.Checkout.find()
+ self.assertEqual(1, len(checkouts))
+ self.assertEqual(450789469, checkouts[0].id)
+ self.assertEqual("2a1ace52255252df566af0faaedfbfa7", checkouts[0].token)
+ self.assertEqual(2, len(checkouts[0].line_items))
diff --git a/test/collection_listing_test.py b/test/collection_listing_test.py
index e2cc897a..4bea7fac 100644
--- a/test/collection_listing_test.py
+++ b/test/collection_listing_test.py
@@ -1,10 +1,10 @@
import shopify
from test.test_helper import TestCase
-class CollectionListingTest(TestCase):
+class CollectionListingTest(TestCase):
def test_get_collection_listings(self):
- self.fake('collection_listings', method='GET', code=200, body=self.load_fixture('collection_listings'))
+ self.fake("collection_listings", method="GET", code=200, body=self.load_fixture("collection_listings"))
collection_listings = shopify.CollectionListing.find()
self.assertEqual(1, len(collection_listings))
@@ -12,7 +12,7 @@ def test_get_collection_listings(self):
self.assertEqual("Home page", collection_listings[0].title)
def test_get_collection_listing(self):
- self.fake('collection_listings/1', method='GET', code=200, body=self.load_fixture('collection_listing'))
+ self.fake("collection_listings/1", method="GET", code=200, body=self.load_fixture("collection_listing"))
collection_listing = shopify.CollectionListing.find(1)
@@ -20,7 +20,7 @@ def test_get_collection_listing(self):
self.assertEqual("Home page", collection_listing.title)
def test_reload_collection_listing(self):
- self.fake('collection_listings/1', method='GET', code=200, body=self.load_fixture('collection_listing'))
+ self.fake("collection_listings/1", method="GET", code=200, body=self.load_fixture("collection_listing"))
collection_listing = shopify.CollectionListing()
collection_listing.collection_id = 1
@@ -30,7 +30,12 @@ def test_reload_collection_listing(self):
self.assertEqual("Home page", collection_listing.title)
def test_get_collection_listing_product_ids(self):
- self.fake('collection_listings/1/product_ids', method='GET', code=200, body=self.load_fixture('collection_listing_product_ids'))
+ self.fake(
+ "collection_listings/1/product_ids",
+ method="GET",
+ code=200,
+ body=self.load_fixture("collection_listing_product_ids"),
+ )
collection_listing = shopify.CollectionListing()
collection_listing.id = 1
diff --git a/test/collection_publication_test.py b/test/collection_publication_test.py
index b0329791..4bd6dc7d 100644
--- a/test/collection_publication_test.py
+++ b/test/collection_publication_test.py
@@ -2,12 +2,13 @@
import json
from test.test_helper import TestCase
+
class CollectionPublicationTest(TestCase):
def test_find_all_collection_publications(self):
self.fake(
- 'publications/55650051/collection_publications',
- method='GET',
- body= self.load_fixture('collection_publications')
+ "publications/55650051/collection_publications",
+ method="GET",
+ body=self.load_fixture("collection_publications"),
)
collection_publications = shopify.CollectionPublication.find(publication_id=55650051)
@@ -16,10 +17,10 @@ def test_find_all_collection_publications(self):
def test_find_collection_publication(self):
self.fake(
- 'publications/55650051/collection_publications/96062799894',
- method='GET',
- body=self.load_fixture('collection_publication'),
- code=200
+ "publications/55650051/collection_publications/96062799894",
+ method="GET",
+ body=self.load_fixture("collection_publication"),
+ code=200,
)
collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051)
@@ -28,25 +29,27 @@ def test_find_collection_publication(self):
def test_create_collection_publication(self):
self.fake(
- 'publications/55650051/collection_publications',
- method='POST',
- headers={'Content-type': 'application/json'},
- body=self.load_fixture('collection_publication'),
- code=201
+ "publications/55650051/collection_publications",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=self.load_fixture("collection_publication"),
+ code=201,
)
- collection_publication = shopify.CollectionPublication.create({
- 'publication_id': 55650051,
- 'published_at': "2018-01-29T14:06:08-05:00",
- 'published': True,
- 'collection_id': 60941828118
- })
+ collection_publication = shopify.CollectionPublication.create(
+ {
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "collection_id": 60941828118,
+ }
+ )
expected_body = {
- 'collection_publication': {
- 'published_at': "2018-01-29T14:06:08-05:00",
- 'published': True,
- 'collection_id': 60941828118,
+ "collection_publication": {
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "collection_id": 60941828118,
}
}
@@ -54,19 +57,14 @@ def test_create_collection_publication(self):
def test_destroy_collection_publication(self):
self.fake(
- 'publications/55650051/collection_publications/96062799894',
- method='GET',
- body=self.load_fixture('collection_publication'),
- code=200
+ "publications/55650051/collection_publications/96062799894",
+ method="GET",
+ body=self.load_fixture("collection_publication"),
+ code=200,
)
collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051)
- self.fake(
- 'publications/55650051/collection_publications/96062799894',
- method='DELETE',
- body='{}',
- code=200
- )
+ self.fake("publications/55650051/collection_publications/96062799894", method="DELETE", body="{}", code=200)
collection_publication.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/currency_test.py b/test/currency_test.py
index 24782f79..2d3b47c9 100644
--- a/test/currency_test.py
+++ b/test/currency_test.py
@@ -1,10 +1,10 @@
import shopify
from test.test_helper import TestCase
-class CurrencyTest(TestCase):
+class CurrencyTest(TestCase):
def test_get_currencies(self):
- self.fake('currencies', method='GET', code=200, body=self.load_fixture('currencies'))
+ self.fake("currencies", method="GET", code=200, body=self.load_fixture("currencies"))
currencies = shopify.Currency.find()
self.assertEqual(4, len(currencies))
@@ -20,4 +20,3 @@ def test_get_currencies(self):
self.assertEqual("HKD", currencies[3].currency)
self.assertEqual("2018-10-03T14:44:08-04:00", currencies[3].rate_updated_at)
self.assertEqual(False, currencies[3].enabled)
-
diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py
index 5c775af7..48873f15 100644
--- a/test/customer_saved_search_test.py
+++ b/test/customer_saved_search_test.py
@@ -1,23 +1,29 @@
import shopify
from test.test_helper import TestCase
+
class CustomerSavedSearchTest(TestCase):
-
def setUp(self):
super(CustomerSavedSearchTest, self).setUp()
self.load_customer_saved_search()
def test_get_customers_from_customer_saved_search(self):
- self.fake('customer_saved_searches/8899730/customers', body=self.load_fixture('customer_saved_search_customers'))
+ self.fake(
+ "customer_saved_searches/8899730/customers", body=self.load_fixture("customer_saved_search_customers")
+ )
self.assertEqual(1, len(self.customer_saved_search.customers()))
self.assertEqual(112223902, self.customer_saved_search.customers()[0].id)
def test_get_customers_from_customer_saved_search_with_params(self):
- self.fake('customer_saved_searches/8899730/customers.json?limit=1', extension=False, body=self.load_fixture('customer_saved_search_customers'))
- customers = self.customer_saved_search.customers(limit = 1)
+ self.fake(
+ "customer_saved_searches/8899730/customers.json?limit=1",
+ extension=False,
+ body=self.load_fixture("customer_saved_search_customers"),
+ )
+ customers = self.customer_saved_search.customers(limit=1)
self.assertEqual(1, len(customers))
self.assertEqual(112223902, customers[0].id)
def load_customer_saved_search(self):
- self.fake('customer_saved_searches/8899730', body=self.load_fixture('customer_saved_search'))
+ self.fake("customer_saved_searches/8899730", body=self.load_fixture("customer_saved_search"))
self.customer_saved_search = shopify.CustomerSavedSearch.find(8899730)
diff --git a/test/customer_test.py b/test/customer_test.py
index ef237b84..04f2a0c8 100644
--- a/test/customer_test.py
+++ b/test/customer_test.py
@@ -2,49 +2,75 @@
import json
from test.test_helper import TestCase
+
class CustomerTest(TestCase):
def setUp(self):
super(CustomerTest, self).setUp()
- self.fake('customers/207119551', method='GET', body=self.load_fixture('customer'))
+ self.fake("customers/207119551", method="GET", body=self.load_fixture("customer"))
self.customer = shopify.Customer.find(207119551)
def test_create_customer(self):
- self.fake("customers", method='POST', body=self.load_fixture('customer'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "customers", method="POST", body=self.load_fixture("customer"), headers={"Content-type": "application/json"}
+ )
customer = shopify.Customer()
- customer.first_name = 'Bob'
- customer.last_name = 'Lastnameson'
- customer.email = 'steve.lastnameson@example.com'
+ customer.first_name = "Bob"
+ customer.last_name = "Lastnameson"
+ customer.email = "steve.lastnameson@example.com"
customer.verified_email = True
customer.password = "newpass"
customer.password_confirmation = "newpass"
- self.assertEqual("newpass", customer.attributes['password'])
+ self.assertEqual("newpass", customer.attributes["password"])
customer.save()
self.assertEqual("Bob", customer.first_name)
- self.assertEqual("newpass", customer.attributes['password'])
+ self.assertEqual("newpass", customer.attributes["password"])
def test_get_customer(self):
self.assertEqual("Bob", self.customer.first_name)
def test_search(self):
- self.fake("customers/search.json?query=Bob+country%3AUnited+States", extension=False, body=self.load_fixture('customers_search'))
+ self.fake(
+ "customers/search.json?query=Bob+country%3AUnited+States",
+ extension=False,
+ body=self.load_fixture("customers_search"),
+ )
- results = shopify.Customer.search(query='Bob country:United States')
- self.assertEqual('Bob', results[0].first_name)
+ results = shopify.Customer.search(query="Bob country:United States")
+ self.assertEqual("Bob", results[0].first_name)
def test_send_invite_with_no_params(self):
- customer_invite_fixture = self.load_fixture('customer_invite')
+ customer_invite_fixture = self.load_fixture("customer_invite")
customer_invite = json.loads(customer_invite_fixture.decode("utf-8"))
- self.fake('customers/207119551/send_invite', method='POST', body=customer_invite_fixture, headers={'Content-type': 'application/json'})
+ self.fake(
+ "customers/207119551/send_invite",
+ method="POST",
+ body=customer_invite_fixture,
+ headers={"Content-type": "application/json"},
+ )
customer_invite_response = self.customer.send_invite()
self.assertEqual(json.loads('{"customer_invite": {}}'), json.loads(self.http.request.data.decode("utf-8")))
self.assertIsInstance(customer_invite_response, shopify.CustomerInvite)
- self.assertEqual(customer_invite['customer_invite']['to'], customer_invite_response.to)
+ self.assertEqual(customer_invite["customer_invite"]["to"], customer_invite_response.to)
def test_send_invite_with_params(self):
- customer_invite_fixture = self.load_fixture('customer_invite')
+ customer_invite_fixture = self.load_fixture("customer_invite")
customer_invite = json.loads(customer_invite_fixture.decode("utf-8"))
- self.fake('customers/207119551/send_invite', method='POST', body=customer_invite_fixture, headers={'Content-type': 'application/json'})
- customer_invite_response = self.customer.send_invite(shopify.CustomerInvite(customer_invite['customer_invite']))
+ self.fake(
+ "customers/207119551/send_invite",
+ method="POST",
+ body=customer_invite_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ customer_invite_response = self.customer.send_invite(shopify.CustomerInvite(customer_invite["customer_invite"]))
self.assertEqual(customer_invite, json.loads(self.http.request.data.decode("utf-8")))
self.assertIsInstance(customer_invite_response, shopify.CustomerInvite)
- self.assertEqual(customer_invite['customer_invite']['to'], customer_invite_response.to)
+ self.assertEqual(customer_invite["customer_invite"]["to"], customer_invite_response.to)
+
+ def test_get_customer_orders(self):
+ self.fake("customers/207119551", method="GET", body=self.load_fixture("customer"))
+ customer = shopify.Customer.find(207119551)
+ self.fake("customers/207119551/orders", method="GET", body=self.load_fixture("orders"))
+ orders = customer.orders()
+ self.assertIsInstance(orders[0], shopify.Order)
+ self.assertEqual(450789469, orders[0].id)
+ self.assertEqual(207119551, orders[0].customer.id)
diff --git a/test/discount_code_creation_test.py b/test/discount_code_creation_test.py
index be814cc9..ce2e3156 100644
--- a/test/discount_code_creation_test.py
+++ b/test/discount_code_creation_test.py
@@ -1,16 +1,17 @@
from test.test_helper import TestCase
import shopify
+
class DiscountCodeCreationTest(TestCase):
def test_find_batch_job_discount_codes(self):
- self.fake('price_rules/1213131', body=self.load_fixture('price_rule'))
+ self.fake("price_rules/1213131", body=self.load_fixture("price_rule"))
price_rule = shopify.PriceRule.find(1213131)
- self.fake('price_rules/1213131/batch/989355119', body=self.load_fixture('discount_code_creation'))
+ self.fake("price_rules/1213131/batch/989355119", body=self.load_fixture("discount_code_creation"))
batch = price_rule.find_batch(989355119)
- self.fake('price_rules/1213131/batch/989355119/discount_codes', body=self.load_fixture('batch_discount_codes'))
+ self.fake("price_rules/1213131/batch/989355119/discount_codes", body=self.load_fixture("batch_discount_codes"))
discount_codes = batch.discount_codes()
- self.assertEqual('foo', discount_codes[0].code)
- self.assertEqual('bar', discount_codes[2].code)
+ self.assertEqual("foo", discount_codes[0].code)
+ self.assertEqual("bar", discount_codes[2].code)
diff --git a/test/discount_code_test.py b/test/discount_code_test.py
index 92469928..44ffd55e 100644
--- a/test/discount_code_test.py
+++ b/test/discount_code_test.py
@@ -6,7 +6,7 @@
class DiscountCodeTest(TestCase):
def setUp(self):
super(DiscountCodeTest, self).setUp()
- self.fake("price_rules/1213131/discount_codes/34", method='GET', body=self.load_fixture('discount_code'))
+ self.fake("price_rules/1213131/discount_codes/34", method="GET", body=self.load_fixture("discount_code"))
self.discount_code = shopify.DiscountCode.find(34, price_rule_id=1213131)
def test_find_a_specific_discount_code(self):
@@ -14,20 +14,18 @@ def test_find_a_specific_discount_code(self):
self.assertEqual("25OFF", discount_code.code)
def test_update_a_specific_discount_code(self):
- self.discount_code.code = 'BOGO'
- self.fake('price_rules/1213131/discount_codes/34',
- method='PUT',
- code=200,
- body=self.load_fixture('discount_code'),
- headers={'Content-type': 'application/json'})
+ self.discount_code.code = "BOGO"
+ self.fake(
+ "price_rules/1213131/discount_codes/34",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("discount_code"),
+ headers={"Content-type": "application/json"},
+ )
self.discount_code.save()
- self.assertEqual('BOGO',
- json.loads(self.http.request.data.decode("utf-8"))["discount_code"]["code"]
- )
- def test_delete_a_specific_discount_code(self):
- self.fake('price_rules/1213131/discount_codes/34', method='DELETE', body='destroyed')
- self.discount_code.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
-
-
+ self.assertEqual("BOGO", json.loads(self.http.request.data.decode("utf-8"))["discount_code"]["code"])
+ def test_delete_a_specific_discount_code(self):
+ self.fake("price_rules/1213131/discount_codes/34", method="DELETE", body="destroyed")
+ self.discount_code.destroy()
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/disputes_test.py b/test/disputes_test.py
index 7484b304..71fd01d0 100644
--- a/test/disputes_test.py
+++ b/test/disputes_test.py
@@ -3,15 +3,14 @@
class DisputeTest(TestCase):
- prefix = '/admin/api/unstable/shopify_payments'
+ prefix = "/admin/api/unstable/shopify_payments"
def test_get_dispute(self):
- self.fake('disputes', method='GET', prefix=self.prefix, body=self.load_fixture('disputes'))
+ self.fake("disputes", method="GET", prefix=self.prefix, body=self.load_fixture("disputes"))
disputes = shopify.Disputes.find()
self.assertGreater(len(disputes), 0)
def test_get_one_dispute(self):
- self.fake('disputes/1052608616', method='GET',
- prefix=self.prefix, body=self.load_fixture('dispute'))
+ self.fake("disputes/1052608616", method="GET", prefix=self.prefix, body=self.load_fixture("dispute"))
disputes = shopify.Disputes.find(1052608616)
- self.assertEqual('won', disputes.status)
+ self.assertEqual("won", disputes.status)
diff --git a/test/draft_order_test.py b/test/draft_order_test.py
index ef155ac2..12a359bd 100644
--- a/test/draft_order_test.py
+++ b/test/draft_order_test.py
@@ -6,97 +6,145 @@
class DraftOrderTest(TestCase):
def setUp(self):
super(DraftOrderTest, self).setUp()
- self.fake('draft_orders/517119332', body=self.load_fixture('draft_order'))
+ self.fake("draft_orders/517119332", body=self.load_fixture("draft_order"))
self.draft_order = shopify.DraftOrder.find(517119332)
def test_get_draft_order(self):
- self.fake('draft_orders/517119332', method='GET', code=200, body=self.load_fixture('draft_order'))
+ self.fake("draft_orders/517119332", method="GET", code=200, body=self.load_fixture("draft_order"))
draft_order = shopify.DraftOrder.find(517119332)
self.assertEqual(517119332, draft_order.id)
def test_get_all_draft_orders(self):
- self.fake('draft_orders', method='GET', code=200, body=self.load_fixture('draft_orders'))
+ self.fake("draft_orders", method="GET", code=200, body=self.load_fixture("draft_orders"))
draft_orders = shopify.DraftOrder.find()
self.assertEqual(1, len(draft_orders))
self.assertEqual(517119332, draft_orders[0].id)
def test_get_count_draft_orders(self):
- self.fake('draft_orders/count', method='GET', code=200, body='{"count": 16}')
+ self.fake("draft_orders/count", method="GET", code=200, body='{"count": 16}')
draft_orders_count = shopify.DraftOrder.count()
self.assertEqual(16, draft_orders_count)
def test_create_draft_order(self):
- self.fake('draft_orders', method='POST', code=201, body=self.load_fixture('draft_order'), headers={'Content-type': 'application/json'})
- draft_order = shopify.DraftOrder.create({"line_items": [{ "quantity": 1, "variant_id": 39072856 }]})
- self.assertEqual(json.loads('{"draft_order": {"line_items": [{"quantity": 1, "variant_id": 39072856}]}}'), json.loads(self.http.request.data.decode("utf-8")))
+ self.fake(
+ "draft_orders",
+ method="POST",
+ code=201,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
+ draft_order = shopify.DraftOrder.create({"line_items": [{"quantity": 1, "variant_id": 39072856}]})
+ self.assertEqual(
+ json.loads('{"draft_order": {"line_items": [{"quantity": 1, "variant_id": 39072856}]}}'),
+ json.loads(self.http.request.data.decode("utf-8")),
+ )
def test_create_draft_order_202(self):
- self.fake('draft_orders', method='POST', code=202, body=self.load_fixture('draft_order'), headers={'Content-type': 'application/json'})
- draft_order = shopify.DraftOrder.create({"line_items": [{ "quantity": 1, "variant_id": 39072856 }]})
+ self.fake(
+ "draft_orders",
+ method="POST",
+ code=202,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
+ draft_order = shopify.DraftOrder.create({"line_items": [{"quantity": 1, "variant_id": 39072856}]})
self.assertEqual(39072856, draft_order.line_items[0].variant_id)
def test_update_draft_order(self):
- self.draft_order.note = 'Test new note'
- self.fake('draft_orders/517119332', method='PUT', code=200, body=self.load_fixture('draft_order'), headers={'Content-type': 'application/json'})
+ self.draft_order.note = "Test new note"
+ self.fake(
+ "draft_orders/517119332",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("draft_order"),
+ headers={"Content-type": "application/json"},
+ )
self.draft_order.save()
- self.assertEqual('Test new note', json.loads(self.http.request.data.decode("utf-8"))['draft_order']['note'])
+ self.assertEqual("Test new note", json.loads(self.http.request.data.decode("utf-8"))["draft_order"]["note"])
def test_send_invoice_with_no_params(self):
- draft_order_invoice_fixture = self.load_fixture('draft_order_invoice')
+ draft_order_invoice_fixture = self.load_fixture("draft_order_invoice")
draft_order_invoice = json.loads(draft_order_invoice_fixture.decode("utf-8"))
- self.fake('draft_orders/517119332/send_invoice', method='POST', body=draft_order_invoice_fixture, headers={'Content-type': 'application/json'})
+ self.fake(
+ "draft_orders/517119332/send_invoice",
+ method="POST",
+ body=draft_order_invoice_fixture,
+ headers={"Content-type": "application/json"},
+ )
draft_order_invoice_response = self.draft_order.send_invoice()
self.assertEqual(json.loads('{"draft_order_invoice": {}}'), json.loads(self.http.request.data.decode("utf-8")))
self.assertIsInstance(draft_order_invoice_response, shopify.DraftOrderInvoice)
- self.assertEqual(draft_order_invoice['draft_order_invoice']['to'], draft_order_invoice_response.to)
+ self.assertEqual(draft_order_invoice["draft_order_invoice"]["to"], draft_order_invoice_response.to)
def test_send_invoice_with_params(self):
- draft_order_invoice_fixture = self.load_fixture('draft_order_invoice')
+ draft_order_invoice_fixture = self.load_fixture("draft_order_invoice")
draft_order_invoice = json.loads(draft_order_invoice_fixture.decode("utf-8"))
- self.fake('draft_orders/517119332/send_invoice', method='POST', body=draft_order_invoice_fixture, headers={'Content-type': 'application/json'})
- draft_order_invoice_response = self.draft_order.send_invoice(shopify.DraftOrderInvoice(draft_order_invoice['draft_order_invoice']))
+ self.fake(
+ "draft_orders/517119332/send_invoice",
+ method="POST",
+ body=draft_order_invoice_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ draft_order_invoice_response = self.draft_order.send_invoice(
+ shopify.DraftOrderInvoice(draft_order_invoice["draft_order_invoice"])
+ )
self.assertEqual(draft_order_invoice, json.loads(self.http.request.data.decode("utf-8")))
self.assertIsInstance(draft_order_invoice_response, shopify.DraftOrderInvoice)
- self.assertEqual(draft_order_invoice['draft_order_invoice']['to'], draft_order_invoice_response.to)
+ self.assertEqual(draft_order_invoice["draft_order_invoice"]["to"], draft_order_invoice_response.to)
def test_delete_draft_order(self):
- self.fake('draft_orders/517119332', method='DELETE', body='destroyed')
+ self.fake("draft_orders/517119332", method="DELETE", body="destroyed")
self.draft_order.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
def test_add_metafields_to_draft_order(self):
- self.fake('draft_orders/517119332/metafields', method='POST', code=201, body=self.load_fixture('metafield'), headers={'Content-type': 'application/json'})
- field = self.draft_order.add_metafield(shopify.Metafield({'namespace': 'contact', 'key': 'email', 'value': '123@example.com', 'value_type': 'string'}))
- self.assertEqual(json.loads('{"metafield":{"namespace":"contact","key":"email","value":"123@example.com","value_type":"string"}}'), json.loads(self.http.request.data.decode("utf-8")))
- self.assertFalse (field.is_new())
- self.assertEqual('contact', field.namespace)
- self.assertEqual('email', field.key)
- self.assertEqual('123@example.com', field.value)
+ self.fake(
+ "draft_orders/517119332/metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+ field = self.draft_order.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
+ self.assertEqual(
+ json.loads(
+ '{"metafield":{"namespace":"contact","key":"email","value":"123@example.com","value_type":"string"}}'
+ ),
+ json.loads(self.http.request.data.decode("utf-8")),
+ )
+ self.assertFalse(field.is_new())
+ self.assertEqual("contact", field.namespace)
+ self.assertEqual("email", field.key)
+ self.assertEqual("123@example.com", field.value)
def test_get_metafields_for_draft_order(self):
- self.fake('draft_orders/517119332/metafields', body=self.load_fixture('metafields'))
+ self.fake("draft_orders/517119332/metafields", body=self.load_fixture("metafields"))
metafields = self.draft_order.metafields()
self.assertEqual(2, len(metafields))
self.assertIsInstance(metafields[0], shopify.Metafield)
self.assertIsInstance(metafields[1], shopify.Metafield)
def test_complete_draft_order_with_no_params(self):
- completed_fixture = self.load_fixture('draft_order_completed')
- completed_draft = json.loads(completed_fixture.decode("utf-8"))['draft_order']
- headers={'Content-type': 'application/json', 'Content-length': '0'}
- self.fake('draft_orders/517119332/complete', method='PUT', body=completed_fixture, headers=headers)
+ completed_fixture = self.load_fixture("draft_order_completed")
+ completed_draft = json.loads(completed_fixture.decode("utf-8"))["draft_order"]
+ headers = {"Content-type": "application/json", "Content-length": "0"}
+ self.fake("draft_orders/517119332/complete", method="PUT", body=completed_fixture, headers=headers)
self.draft_order.complete()
- self.assertEqual(completed_draft['status'], self.draft_order.status)
- self.assertEqual(completed_draft['order_id'], self.draft_order.order_id)
+ self.assertEqual(completed_draft["status"], self.draft_order.status)
+ self.assertEqual(completed_draft["order_id"], self.draft_order.order_id)
self.assertIsNotNone(self.draft_order.completed_at)
def test_complete_draft_order_with_params(self):
- completed_fixture = self.load_fixture('draft_order_completed')
- completed_draft = json.loads(completed_fixture.decode("utf-8"))['draft_order']
- headers = {'Content-type': 'application/json', 'Content-length': '0'}
- url = 'draft_orders/517119332/complete.json?payment_pending=true'
- self.fake(url, extension=False, method='PUT', body=completed_fixture, headers=headers)
- self.draft_order.complete({'payment_pending': True})
- self.assertEqual(completed_draft['status'], self.draft_order.status)
- self.assertEqual(completed_draft['order_id'], self.draft_order.order_id)
+ completed_fixture = self.load_fixture("draft_order_completed")
+ completed_draft = json.loads(completed_fixture.decode("utf-8"))["draft_order"]
+ headers = {"Content-type": "application/json", "Content-length": "0"}
+ url = "draft_orders/517119332/complete.json?payment_pending=true"
+ self.fake(url, extension=False, method="PUT", body=completed_fixture, headers=headers)
+ self.draft_order.complete({"payment_pending": True})
+ self.assertEqual(completed_draft["status"], self.draft_order.status)
+ self.assertEqual(completed_draft["order_id"], self.draft_order.order_id)
self.assertIsNotNone(self.draft_order.completed_at)
diff --git a/test/event_test.py b/test/event_test.py
index 67e80335..cbc28802 100644
--- a/test/event_test.py
+++ b/test/event_test.py
@@ -1,12 +1,12 @@
import shopify
from test.test_helper import TestCase
+
class EventTest(TestCase):
def test_prefix_uses_resource(self):
- prefix = shopify.Event._prefix(options={'resource': "orders", "resource_id": 42})
+ prefix = shopify.Event._prefix(options={"resource": "orders", "resource_id": 42})
self.assertEqual("https://this-is-my-test-show.myshopify.com/admin/api/unstable/orders/42", prefix)
def test_prefix_doesnt_need_resource(self):
prefix = shopify.Event._prefix()
self.assertEqual("https://this-is-my-test-show.myshopify.com/admin/api/unstable", prefix)
-
diff --git a/test/fixtures/access_scopes.json b/test/fixtures/access_scopes.json
index 4928cbcd..6f4fb0c2 100644
--- a/test/fixtures/access_scopes.json
+++ b/test/fixtures/access_scopes.json
@@ -10,4 +10,4 @@
"handle": "read_orders"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/api_version.json b/test/fixtures/api_version.json
deleted file mode 100644
index 43d1354d..00000000
--- a/test/fixtures/api_version.json
+++ /dev/null
@@ -1 +0,0 @@
-{"apis":[{"handle":"admin","versions":[{"handle":"2019-04","latest_supported":false,"display_name":"2019-04 (Unsupported)","supported":true},{"handle":"2019-07","latest_supported":false,"display_name":"2019-07 (Unsupported)","supported":true},{"handle":"2019-10","latest_supported":false,"display_name":"2019-10 (Unsupported)","supported":true},{"handle":"2020-01","latest_supported":false,"display_name":"2020-01","supported":true},{"handle":"2020-04","latest_supported":false,"display_name":"2020-04","supported":true},{"handle":"2020-07","latest_supported":false,"display_name":"2020-07","supported":true},{"handle":"2020-10","latest_supported":true,"display_name":"2020-10 (Latest)","supported":true},{"handle":"2021-01","latest_supported":false,"display_name":"2021-01 (Release candidate)","supported":false},{"handle":"unstable","latest_supported":false,"display_name":"unstable","supported":false}]},{"handle":"storefront","versions":[{"handle":"2019-07","latest_supported":false,"display_name":"2019-07 (Unsupported)","supported":true},{"handle":"2019-10","latest_supported":false,"display_name":"2019-10 (Unsupported)","supported":true},{"handle":"2020-01","latest_supported":false,"display_name":"2020-01","supported":true},{"handle":"2020-04","latest_supported":false,"display_name":"2020-04","supported":true},{"handle":"2020-07","latest_supported":false,"display_name":"2020-07","supported":true},{"handle":"2020-10","latest_supported":true,"display_name":"2020-10 (Latest)","supported":true},{"handle":"2021-01","latest_supported":false,"display_name":"2021-01 (Release candidate)","supported":false},{"handle":"unstable","latest_supported":false,"display_name":"unstable","supported":false}]}]}
\ No newline at end of file
diff --git a/test/fixtures/application_credit.json b/test/fixtures/application_credit.json
new file mode 100644
index 00000000..4d6c3de0
--- /dev/null
+++ b/test/fixtures/application_credit.json
@@ -0,0 +1,8 @@
+{
+ "application_credit": {
+ "id": 445365009,
+ "amount": "5.00",
+ "description": "credit for application refund",
+ "test": null
+ }
+}
diff --git a/test/fixtures/application_credits.json b/test/fixtures/application_credits.json
new file mode 100644
index 00000000..b487a523
--- /dev/null
+++ b/test/fixtures/application_credits.json
@@ -0,0 +1,10 @@
+{
+ "application_credits": [
+ {
+ "id": 445365009,
+ "amount": "5.00",
+ "description": "credit for application refund",
+ "test": null
+ }
+ ]
+}
diff --git a/test/fixtures/article.json b/test/fixtures/article.json
index 65ac8c6d..9910f0eb 100644
--- a/test/fixtures/article.json
+++ b/test/fixtures/article.json
@@ -12,4 +12,4 @@
"user_id": null,
"tags": "consequuntur, cupiditate, repellendus"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/articles.json b/test/fixtures/articles.json
index df83a9cb..5954b09f 100644
--- a/test/fixtures/articles.json
+++ b/test/fixtures/articles.json
@@ -36,4 +36,4 @@
"user_id": 2221540,
"tags": ""
}]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/balance.json b/test/fixtures/balance.json
index 8053cb99..851a2d76 100644
--- a/test/fixtures/balance.json
+++ b/test/fixtures/balance.json
@@ -5,4 +5,4 @@
"amount": "53.99"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/blog.json b/test/fixtures/blog.json
index df94412c..2c92f6d7 100644
--- a/test/fixtures/blog.json
+++ b/test/fixtures/blog.json
@@ -10,4 +10,4 @@
"feedburner": null,
"commentable": "no"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/blogs.json b/test/fixtures/blogs.json
index 3f779b25..0749df28 100644
--- a/test/fixtures/blogs.json
+++ b/test/fixtures/blogs.json
@@ -10,4 +10,4 @@
"feedburner": null,
"commentable": "no"
}]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/carts.json b/test/fixtures/carts.json
index 64d51246..4238684f 100644
--- a/test/fixtures/carts.json
+++ b/test/fixtures/carts.json
@@ -5,7 +5,7 @@
"note": null,
"token": "3eed8183d4281db6ea82ee2b8f23e9cc",
"updated_at": "2012-02-13T14:39:37-05:00",
- "line_items":
+ "line_items":
[
{
"id": 1,
@@ -40,4 +40,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/discount_code.json b/test/fixtures/discount_code.json
index e5e0d41f..4f3da0f1 100644
--- a/test/fixtures/discount_code.json
+++ b/test/fixtures/discount_code.json
@@ -6,4 +6,4 @@
"created_at": "2016-09-11T09:00:00-04:00",
"updated_at": "2016-09-11T09:30:00-04:00"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/discount_codes.json b/test/fixtures/discount_codes.json
index f002f5fe..453e384c 100644
--- a/test/fixtures/discount_codes.json
+++ b/test/fixtures/discount_codes.json
@@ -8,4 +8,4 @@
"updated_at": "2016-09-11T09:30:00-04:00"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/dispute.json b/test/fixtures/dispute.json
index 1723dd42..e88489a8 100644
--- a/test/fixtures/dispute.json
+++ b/test/fixtures/dispute.json
@@ -13,4 +13,4 @@
"finalized_on": null,
"initiated_at": "2013-05-03T20:00:00-04:00"
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/disputes.json b/test/fixtures/disputes.json
index 656d27ff..867c7519 100644
--- a/test/fixtures/disputes.json
+++ b/test/fixtures/disputes.json
@@ -99,4 +99,4 @@
"initiated_at": "2013-05-03T20:00:00-04:00"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/graphql.json b/test/fixtures/graphql.json
index 27310115..ee925166 100644
--- a/test/fixtures/graphql.json
+++ b/test/fixtures/graphql.json
@@ -24,4 +24,3 @@
"email": "steve@apple.com"
}
}
-
diff --git a/test/fixtures/orders.json b/test/fixtures/orders.json
new file mode 100644
index 00000000..925a4a5d
--- /dev/null
+++ b/test/fixtures/orders.json
@@ -0,0 +1,206 @@
+{
+ "orders": [
+ {
+ "id": 450789469,
+ "email": "bob.norman@hostmail.com",
+ "closed_at": null,
+ "created_at": "2008-01-10T11:00:00-05:00",
+ "updated_at": "2008-01-10T11:00:00-05:00",
+ "number": 1,
+ "note": null,
+ "token": "b1946ac92492d2347c6235b4d2611184",
+ "gateway": "authorize_net",
+ "test": false,
+ "total_price": "598.94",
+ "subtotal_price": "597.00",
+ "total_weight": 0,
+ "total_tax": "11.94",
+ "taxes_included": false,
+ "currency": "USD",
+ "financial_status": "partially_refunded",
+ "confirmed": true,
+ "total_discounts": "10.00",
+ "total_line_items_price": "597.00",
+ "cart_token": "68778783ad298f1c80c3bafcddeea02f",
+ "buyer_accepts_marketing": false,
+ "name": "#1001",
+ "referring_site": "http://www.otherexample.com",
+ "landing_site": "http://www.example.com?source=abc",
+ "cancelled_at": null,
+ "cancel_reason": null,
+ "total_price_usd": "598.94",
+ "checkout_token": "bd5a8aa1ecd019dd3520ff791ee3a24c",
+ "reference": "fhwdgads",
+ "user_id": null,
+ "location_id": null,
+ "source_identifier": "fhwdgads",
+ "source_url": null,
+ "processed_at": "2008-01-10T11:00:00-05:00",
+ "device_id": null,
+ "phone": "+557734881234",
+ "customer_locale": null,
+ "app_id": null,
+ "browser_ip": "0.0.0.0",
+ "client_details": {
+ "accept_language": null,
+ "browser_height": null,
+ "browser_ip": "0.0.0.0",
+ "browser_width": null,
+ "session_hash": null,
+ "user_agent": null
+ },
+ "landing_site_ref": "abc",
+ "order_number": 1001,
+ "payment_details": {
+ "credit_card_number": "โขโขโขโข โขโขโขโข โขโขโขโข 4242",
+ "credit_card_company": "Visa"
+ },
+ "payment_gateway_names": [
+ "bogus"
+ ],
+ "tags": "",
+ "contact_email": "bob.norman@hostmail.com",
+ "order_status_url": "https://apple.myshopify.com/690933842/orders/b1946ac92492d2347c6235b4d2611184/authenticate?key=ccde591a93123786bd8d257abd970200",
+ "presentment_currency": "USD",
+ "total_line_items_price_set": {
+ "shop_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_discounts_set": {
+ "shop_money": {
+ "amount": "10.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "10.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_shipping_price_set": {
+ "shop_money": {
+ "amount": "0.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "0.00",
+ "currency_code": "USD"
+ }
+ },
+ "subtotal_price_set": {
+ "shop_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "597.00",
+ "currency_code": "USD"
+ }
+ },
+ "total_price_set": {
+ "shop_money": {
+ "amount": "598.94",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "598.94",
+ "currency_code": "USD"
+ }
+ },
+ "total_tax_set": {
+ "shop_money": {
+ "amount": "11.94",
+ "currency_code": "USD"
+ },
+ "presentment_money": {
+ "amount": "11.94",
+ "currency_code": "USD"
+ }
+ },
+ "admin_graphql_api_id": "gid://shopify/Order/450789469",
+ "billing_address": {
+ "first_name": "Bob",
+ "address1": "Chestnut Street 92",
+ "phone": "555-625-1199",
+ "city": "Louisville",
+ "zip": "40202",
+ "province": "Kentucky",
+ "country": "United States",
+ "last_name": "Norman",
+ "address2": "",
+ "company": null,
+ "latitude": 45.41634,
+ "longitude": -75.6868,
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "shipping_address": {
+ "first_name": "Bob",
+ "address1": "Chestnut Street 92",
+ "phone": "555-625-1199",
+ "city": "Louisville",
+ "zip": "40202",
+ "province": "Kentucky",
+ "country": "United States",
+ "last_name": "Norman",
+ "address2": "",
+ "company": null,
+ "latitude": 45.41634,
+ "longitude": -75.6868,
+ "name": "Bob Norman",
+ "country_code": "US",
+ "province_code": "KY"
+ },
+ "customer": {
+ "id": 207119551,
+ "email": "bob.norman@hostmail.com",
+ "accepts_marketing": false,
+ "created_at": "2021-02-12T13:51:00-05:00",
+ "updated_at": "2021-02-12T13:51:00-05:00",
+ "first_name": "Bob",
+ "last_name": "Norman",
+ "orders_count": 1,
+ "state": "disabled",
+ "total_spent": "199.65",
+ "last_order_id": 450789469,
+ "note": null,
+ "verified_email": true,
+ "multipass_identifier": null,
+ "tax_exempt": false,
+ "phone": "+16136120707",
+ "tags": "",
+ "last_order_name": "#1001",
+ "currency": "USD",
+ "accepts_marketing_updated_at": "2005-06-12T11:57:11-04:00",
+ "marketing_opt_in_level": null,
+ "tax_exemptions": [],
+ "admin_graphql_api_id": "gid://shopify/Customer/207119551",
+ "default_address": {
+ "id": 207119551,
+ "customer_id": 207119551,
+ "first_name": null,
+ "last_name": null,
+ "company": null,
+ "address1": "Chestnut Street 92",
+ "address2": "",
+ "city": "Louisville",
+ "province": "Kentucky",
+ "country": "United States",
+ "zip": "40202",
+ "phone": "555-625-1199",
+ "name": "",
+ "province_code": "KY",
+ "country_code": "US",
+ "country_name": "United States",
+ "default": true
+ }
+ }
+ }
+ ]
+}
diff --git a/test/fixtures/payout.json b/test/fixtures/payout.json
index 4c12b1e2..e11a8f47 100644
--- a/test/fixtures/payout.json
+++ b/test/fixtures/payout.json
@@ -18,4 +18,4 @@
"retried_payouts_gross_amount": "0.00"
}
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/payouts.json b/test/fixtures/payouts.json
index b8e247b7..26ad9b3a 100644
--- a/test/fixtures/payouts.json
+++ b/test/fixtures/payouts.json
@@ -115,4 +115,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/payouts_transactions.json b/test/fixtures/payouts_transactions.json
index aa70797c..74220886 100644
--- a/test/fixtures/payouts_transactions.json
+++ b/test/fixtures/payouts_transactions.json
@@ -401,4 +401,4 @@
"processed_at": "2012-11-11T19:00:00-05:00"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/price_rule.json b/test/fixtures/price_rule.json
index cc176787..dfeb626e 100644
--- a/test/fixtures/price_rule.json
+++ b/test/fixtures/price_rule.json
@@ -15,4 +15,4 @@
"starts_at": "2017-05-30T04:13:56Z",
"ends_at": null
}
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/price_rules.json b/test/fixtures/price_rules.json
index 517e0e99..aef51c8e 100644
--- a/test/fixtures/price_rules.json
+++ b/test/fixtures/price_rules.json
@@ -15,7 +15,7 @@
"prerequisite_shipping_price_range": null,
"starts_at": "2017-05-30T04:13:56Z",
"ends_at": null
- },
+ },
{
"id": 1213132,
"title": "TENOFF",
@@ -33,4 +33,4 @@
"ends_at": null
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/recurring_application_charges_no_active.json b/test/fixtures/recurring_application_charges_no_active.json
index c806aa37..f3812604 100644
--- a/test/fixtures/recurring_application_charges_no_active.json
+++ b/test/fixtures/recurring_application_charges_no_active.json
@@ -35,4 +35,4 @@
"decorated_return_url": "http://yourapp.com?charge_id=455696195"
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/shipping_zones.json b/test/fixtures/shipping_zones.json
index f07b8ff2..877534a7 100644
--- a/test/fixtures/shipping_zones.json
+++ b/test/fixtures/shipping_zones.json
@@ -111,4 +111,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/test/fixtures/transaction.json b/test/fixtures/transaction.json
index 33a70e9d..61ae6f98 100644
--- a/test/fixtures/transaction.json
+++ b/test/fixtures/transaction.json
@@ -26,4 +26,4 @@
"credit_card_company": "Visa"
}
}
-}
\ No newline at end of file
+}
diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py
index 1ba9a73e..df92c3b6 100644
--- a/test/fulfillment_event_test.py
+++ b/test/fulfillment_event_test.py
@@ -1,24 +1,40 @@
import shopify
from test.test_helper import TestCase
-from pyactiveresource.activeresource import ActiveResource
+
class FulFillmentEventTest(TestCase):
def test_get_fulfillment_event(self):
- self.fake("orders/2776493818019/fulfillments/2608403447971/events", method='GET', body=self.load_fixture('fulfillment_event'))
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events",
+ method="GET",
+ body=self.load_fixture("fulfillment_event"),
+ )
fulfillment_event = shopify.FulfillmentEvent.find(order_id=2776493818019, fulfillment_id=2608403447971)
self.assertEqual(1, len(fulfillment_event))
def test_create_fulfillment_event(self):
- self.fake("orders/2776493818019/fulfillments/2608403447971/events", method='POST', body=self.load_fixture('fulfillment_event'), headers={'Content-type': 'application/json'})
- new_fulfillment_event = shopify.FulfillmentEvent({'order_id': '2776493818019', 'fulfillment_id': '2608403447971'})
- new_fulfillment_event.status = 'ready_for_pickup'
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events",
+ method="POST",
+ body=self.load_fixture("fulfillment_event"),
+ headers={"Content-type": "application/json"},
+ )
+ new_fulfillment_event = shopify.FulfillmentEvent(
+ {"order_id": "2776493818019", "fulfillment_id": "2608403447971"}
+ )
+ new_fulfillment_event.status = "ready_for_pickup"
new_fulfillment_event.save()
def test_error_on_incorrect_status(self):
with self.assertRaises(AttributeError):
- self.fake("orders/2776493818019/fulfillments/2608403447971/events/12584341209251", method='GET', body=self.load_fixture('fulfillment_event'))
- incorrect_status = 'asdf'
- fulfillment_event = shopify.FulfillmentEvent.find(12584341209251, order_id='2776493818019', fulfillment_id='2608403447971')
+ self.fake(
+ "orders/2776493818019/fulfillments/2608403447971/events/12584341209251",
+ method="GET",
+ body=self.load_fixture("fulfillment_event"),
+ )
+ incorrect_status = "asdf"
+ fulfillment_event = shopify.FulfillmentEvent.find(
+ 12584341209251, order_id="2776493818019", fulfillment_id="2608403447971"
+ )
fulfillment_event.status = incorrect_status
fulfillment_event.save()
-
diff --git a/test/fulfillment_service_test.py b/test/fulfillment_service_test.py
index 81296698..7519b3b9 100644
--- a/test/fulfillment_service_test.py
+++ b/test/fulfillment_service_test.py
@@ -1,15 +1,21 @@
import shopify
from test.test_helper import TestCase
+
class FulfillmentServiceTest(TestCase):
def test_create_new_fulfillment_service(self):
- self.fake("fulfillment_services", method='POST', body=self.load_fixture('fulfillment_service'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "fulfillment_services",
+ method="POST",
+ body=self.load_fixture("fulfillment_service"),
+ headers={"Content-type": "application/json"},
+ )
- fulfillment_service = shopify.FulfillmentService.create({'name': "SomeService"})
+ fulfillment_service = shopify.FulfillmentService.create({"name": "SomeService"})
self.assertEqual("SomeService", fulfillment_service.name)
def test_get_fulfillment_service(self):
- self.fake("fulfillment_services/123456", method='GET', body=self.load_fixture('fulfillment_service'))
+ self.fake("fulfillment_services/123456", method="GET", body=self.load_fixture("fulfillment_service"))
fulfillment_service = shopify.FulfillmentService.find(123456)
self.assertEqual("SomeService", fulfillment_service.name)
@@ -17,4 +23,4 @@ def test_get_fulfillment_service(self):
def test_set_format_attribute(self):
fulfillment_service = shopify.FulfillmentService()
fulfillment_service.format = "json"
- self.assertEqual("json", fulfillment_service.attributes['format'])
+ self.assertEqual("json", fulfillment_service.attributes["format"])
diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py
index 7b88623f..8bb84efb 100644
--- a/test/fulfillment_test.py
+++ b/test/fulfillment_test.py
@@ -2,62 +2,82 @@
from test.test_helper import TestCase
from pyactiveresource.activeresource import ActiveResource
-class FulFillmentTest(TestCase):
+class FulFillmentTest(TestCase):
def setUp(self):
super(FulFillmentTest, self).setUp()
- self.fake("orders/450789469/fulfillments/255858046", method='GET', body=self.load_fixture('fulfillment'))
+ self.fake("orders/450789469/fulfillments/255858046", method="GET", body=self.load_fixture("fulfillment"))
def test_able_to_open_fulfillment(self):
fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
- success = self.load_fixture('fulfillment')
- success = success.replace(b'pending',b'open')
- self.fake("orders/450789469/fulfillments/255858046/open", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=success)
+ success = self.load_fixture("fulfillment")
+ success = success.replace(b"pending", b"open")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/open",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=success,
+ )
- self.assertEqual('pending', fulfillment.status)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.open()
- self.assertEqual('open', fulfillment.status)
+ self.assertEqual("open", fulfillment.status)
def test_able_to_complete_fulfillment(self):
fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
- success = self.load_fixture('fulfillment')
- success = success.replace(b'pending',b'success')
- self.fake("orders/450789469/fulfillments/255858046/complete", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=success)
+ success = self.load_fixture("fulfillment")
+ success = success.replace(b"pending", b"success")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/complete",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=success,
+ )
- self.assertEqual('pending', fulfillment.status)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.complete()
- self.assertEqual('success', fulfillment.status)
-
+ self.assertEqual("success", fulfillment.status)
+
def test_able_to_cancel_fulfillment(self):
fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
- cancelled = self.load_fixture('fulfillment')
- cancelled = cancelled.replace(b'pending', b'cancelled')
- self.fake("orders/450789469/fulfillments/255858046/cancel", method='POST', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=cancelled)
+ cancelled = self.load_fixture("fulfillment")
+ cancelled = cancelled.replace(b"pending", b"cancelled")
+ self.fake(
+ "orders/450789469/fulfillments/255858046/cancel",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=cancelled,
+ )
- self.assertEqual('pending', fulfillment.status)
+ self.assertEqual("pending", fulfillment.status)
fulfillment.cancel()
- self.assertEqual('cancelled', fulfillment.status)
+ self.assertEqual("cancelled", fulfillment.status)
def test_update_tracking(self):
fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469)
- tracking_info = { "number": 1111, "url": "http://www.my-url.com", "company": "my-company"}
+ tracking_info = {"number": 1111, "url": "http://www.my-url.com", "company": "my-company"}
notify_customer = False
- update_tracking = self.load_fixture('fulfillment')
- update_tracking = update_tracking.replace(b'null-company', b'my-company')
- update_tracking = update_tracking.replace(b'http://www.google.com/search?q=1Z2345', b'http://www.my-url.com')
- update_tracking = update_tracking.replace(b'1Z2345', b'1111')
+ update_tracking = self.load_fixture("fulfillment")
+ update_tracking = update_tracking.replace(b"null-company", b"my-company")
+ update_tracking = update_tracking.replace(b"http://www.google.com/search?q=1Z2345", b"http://www.my-url.com")
+ update_tracking = update_tracking.replace(b"1Z2345", b"1111")
- self.fake("fulfillments/255858046/update_tracking", method="POST", headers={'Content-type': 'application/json'}, body=update_tracking)
+ self.fake(
+ "fulfillments/255858046/update_tracking",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=update_tracking,
+ )
self.assertEqual("null-company", fulfillment.tracking_company)
self.assertEqual("1Z2345", fulfillment.tracking_number)
self.assertEqual("http://www.google.com/search?q=1Z2345", fulfillment.tracking_url)
fulfillment.update_tracking(tracking_info, notify_customer)
self.assertEqual("my-company", fulfillment.tracking_company)
- self.assertEqual('1111', fulfillment.tracking_number)
- self.assertEqual('http://www.my-url.com', fulfillment.tracking_url)
+ self.assertEqual("1111", fulfillment.tracking_number)
+ self.assertEqual("http://www.my-url.com", fulfillment.tracking_url)
diff --git a/test/gift_card_test.py b/test/gift_card_test.py
index ca209090..2b5e6055 100644
--- a/test/gift_card_test.py
+++ b/test/gift_card_test.py
@@ -2,40 +2,64 @@
import shopify
from test.test_helper import TestCase
-class GiftCardTest(TestCase):
+class GiftCardTest(TestCase):
def test_gift_card_creation(self):
- self.fake('gift_cards', method='POST', code=202, body=self.load_fixture('gift_card'), headers={'Content-type': 'application/json'})
- gift_card = shopify.GiftCard.create({'code': 'd7a2bcggda89c293', 'note': "Gift card note."})
+ self.fake(
+ "gift_cards",
+ method="POST",
+ code=202,
+ body=self.load_fixture("gift_card"),
+ headers={"Content-type": "application/json"},
+ )
+ gift_card = shopify.GiftCard.create({"code": "d7a2bcggda89c293", "note": "Gift card note."})
self.assertEqual("Gift card note.", gift_card.note)
self.assertEqual("c293", gift_card.last_characters)
def test_fetch_gift_cards(self):
- self.fake('gift_cards', method='GET', code=200, body=self.load_fixture('gift_cards'))
+ self.fake("gift_cards", method="GET", code=200, body=self.load_fixture("gift_cards"))
gift_cards = shopify.GiftCard.find()
self.assertEqual(1, len(gift_cards))
def test_disable_gift_card(self):
- self.fake('gift_cards/4208208', method='GET', code=200, body=self.load_fixture('gift_card'))
- self.fake('gift_cards/4208208/disable', method='POST', code=200, body=self.load_fixture('gift_card_disabled'), headers={'Content-length': '0', 'Content-type': 'application/json'})
+ self.fake("gift_cards/4208208", method="GET", code=200, body=self.load_fixture("gift_card"))
+ self.fake(
+ "gift_cards/4208208/disable",
+ method="POST",
+ code=200,
+ body=self.load_fixture("gift_card_disabled"),
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ )
gift_card = shopify.GiftCard.find(4208208)
self.assertFalse(gift_card.disabled_at)
gift_card.disable()
self.assertTrue(gift_card.disabled_at)
def test_adjust_gift_card(self):
- self.fake('gift_cards/4208208', method='GET', code=200, body=self.load_fixture('gift_card'))
- self.fake('gift_cards/4208208/adjustments', method='POST', code=201, body=self.load_fixture('gift_card_adjustment'), headers={'Content-type': 'application/json'})
+ self.fake("gift_cards/4208208", method="GET", code=200, body=self.load_fixture("gift_card"))
+ self.fake(
+ "gift_cards/4208208/adjustments",
+ method="POST",
+ code=201,
+ body=self.load_fixture("gift_card_adjustment"),
+ headers={"Content-type": "application/json"},
+ )
gift_card = shopify.GiftCard.find(4208208)
self.assertEqual(gift_card.balance, "25.00")
- adjustment = gift_card.add_adjustment(shopify.GiftCardAdjustment({
- 'amount': 100,
- }))
+ adjustment = gift_card.add_adjustment(
+ shopify.GiftCardAdjustment(
+ {
+ "amount": 100,
+ }
+ )
+ )
self.assertIsInstance(adjustment, shopify.GiftCardAdjustment)
self.assertEqual(Decimal(adjustment.amount), Decimal("100"))
def test_search(self):
- self.fake("gift_cards/search.json?query=balance%3A10", extension=False, body=self.load_fixture('gift_cards_search'))
+ self.fake(
+ "gift_cards/search.json?query=balance%3A10", extension=False, body=self.load_fixture("gift_cards_search")
+ )
- results = shopify.GiftCard.search(query='balance:10')
- self.assertEqual(results[0].balance, "10.00")
\ No newline at end of file
+ results = shopify.GiftCard.search(query="balance:10")
+ self.assertEqual(results[0].balance, "10.00")
diff --git a/test/graphql_test.py b/test/graphql_test.py
index e6115f46..dc32b935 100644
--- a/test/graphql_test.py
+++ b/test/graphql_test.py
@@ -2,33 +2,45 @@
import json
from test.test_helper import TestCase
-class GraphQLTest(TestCase):
+class GraphQLTest(TestCase):
def setUp(self):
super(GraphQLTest, self).setUp()
shopify.ApiVersion.define_known_versions()
- shopify_session = shopify.Session('this-is-my-test-show.myshopify.com', 'unstable', 'token')
+ shopify_session = shopify.Session("this-is-my-test-show.myshopify.com", "unstable", "token")
shopify.ShopifyResource.activate_session(shopify_session)
- client = shopify.GraphQL()
+ self.client = shopify.GraphQL()
self.fake(
- 'graphql',
- method='POST',
- code=201,
+ "graphql",
+ method="POST",
+ code=201,
headers={
- 'X-Shopify-Access-Token': 'token',
- 'Accept': 'application/json',
- 'Content-Type': 'application/json'
- })
- query = '''
+ "X-Shopify-Access-Token": "token",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ },
+ )
+
+ def test_fetch_shop_with_graphql(self):
+ query = """
{
shop {
name
id
}
}
- '''
- self.result = client.execute(query)
-
+ """
+ result = self.client.execute(query)
+ self.assertTrue(json.loads(result)["shop"]["name"] == "Apple Computers")
- def test_fetch_shop_with_graphql(self):
- self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers')
\ No newline at end of file
+ def test_specify_operation_name(self):
+ query = """
+ query GetShop{
+ shop {
+ name
+ id
+ }
+ }
+ """
+ result = self.client.execute(query, operation_name="GetShop")
+ self.assertTrue(json.loads(result)["shop"]["name"] == "Apple Computers")
diff --git a/test/image_test.py b/test/image_test.py
index bde789ec..3dad3817 100644
--- a/test/image_test.py
+++ b/test/image_test.py
@@ -2,57 +2,80 @@
from test.test_helper import TestCase
import base64
-class ImageTest(TestCase):
+class ImageTest(TestCase):
def test_create_image(self):
- self.fake("products/632910392/images", method='POST', body=self.load_fixture('image'), headers={'Content-type': 'application/json'})
- image = shopify.Image({'product_id':632910392})
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
+ image = shopify.Image({"product_id": 632910392})
image.position = 1
image.attachment = "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
image.save()
- self.assertEqual('http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540', image.src)
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
self.assertEqual(850703190, image.id)
def test_attach_image(self):
- self.fake("products/632910392/images", method='POST', body=self.load_fixture('image'), headers={'Content-type': 'application/json'})
- image = shopify.Image({'product_id':632910392})
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
+ image = shopify.Image({"product_id": 632910392})
image.position = 1
- binary_in = base64.b64decode("R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf==")
- image.attach_image(data=binary_in, filename='ipod-nano.png')
+ binary_in = base64.b64decode(
+ "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
+ )
+ image.attach_image(data=binary_in, filename="ipod-nano.png")
image.save()
binary_out = base64.b64decode(image.attachment)
- self.assertEqual('http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540', image.src)
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
self.assertEqual(850703190, image.id)
self.assertEqual(binary_in, binary_out)
def test_create_image_then_add_parent_id(self):
- self.fake("products/632910392/images", method='POST', body=self.load_fixture('image'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "products/632910392/images",
+ method="POST",
+ body=self.load_fixture("image"),
+ headers={"Content-type": "application/json"},
+ )
image = shopify.Image()
image.position = 1
image.product_id = 632910392
image.attachment = "R0lGODlhbgCMAPf/APbr48VySrxTO7IgKt2qmKQdJeK8lsFjROG5p/nz7Zg3MNmnd7Q1MLNVS9GId71hSJMZIuzTu4UtKbeEeakhKMl8U8WYjfr18YQaIbAf=="
image.save()
- self.assertEqual('http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540', image.src)
+ self.assertEqual(
+ "http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540", image.src
+ )
self.assertEqual(850703190, image.id)
def test_get_images(self):
- self.fake("products/632910392/images", method='GET', body=self.load_fixture('images'))
+ self.fake("products/632910392/images", method="GET", body=self.load_fixture("images"))
image = shopify.Image.find(product_id=632910392)
self.assertEqual(2, len(image))
def test_get_image(self):
- self.fake("products/632910392/images/850703190", method='GET', body=self.load_fixture('image'))
+ self.fake("products/632910392/images/850703190", method="GET", body=self.load_fixture("image"))
image = shopify.Image.find(850703190, product_id=632910392)
self.assertEqual(850703190, image.id)
def test_get_metafields_for_image(self):
- fake_extension = 'json?metafield[owner_id]=850703190&metafield[owner_resource]=product_image'
- self.fake("metafields", method='GET', extension=fake_extension, body=self.load_fixture('image_metafields'))
+ fake_extension = "json?metafield[owner_id]=850703190&metafield[owner_resource]=product_image"
+ self.fake("metafields", method="GET", extension=fake_extension, body=self.load_fixture("image_metafields"))
- image = shopify.Image(attributes = { 'id': 850703190, 'product_id': 632910392 })
+ image = shopify.Image(attributes={"id": 850703190, "product_id": 632910392})
metafields = image.metafields()
self.assertEqual(1, len(metafields))
diff --git a/test/inventory_item_test.py b/test/inventory_item_test.py
index ca4d9476..61c66e60 100644
--- a/test/inventory_item_test.py
+++ b/test/inventory_item_test.py
@@ -1,23 +1,19 @@
import shopify
from test.test_helper import TestCase
-class InventoryItemTest(TestCase):
+class InventoryItemTest(TestCase):
def test_fetch_inventory_item(self):
- self.fake(
- 'inventory_items/123456789',
- method='GET',
- body=self.load_fixture('inventory_item')
- )
+ self.fake("inventory_items/123456789", method="GET", body=self.load_fixture("inventory_item"))
inventory_item = shopify.InventoryItem.find(123456789)
self.assertEqual(inventory_item.sku, "IPOD2008PINK")
def test_fetch_inventory_item_ids(self):
self.fake(
- 'inventory_items.json?ids=123456789%2C234567891',
- extension='',
- method='GET',
- body=self.load_fixture('inventory_items')
+ "inventory_items.json?ids=123456789%2C234567891",
+ extension="",
+ method="GET",
+ body=self.load_fixture("inventory_items"),
)
- inventory_items = shopify.InventoryItem.find(ids='123456789,234567891')
+ inventory_items = shopify.InventoryItem.find(ids="123456789,234567891")
self.assertEqual(3, len(inventory_items))
diff --git a/test/inventory_level_test.py b/test/inventory_level_test.py
index c969c1d0..cf621b7c 100644
--- a/test/inventory_level_test.py
+++ b/test/inventory_level_test.py
@@ -3,65 +3,66 @@
from six.moves.urllib.parse import urlencode
from test.test_helper import TestCase
-class InventoryLevelTest(TestCase):
+class InventoryLevelTest(TestCase):
def test_fetch_inventory_level(self):
- params = {'inventory_item_ids': [808950810, 39072856], 'location_ids': [905684977, 487838322]}
+ params = {"inventory_item_ids": [808950810, 39072856], "location_ids": [905684977, 487838322]}
self.fake(
- 'inventory_levels.json?location_ids=905684977%2C487838322&inventory_item_ids=808950810%2C39072856',
- method='GET',
- extension='',
- body=self.load_fixture('inventory_levels')
+ "inventory_levels.json?location_ids=905684977%2C487838322&inventory_item_ids=808950810%2C39072856",
+ method="GET",
+ extension="",
+ body=self.load_fixture("inventory_levels"),
)
inventory_levels = shopify.InventoryLevel.find(
- inventory_item_ids='808950810,39072856',
- location_ids='905684977,487838322'
+ inventory_item_ids="808950810,39072856", location_ids="905684977,487838322"
)
self.assertTrue(
- all(item.location_id in params['location_ids'] and item.inventory_item_id in params['inventory_item_ids'] for item in inventory_levels)
+ all(
+ item.location_id in params["location_ids"] and item.inventory_item_id in params["inventory_item_ids"]
+ for item in inventory_levels
+ )
)
def test_inventory_level_adjust(self):
self.fake(
- 'inventory_levels/adjust',
- method='POST',
- body=self.load_fixture('inventory_level'),
- headers={'Content-type': 'application/json'}
+ "inventory_levels/adjust",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
)
inventory_level = shopify.InventoryLevel.adjust(905684977, 808950810, 5)
self.assertEqual(inventory_level.available, 6)
def test_inventory_level_connect(self):
self.fake(
- 'inventory_levels/connect',
- method='POST',
- body=self.load_fixture('inventory_level'),
- headers={'Content-type': 'application/json'},
- code=201
+ "inventory_levels/connect",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
+ code=201,
)
inventory_level = shopify.InventoryLevel.connect(905684977, 808950810)
self.assertEqual(inventory_level.available, 6)
def test_inventory_level_set(self):
self.fake(
- 'inventory_levels/set',
- method='POST',
- body=self.load_fixture('inventory_level'),
- headers={'Content-type': 'application/json'},
+ "inventory_levels/set",
+ method="POST",
+ body=self.load_fixture("inventory_level"),
+ headers={"Content-type": "application/json"},
)
inventory_level = shopify.InventoryLevel.set(905684977, 808950810, 6)
self.assertEqual(inventory_level.available, 6)
def test_destroy_inventory_level(self):
- inventory_level_response = json.loads(self.load_fixture('inventory_level').decode())
- inventory_level = shopify.InventoryLevel(inventory_level_response['inventory_level'])
+ inventory_level_response = json.loads(self.load_fixture("inventory_level").decode())
+ inventory_level = shopify.InventoryLevel(inventory_level_response["inventory_level"])
- query_params = urlencode({
- 'inventory_item_id': inventory_level.inventory_item_id,
- 'location_id': inventory_level.location_id
- })
+ query_params = urlencode(
+ {"inventory_item_id": inventory_level.inventory_item_id, "location_id": inventory_level.location_id}
+ )
path = "inventory_levels.json?" + query_params
- self.fake(path, extension=False, method='DELETE', code=204, body='{}')
+ self.fake(path, extension=False, method="DELETE", code=204, body="{}")
inventory_level.destroy()
diff --git a/test/limits_test.py b/test/limits_test.py
index 5651c902..0d705abb 100644
--- a/test/limits_test.py
+++ b/test/limits_test.py
@@ -9,13 +9,14 @@ class LimitsTest(TestCase):
Conversion of test/limits_test.rb
"""
+
@classmethod
def setUpClass(self):
self.original_headers = None
def setUp(self):
super(LimitsTest, self).setUp()
- self.fake('shop')
+ self.fake("shop")
shopify.Shop.current()
# TODO: Fake not support Headers
self.original_headers = shopify.Shop.connection.response.headers
@@ -29,44 +30,36 @@ def test_raise_error_no_header(self):
shopify.Limits.credit_left()
def test_raise_error_invalid_header(self):
- with patch.dict(
- shopify.Shop.connection.response.headers,
- {'bad': 'value'},
- clear=True):
+ with patch.dict(shopify.Shop.connection.response.headers, {"bad": "value"}, clear=True):
with self.assertRaises(Exception):
shopify.Limits.credit_left()
def test_fetch_limits_total(self):
with patch.dict(
- shopify.Shop.connection.response.headers,
- {'X-Shopify-Shop-Api-Call-Limit': '40/40'},
- clear=True):
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "40/40"}, clear=True
+ ):
self.assertEqual(40, shopify.Limits.credit_limit())
def test_fetch_used_calls(self):
with patch.dict(
- shopify.Shop.connection.response.headers,
- {'X-Shopify-Shop-Api-Call-Limit': '1/40'},
- clear=True):
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "1/40"}, clear=True
+ ):
self.assertEqual(1, shopify.Limits.credit_used())
def test_calculate_remaining_calls(self):
with patch.dict(
- shopify.Shop.connection.response.headers,
- {'X-Shopify-Shop-Api-Call-Limit': '292/300'},
- clear=True):
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "292/300"}, clear=True
+ ):
self.assertEqual(8, shopify.Limits.credit_left())
def test_maxed_credits_false(self):
with patch.dict(
- shopify.Shop.connection.response.headers,
- {'X-Shopify-Shop-Api-Call-Limit': '125/300'},
- clear=True):
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "125/300"}, clear=True
+ ):
self.assertFalse(shopify.Limits.credit_maxed())
def test_maxed_credits_true(self):
with patch.dict(
- shopify.Shop.connection.response.headers,
- {'X-Shopify-Shop-Api-Call-Limit': '40/40'},
- clear=True):
+ shopify.Shop.connection.response.headers, {"X-Shopify-Shop-Api-Call-Limit": "40/40"}, clear=True
+ ):
self.assertTrue(shopify.Limits.credit_maxed())
diff --git a/test/location_test.py b/test/location_test.py
index 4d22ae6e..8550b1af 100644
--- a/test/location_test.py
+++ b/test/location_test.py
@@ -2,29 +2,30 @@
import json
from test.test_helper import TestCase
+
class LocationTest(TestCase):
def test_fetch_locations(self):
- self.fake("locations", method='GET', body=self.load_fixture('locations'))
+ self.fake("locations", method="GET", body=self.load_fixture("locations"))
locations = shopify.Location.find()
- self.assertEqual(2,len(locations))
+ self.assertEqual(2, len(locations))
def test_fetch_location(self):
- self.fake("locations/487838322", method='GET', body=self.load_fixture('location'))
+ self.fake("locations/487838322", method="GET", body=self.load_fixture("location"))
location = shopify.Location.find(487838322)
- self.assertEqual(location.id,487838322)
- self.assertEqual(location.name,"Fifth Avenue AppleStore")
+ self.assertEqual(location.id, 487838322)
+ self.assertEqual(location.name, "Fifth Avenue AppleStore")
def test_inventory_levels_returns_all_inventory_levels(self):
- location = shopify.Location({'id':487838322})
+ location = shopify.Location({"id": 487838322})
self.fake(
"locations/%s/inventory_levels" % location.id,
- method='GET',
+ method="GET",
code=200,
- body=self.load_fixture('location_inventory_levels')
+ body=self.load_fixture("location_inventory_levels"),
)
inventory_levels = location.inventory_levels()
self.assertEqual(location.id, inventory_levels[0].location_id)
self.assertEqual(27, inventory_levels[0].available)
- self.assertEqual(9, inventory_levels[1].available)
\ No newline at end of file
+ self.assertEqual(9, inventory_levels[1].available)
diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py
index a49afa9a..2f73029d 100644
--- a/test/marketing_event_test.py
+++ b/test/marketing_event_test.py
@@ -4,82 +4,94 @@
class MarketingEventTest(TestCase):
- def setUp(self):
- super(MarketingEventTest, self).setUp()
-
def test_get_marketing_event(self):
- self.fake('marketing_events/1', method='GET', body=self.load_fixture('marketing_event'))
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
marketing_event = shopify.MarketingEvent.find(1)
self.assertEqual(marketing_event.id, 1)
def test_get_marketing_events(self):
- self.fake('marketing_events', method='GET', body=self.load_fixture('marketing_events'))
+ self.fake("marketing_events", method="GET", body=self.load_fixture("marketing_events"))
marketing_events = shopify.MarketingEvent.find()
self.assertEqual(len(marketing_events), 2)
def test_create_marketing_event(self):
- self.fake('marketing_events', method='POST', body=self.load_fixture('marketing_event'), headers={ 'Content-type': 'application/json' })
+ self.fake(
+ "marketing_events",
+ method="POST",
+ body=self.load_fixture("marketing_event"),
+ headers={"Content-type": "application/json"},
+ )
marketing_event = shopify.MarketingEvent()
- marketing_event.currency_code = 'GBP'
- marketing_event.event_target = 'facebook'
- marketing_event.event_type = 'post'
+ marketing_event.currency_code = "GBP"
+ marketing_event.event_target = "facebook"
+ marketing_event.event_type = "post"
marketing_event.save()
- self.assertEqual(marketing_event.event_target, 'facebook')
- self.assertEqual(marketing_event.currency_code, 'GBP')
- self.assertEqual(marketing_event.event_type, 'post')
+ self.assertEqual(marketing_event.event_target, "facebook")
+ self.assertEqual(marketing_event.currency_code, "GBP")
+ self.assertEqual(marketing_event.event_type, "post")
def test_delete_marketing_event(self):
- self.fake('marketing_events/1', method='GET', body=self.load_fixture('marketing_event'))
- self.fake('marketing_events/1', method='DELETE', body='destroyed')
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
+ self.fake("marketing_events/1", method="DELETE", body="destroyed")
marketing_event = shopify.MarketingEvent.find(1)
marketing_event.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
def test_update_marketing_event(self):
- self.fake('marketing_events/1', method='GET', code=200, body=self.load_fixture('marketing_event'))
- self.fake('marketing_events/1', method='PUT', code=200, body=self.load_fixture('marketing_event'), headers={'Content-type': 'application/json'})
+ self.fake("marketing_events/1", method="GET", code=200, body=self.load_fixture("marketing_event"))
+ self.fake(
+ "marketing_events/1",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("marketing_event"),
+ headers={"Content-type": "application/json"},
+ )
marketing_event = shopify.MarketingEvent.find(1)
- marketing_event.currency = 'USD'
+ marketing_event.currency = "USD"
self.assertTrue(marketing_event.save())
def test_count_marketing_events(self):
- self.fake('marketing_events/count', method='GET', body='{"count": 2}')
+ self.fake("marketing_events/count", method="GET", body='{"count": 2}')
marketing_events_count = shopify.MarketingEvent.count()
self.assertEqual(marketing_events_count, 2)
def test_add_engagements(self):
- self.fake('marketing_events/1', method='GET', body=self.load_fixture('marketing_event'))
+ self.fake("marketing_events/1", method="GET", body=self.load_fixture("marketing_event"))
self.fake(
- 'marketing_events/1/engagements',
- method='POST',
- code=201,
- body=self.load_fixture('engagement'),
- headers={'Content-type': 'application/json'}
+ "marketing_events/1/engagements",
+ method="POST",
+ code=201,
+ body=self.load_fixture("engagement"),
+ headers={"Content-type": "application/json"},
)
marketing_event = shopify.MarketingEvent.find(1)
- response = marketing_event.add_engagements([{
- 'occurred_on': '2017-04-20',
- 'impressions_count': None,
- 'views_count': None,
- 'clicks_count': 10,
- 'shares_count': None,
- 'favorites_count': None,
- 'comments_count': None,
- 'ad_spend': None,
- 'is_cumulative': True
- }])
-
- request_data = json.loads(self.http.request.data.decode("utf-8"))['engagements']
+ response = marketing_event.add_engagements(
+ [
+ {
+ "occurred_on": "2017-04-20",
+ "impressions_count": None,
+ "views_count": None,
+ "clicks_count": 10,
+ "shares_count": None,
+ "favorites_count": None,
+ "comments_count": None,
+ "ad_spend": None,
+ "is_cumulative": True,
+ }
+ ]
+ )
+
+ request_data = json.loads(self.http.request.data.decode("utf-8"))["engagements"]
self.assertEqual(len(request_data), 1)
- self.assertEqual(request_data[0]['occurred_on'], '2017-04-20')
+ self.assertEqual(request_data[0]["occurred_on"], "2017-04-20")
- response_data = json.loads(response.body.decode("utf-8"))['engagements']
+ response_data = json.loads(response.body.decode("utf-8"))["engagements"]
self.assertEqual(len(response_data), 1)
- self.assertEqual(response_data[0]['occurred_on'], '2017-04-20')
+ self.assertEqual(response_data[0]["occurred_on"], "2017-04-20")
diff --git a/test/order_risk_test.py b/test/order_risk_test.py
index fb8b09b8..4ca6dbff 100644
--- a/test/order_risk_test.py
+++ b/test/order_risk_test.py
@@ -1,42 +1,52 @@
import shopify
from test.test_helper import TestCase
-class OrderRiskTest(TestCase):
- def test_create_order_risk(self):
- self.fake("orders/450789469/risks", method='POST', body= self.load_fixture('order_risk'), headers={'Content-type': 'application/json'})
- v = shopify.OrderRisk({'order_id':450789469})
- v.message = "This order was placed from a proxy IP"
- v.recommendation = "cancel"
- v.score = "1.0"
- v.source = "External"
- v.merchant_message = "This order was placed from a proxy IP"
- v.display = True
- v.cause_cancel = True
- v.save()
-
- self.assertEqual(284138680, v.id)
-
- def test_get_order_risks(self):
- self.fake("orders/450789469/risks", method='GET', body= self.load_fixture('order_risks'))
- v = shopify.OrderRisk.find(order_id=450789469)
- self.assertEqual(2, len(v))
-
- def test_get_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- self.assertEqual(284138680, v.id)
-
- def test_delete_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- self.fake("orders/450789469/risks/284138680", method='DELETE', body="destroyed")
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- v.destroy()
-
- def test_delete_order_risk(self):
- self.fake("orders/450789469/risks/284138680", method='GET', body= self.load_fixture('order_risk'))
- self.fake("orders/450789469/risks/284138680", method='PUT', body= self.load_fixture('order_risk'), headers={'Content-type': 'application/json'})
-
- v = shopify.OrderRisk.find(284138680, order_id=450789469)
- v.position = 3
- v.save()
+class OrderRiskTest(TestCase):
+ def test_create_order_risk(self):
+ self.fake(
+ "orders/450789469/risks",
+ method="POST",
+ body=self.load_fixture("order_risk"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.OrderRisk({"order_id": 450789469})
+ v.message = "This order was placed from a proxy IP"
+ v.recommendation = "cancel"
+ v.score = "1.0"
+ v.source = "External"
+ v.merchant_message = "This order was placed from a proxy IP"
+ v.display = True
+ v.cause_cancel = True
+ v.save()
+
+ self.assertEqual(284138680, v.id)
+
+ def test_get_order_risks(self):
+ self.fake("orders/450789469/risks", method="GET", body=self.load_fixture("order_risks"))
+ v = shopify.OrderRisk.find(order_id=450789469)
+ self.assertEqual(2, len(v))
+
+ def test_get_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ self.assertEqual(284138680, v.id)
+
+ def test_delete_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ self.fake("orders/450789469/risks/284138680", method="DELETE", body="destroyed")
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ v.destroy()
+
+ def test_delete_order_risk(self):
+ self.fake("orders/450789469/risks/284138680", method="GET", body=self.load_fixture("order_risk"))
+ self.fake(
+ "orders/450789469/risks/284138680",
+ method="PUT",
+ body=self.load_fixture("order_risk"),
+ headers={"Content-type": "application/json"},
+ )
+
+ v = shopify.OrderRisk.find(284138680, order_id=450789469)
+ v.position = 3
+ v.save()
diff --git a/test/order_test.py b/test/order_test.py
index 8b173c00..257ab08a 100644
--- a/test/order_test.py
+++ b/test/order_test.py
@@ -3,8 +3,8 @@
from pyactiveresource.activeresource import ActiveResource
from pyactiveresource.util import xml_to_dict
-class OrderTest(TestCase):
+class OrderTest(TestCase):
def test_should_be_loaded_correctly_from_order_xml(self):
order_xml = """
@@ -26,7 +26,7 @@ def test_should_be_loaded_correctly_from_order_xml(self):
def test_should_be_able_to_add_note_attributes_to_an_order(self):
order = shopify.Order()
order.note_attributes = []
- order.note_attributes.append(shopify.NoteAttribute({'name': "color", 'value': "blue"}))
+ order.note_attributes.append(shopify.NoteAttribute({"name": "color", "value": "blue"}))
order_xml = xml_to_dict(order.to_xml())
note_attributes = order_xml["order"]["note_attributes"]
@@ -37,13 +37,20 @@ def test_should_be_able_to_add_note_attributes_to_an_order(self):
self.assertEqual("blue", attribute["value"])
def test_get_order(self):
- self.fake('orders/450789469', method='GET', body=self.load_fixture('order'))
+ self.fake("orders/450789469", method="GET", body=self.load_fixture("order"))
order = shopify.Order.find(450789469)
- self.assertEqual('bob.norman@hostmail.com', order.email)
+ self.assertEqual("bob.norman@hostmail.com", order.email)
def test_get_order_transaction(self):
- self.fake('orders/450789469', method='GET', body=self.load_fixture('order'))
+ self.fake("orders/450789469", method="GET", body=self.load_fixture("order"))
order = shopify.Order.find(450789469)
- self.fake('orders/450789469/transactions', method='GET', body=self.load_fixture('transactions'))
+ self.fake("orders/450789469/transactions", method="GET", body=self.load_fixture("transactions"))
transactions = order.transactions()
self.assertEqual("409.94", transactions[0].amount)
+
+ def test_get_customer_orders(self):
+ self.fake("customers/207119551/orders", method="GET", body=self.load_fixture("orders"), code=200)
+ orders = shopify.Order.find(customer_id=207119551)
+ self.assertIsInstance(orders[0], shopify.Order)
+ self.assertEqual(450789469, orders[0].id)
+ self.assertEqual(207119551, orders[0].customer.id)
diff --git a/test/pagination_test.py b/test/pagination_test.py
index 6b00bb78..72fc78e9 100644
--- a/test/pagination_test.py
+++ b/test/pagination_test.py
@@ -2,38 +2,46 @@
import json
from test.test_helper import TestCase
-class PaginationTest(TestCase):
+class PaginationTest(TestCase):
def setUp(self):
super(PaginationTest, self).setUp()
prefix = self.http.site + "/admin/api/unstable"
- fixture = json.loads(self.load_fixture('products').decode())
+ fixture = json.loads(self.load_fixture("products").decode())
self.next_page_url = prefix + "/products.json?limit=2&page_info=FOOBAR"
self.prev_page_url = prefix + "/products.json?limit=2&page_info=BAZQUUX"
- next_headers = {"Link": "<" + self.next_page_url + ">; rel=\"next\""}
- prev_headers = {"Link": "<" + self.prev_page_url + ">; rel=\"previous\""}
-
- self.fake("products",
- url=prefix + "/products.json?limit=2",
- body=json.dumps({ "products": fixture[:2] }),
- response_headers=next_headers)
- self.fake("products",
- url=prefix + "/products.json?limit=2&page_info=FOOBAR",
- body=json.dumps({ "products": fixture[2:4] }),
- response_headers=prev_headers)
- self.fake("products",
- url=prefix + "/products.json?limit=2&page_info=BAZQUUX",
- body=json.dumps({ "products": fixture[:2] }),
- response_headers=next_headers)
+ next_headers = {"Link": "<" + self.next_page_url + '>; rel="next"'}
+ prev_headers = {"Link": "<" + self.prev_page_url + '>; rel="previous"'}
+
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2",
+ body=json.dumps({"products": fixture[:2]}),
+ response_headers=next_headers,
+ )
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2&page_info=FOOBAR",
+ body=json.dumps({"products": fixture[2:4]}),
+ response_headers=prev_headers,
+ )
+ self.fake(
+ "products",
+ url=prefix + "/products.json?limit=2&page_info=BAZQUUX",
+ body=json.dumps({"products": fixture[:2]}),
+ response_headers=next_headers,
+ )
def test_nonpaginates_collection(self):
- self.fake('draft_orders', method='GET', code=200, body=self.load_fixture('draft_orders'))
+ self.fake("draft_orders", method="GET", code=200, body=self.load_fixture("draft_orders"))
draft_orders = shopify.DraftOrder.find()
self.assertEqual(1, len(draft_orders))
self.assertEqual(517119332, draft_orders[0].id)
- self.assertIsInstance(draft_orders, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection")
+ self.assertIsInstance(
+ draft_orders, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection"
+ )
def test_paginated_collection(self):
items = shopify.Product.find(limit=2)
@@ -45,12 +53,12 @@ def test_pagination_next_page(self):
self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect")
n = c.next_page()
self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect")
- self.assertIsInstance(n, shopify.collection.PaginatedCollection,
- "next_page() result is not PaginatedCollection")
+ self.assertIsInstance(
+ n, shopify.collection.PaginatedCollection, "next_page() result is not PaginatedCollection"
+ )
self.assertEqual(len(n), 2, "next_page() collection has incorrect length")
self.assertIn("pagination", n.metadata)
- self.assertIn("previous", n.metadata["pagination"],
- "next_page() collection doesn't have a previous page")
+ self.assertIn("previous", n.metadata["pagination"], "next_page() collection doesn't have a previous page")
with self.assertRaises(IndexError, msg="next_page() did not raise with no next page"):
n.next_page()
@@ -65,13 +73,12 @@ def test_pagination_previous(self):
p = n.previous_page()
- self.assertIsInstance(p, shopify.collection.PaginatedCollection,
- "previous_page() result is not PaginatedCollection")
- self.assertEqual(len(p), 4, # cached
- "previous_page() collection has incorrect length")
+ self.assertIsInstance(
+ p, shopify.collection.PaginatedCollection, "previous_page() result is not PaginatedCollection"
+ )
+ self.assertEqual(len(p), 4, "previous_page() collection has incorrect length") # cached
self.assertIn("pagination", p.metadata)
- self.assertIn("next", p.metadata["pagination"],
- "previous_page() collection doesn't have a next page")
+ self.assertIn("next", p.metadata["pagination"], "previous_page() collection doesn't have a next page")
with self.assertRaises(IndexError, msg="previous_page() did not raise with no previous page"):
p.previous_page()
diff --git a/test/payouts_test.py b/test/payouts_test.py
index a2478889..f82851c2 100644
--- a/test/payouts_test.py
+++ b/test/payouts_test.py
@@ -3,16 +3,15 @@
class PayoutsTest(TestCase):
- prefix = '/admin/api/unstable/shopify_payments'
+ prefix = "/admin/api/unstable/shopify_payments"
def test_get_payouts(self):
- self.fake('payouts', method='GET', prefix=self.prefix, body=self.load_fixture('payouts'))
+ self.fake("payouts", method="GET", prefix=self.prefix, body=self.load_fixture("payouts"))
payouts = shopify.Payouts.find()
self.assertGreater(len(payouts), 0)
def test_get_one_payout(self):
- self.fake('payouts/623721858', method='GET',
- prefix=self.prefix, body=self.load_fixture('payout'))
+ self.fake("payouts/623721858", method="GET", prefix=self.prefix, body=self.load_fixture("payout"))
payouts = shopify.Payouts.find(623721858)
- self.assertEqual('paid', payouts.status)
- self.assertEqual('41.90', payouts.amount)
+ self.assertEqual("paid", payouts.status)
+ self.assertEqual("41.90", payouts.amount)
diff --git a/test/price_rules_test.py b/test/price_rules_test.py
index 0c822cf4..a28b15de 100644
--- a/test/price_rules_test.py
+++ b/test/price_rules_test.py
@@ -5,90 +5,105 @@
class PriceRuleTest(TestCase):
-
def setUp(self):
super(PriceRuleTest, self).setUp()
- self.fake('price_rules/1213131', body=self.load_fixture('price_rule'))
+ self.fake("price_rules/1213131", body=self.load_fixture("price_rule"))
self.price_rule = shopify.PriceRule.find(1213131)
def test_get_price_rule(self):
- self.fake('price_rule/1213131', method='GET', code=200, body=self.load_fixture('price_rule'))
+ self.fake("price_rule/1213131", method="GET", code=200, body=self.load_fixture("price_rule"))
price_rule = shopify.PriceRule.find(1213131)
self.assertEqual(1213131, price_rule.id)
def test_get_all_price_rules(self):
- self.fake('price_rules',
- method='GET',
- code=200,
- body=self.load_fixture('price_rules'))
+ self.fake("price_rules", method="GET", code=200, body=self.load_fixture("price_rules"))
price_rules = shopify.PriceRule.find()
self.assertEqual(2, len(price_rules))
def test_update_price_rule(self):
self.price_rule.title = "Buy One Get One"
- self.fake('price_rules/1213131', method='PUT', code=200, body=self.load_fixture('price_rule'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "price_rules/1213131",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("price_rule"),
+ headers={"Content-type": "application/json"},
+ )
self.price_rule.save()
- self.assertEqual('Buy One Get One', json.loads(self.http.request.data.decode("utf-8"))['price_rule']['title'])
-
+ self.assertEqual("Buy One Get One", json.loads(self.http.request.data.decode("utf-8"))["price_rule"]["title"])
def test_delete_price_rule(self):
- self.fake('price_rules/1213131', method='DELETE', body='destroyed')
+ self.fake("price_rules/1213131", method="DELETE", body="destroyed")
self.price_rule.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
def test_price_rule_creation(self):
- self.fake('price_rules',
- method='POST',
- code=202,
- body=self.load_fixture('price_rule'),
- headers={'Content-type': 'application/json'})
- price_rule = shopify.PriceRule.create({
- "title": "BOGO",
- "target_type": "line_item",
- "target_selection": "all",
- "allocation_method": "across",
- "value_type": "percentage",
- "value": -100,
- "once_per_customer": 'true',
- "customer_selection": 'all'
- })
+ self.fake(
+ "price_rules",
+ method="POST",
+ code=202,
+ body=self.load_fixture("price_rule"),
+ headers={"Content-type": "application/json"},
+ )
+ price_rule = shopify.PriceRule.create(
+ {
+ "title": "BOGO",
+ "target_type": "line_item",
+ "target_selection": "all",
+ "allocation_method": "across",
+ "value_type": "percentage",
+ "value": -100,
+ "once_per_customer": "true",
+ "customer_selection": "all",
+ }
+ )
self.assertEqual("BOGO", price_rule.title)
self.assertEqual("line_item", price_rule.target_type)
def test_get_discount_codes(self):
- self.fake('price_rules/1213131/discount_codes', method='GET', code=200, body=self.load_fixture('discount_codes'))
+ self.fake(
+ "price_rules/1213131/discount_codes", method="GET", code=200, body=self.load_fixture("discount_codes")
+ )
discount_codes = self.price_rule.discount_codes()
self.assertEqual(1, len(discount_codes))
def test_add_discount_code(self):
- price_rule_discount_fixture = self.load_fixture('discount_code')
+ price_rule_discount_fixture = self.load_fixture("discount_code")
discount_code = json.loads(price_rule_discount_fixture.decode("utf-8"))
- self.fake('price_rules/1213131/discount_codes',
- method='POST',
- body=price_rule_discount_fixture,
- headers={'Content-type': 'application/json'})
- price_rule_discount_response = self.price_rule.add_discount_code(shopify.DiscountCode(discount_code['discount_code']))
+ self.fake(
+ "price_rules/1213131/discount_codes",
+ method="POST",
+ body=price_rule_discount_fixture,
+ headers={"Content-type": "application/json"},
+ )
+ price_rule_discount_response = self.price_rule.add_discount_code(
+ shopify.DiscountCode(discount_code["discount_code"])
+ )
self.assertEqual(discount_code, json.loads(self.http.request.data.decode("utf-8")))
self.assertIsInstance(price_rule_discount_response, shopify.DiscountCode)
- self.assertEqual(discount_code['discount_code']['code'], price_rule_discount_response.code)
+ self.assertEqual(discount_code["discount_code"]["code"], price_rule_discount_response.code)
def test_create_batch_discount_codes(self):
- self.fake('price_rules/1213131/batch',
- method='POST',
- code=201,
- body=self.load_fixture('discount_code_creation'),
- headers={'Content-type': 'application/json'})
- batch = self.price_rule.create_batch([{'code': 'SUMMER1'}, {'code': 'SUMMER2'}, {'code': 'SUMMER3'}])
+ self.fake(
+ "price_rules/1213131/batch",
+ method="POST",
+ code=201,
+ body=self.load_fixture("discount_code_creation"),
+ headers={"Content-type": "application/json"},
+ )
+ batch = self.price_rule.create_batch([{"code": "SUMMER1"}, {"code": "SUMMER2"}, {"code": "SUMMER3"}])
self.assertEqual(3, batch.codes_count)
- self.assertEqual('queued', batch.status)
+ self.assertEqual("queued", batch.status)
def test_find_batch_job(self):
- self.fake('price_rules/1213131/batch/989355119',
- method='GET',
- code=200,
- body=self.load_fixture('discount_code_creation'))
+ self.fake(
+ "price_rules/1213131/batch/989355119",
+ method="GET",
+ code=200,
+ body=self.load_fixture("discount_code_creation"),
+ )
batch = self.price_rule.find_batch(989355119)
self.assertEqual(3, batch.codes_count)
- self.assertEqual('queued', batch.status)
+ self.assertEqual("queued", batch.status)
diff --git a/test/product_listing_test.py b/test/product_listing_test.py
index e0726b9f..dcdc048d 100644
--- a/test/product_listing_test.py
+++ b/test/product_listing_test.py
@@ -1,10 +1,10 @@
import shopify
from test.test_helper import TestCase
-class ProductListingTest(TestCase):
+class ProductListingTest(TestCase):
def test_get_product_listings(self):
- self.fake('product_listings', method='GET', code=200, body=self.load_fixture('product_listings'))
+ self.fake("product_listings", method="GET", code=200, body=self.load_fixture("product_listings"))
product_listings = shopify.ProductListing.find()
self.assertEqual(2, len(product_listings))
@@ -14,13 +14,13 @@ def test_get_product_listings(self):
self.assertEqual("Rustic Copper Bottle", product_listings[1].title)
def test_get_product_listing(self):
- self.fake('product_listings/2', method='GET', code=200, body=self.load_fixture('product_listing'))
+ self.fake("product_listings/2", method="GET", code=200, body=self.load_fixture("product_listing"))
product_listing = shopify.ProductListing.find(2)
self.assertEqual("Synergistic Silk Chair", product_listing.title)
def test_reload_product_listing(self):
- self.fake('product_listings/2', method='GET', code=200, body=self.load_fixture('product_listing'))
+ self.fake("product_listings/2", method="GET", code=200, body=self.load_fixture("product_listing"))
product_listing = shopify.ProductListing()
product_listing.product_id = 2
@@ -29,7 +29,12 @@ def test_reload_product_listing(self):
self.assertEqual("Synergistic Silk Chair", product_listing.title)
def test_get_product_listing_product_ids(self):
- self.fake('product_listings/product_ids', method='GET', status = 200, body=self.load_fixture('product_listing_product_ids'))
+ self.fake(
+ "product_listings/product_ids",
+ method="GET",
+ status=200,
+ body=self.load_fixture("product_listing_product_ids"),
+ )
product_ids = shopify.ProductListing.product_ids()
diff --git a/test/product_publication_test.py b/test/product_publication_test.py
index 2a945bdb..671e7786 100644
--- a/test/product_publication_test.py
+++ b/test/product_publication_test.py
@@ -2,12 +2,11 @@
import json
from test.test_helper import TestCase
+
class ProductPublicationTest(TestCase):
def test_find_all_product_publications(self):
self.fake(
- 'publications/55650051/product_publications',
- method='GET',
- body= self.load_fixture('product_publications')
+ "publications/55650051/product_publications", method="GET", body=self.load_fixture("product_publications")
)
product_publications = shopify.ProductPublication.find(publication_id=55650051)
@@ -16,10 +15,10 @@ def test_find_all_product_publications(self):
def test_find_product_publication(self):
self.fake(
- 'publications/55650051/product_publications/647162527768',
- method='GET',
- body=self.load_fixture('product_publication'),
- code=200
+ "publications/55650051/product_publications/647162527768",
+ method="GET",
+ body=self.load_fixture("product_publication"),
+ code=200,
)
product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051)
@@ -28,25 +27,27 @@ def test_find_product_publication(self):
def test_create_product_publication(self):
self.fake(
- 'publications/55650051/product_publications',
- method='POST',
- headers={'Content-type': 'application/json'},
- body=self.load_fixture('product_publication'),
- code=201
+ "publications/55650051/product_publications",
+ method="POST",
+ headers={"Content-type": "application/json"},
+ body=self.load_fixture("product_publication"),
+ code=201,
)
- product_publication = shopify.ProductPublication.create({
- 'publication_id': 55650051,
- 'published_at': "2018-01-29T14:06:08-05:00",
- 'published': True,
- 'product_id': 8267093571
- })
+ product_publication = shopify.ProductPublication.create(
+ {
+ "publication_id": 55650051,
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "product_id": 8267093571,
+ }
+ )
expected_body = {
- 'product_publication': {
- 'published_at': "2018-01-29T14:06:08-05:00",
- 'published': True,
- 'product_id': 8267093571,
+ "product_publication": {
+ "published_at": "2018-01-29T14:06:08-05:00",
+ "published": True,
+ "product_id": 8267093571,
}
}
@@ -54,19 +55,14 @@ def test_create_product_publication(self):
def test_destroy_product_publication(self):
self.fake(
- 'publications/55650051/product_publications/647162527768',
- method='GET',
- body=self.load_fixture('product_publication'),
- code=200
+ "publications/55650051/product_publications/647162527768",
+ method="GET",
+ body=self.load_fixture("product_publication"),
+ code=200,
)
product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051)
- self.fake(
- 'publications/55650051/product_publications/647162527768',
- method='DELETE',
- body='{}',
- code=200
- )
+ self.fake("publications/55650051/product_publications/647162527768", method="DELETE", body="{}", code=200)
product_publication.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
diff --git a/test/product_test.py b/test/product_test.py
index dcc9ae72..de183691 100644
--- a/test/product_test.py
+++ b/test/product_test.py
@@ -1,18 +1,28 @@
import shopify
from test.test_helper import TestCase
-class ProductTest(TestCase):
+class ProductTest(TestCase):
def setUp(self):
super(ProductTest, self).setUp()
- self.fake("products/632910392", body=self.load_fixture('product'))
+ self.fake("products/632910392", body=self.load_fixture("product"))
self.product = shopify.Product.find(632910392)
def test_add_metafields_to_product(self):
- self.fake("products/632910392/metafields", method='POST', code=201, body=self.load_fixture('metafield'), headers={'Content-type': 'application/json'})
-
- field = self.product.add_metafield(shopify.Metafield({'namespace': "contact", 'key': "email", 'value': "123@example.com", 'value_type': "string"}))
+ self.fake(
+ "products/632910392/metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+
+ field = self.product.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
self.assertFalse(field.is_new())
self.assertEqual("contact", field.namespace)
@@ -20,7 +30,7 @@ def test_add_metafields_to_product(self):
self.assertEqual("123@example.com", field.value)
def test_get_metafields_for_product(self):
- self.fake("products/632910392/metafields", body=self.load_fixture('metafields'))
+ self.fake("products/632910392/metafields", body=self.load_fixture("metafields"))
metafields = self.product.metafields()
@@ -29,7 +39,7 @@ def test_get_metafields_for_product(self):
self.assertTrue(isinstance(field, shopify.Metafield))
def test_get_metafields_for_product_with_params(self):
- self.fake("products/632910392/metafields.json?limit=2", extension=False, body=self.load_fixture('metafields'))
+ self.fake("products/632910392/metafields.json?limit=2", extension=False, body=self.load_fixture("metafields"))
metafields = self.product.metafields(limit=2)
self.assertEqual(2, len(metafields))
@@ -37,26 +47,41 @@ def test_get_metafields_for_product_with_params(self):
self.assertTrue(isinstance(field, shopify.Metafield))
def test_get_metafields_for_product_count(self):
- self.fake("products/632910392/metafields/count", body=self.load_fixture('metafields_count'))
+ self.fake("products/632910392/metafields/count", body=self.load_fixture("metafields_count"))
metafields_count = self.product.metafields_count()
self.assertEqual(2, metafields_count)
def test_get_metafields_for_product_count_with_params(self):
- self.fake("products/632910392/metafields/count.json?value_type=string", extension=False, body=self.load_fixture('metafields_count'))
+ self.fake(
+ "products/632910392/metafields/count.json?value_type=string",
+ extension=False,
+ body=self.load_fixture("metafields_count"),
+ )
metafields_count = self.product.metafields_count(value_type="string")
self.assertEqual(2, metafields_count)
def test_update_loaded_variant(self):
- self.fake("products/632910392/variants/808950810", method='PUT', code=200, body=self.load_fixture('variant'))
+ self.fake("products/632910392/variants/808950810", method="PUT", code=200, body=self.load_fixture("variant"))
variant = self.product.variants[0]
variant.price = "0.50"
variant.save
def test_add_variant_to_product(self):
- self.fake("products/632910392/variants", method='POST', body=self.load_fixture('variant'), headers={'Content-type': 'application/json'})
- self.fake("products/632910392/variants/808950810", method='PUT', code=200, body=self.load_fixture('variant'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ self.fake(
+ "products/632910392/variants/808950810",
+ method="PUT",
+ code=200,
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
v = shopify.Variant()
self.assertTrue(self.product.add_variant(v))
diff --git a/test/publication_test.py b/test/publication_test.py
index aeee1a34..dab26fc5 100644
--- a/test/publication_test.py
+++ b/test/publication_test.py
@@ -1,9 +1,10 @@
import shopify
from test.test_helper import TestCase
+
class PublicationTest(TestCase):
def test_find_all_publications(self):
- self.fake('publications')
+ self.fake("publications")
publications = shopify.Publication.find()
self.assertEqual(55650051, publications[0].id)
diff --git a/test/recurring_charge_test.py b/test/recurring_charge_test.py
index 7d178c20..c785388e 100644
--- a/test/recurring_charge_test.py
+++ b/test/recurring_charge_test.py
@@ -1,11 +1,17 @@
import shopify
from test.test_helper import TestCase
+
class RecurringApplicationChargeTest(TestCase):
def test_activate_charge(self):
# Just check that calling activate doesn't raise an exception.
- self.fake("recurring_application_charges/35463/activate", method='POST',headers={'Content-length':'0', 'Content-type': 'application/json'}, body=" ")
- charge = shopify.RecurringApplicationCharge({'id': 35463})
+ self.fake(
+ "recurring_application_charges/35463/activate",
+ method="POST",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=" ",
+ )
+ charge = shopify.RecurringApplicationCharge({"id": 35463})
charge.activate()
def test_current_method_returns_active_charge(self):
@@ -26,7 +32,11 @@ def test_usage_charges_method_returns_associated_usage_charges(self):
self.fake("recurring_application_charges")
charge = shopify.RecurringApplicationCharge.current()
- self.fake("recurring_application_charges/455696195/usage_charges", method='GET', body=self.load_fixture('usage_charges'))
+ self.fake(
+ "recurring_application_charges/455696195/usage_charges",
+ method="GET",
+ body=self.load_fixture("usage_charges"),
+ )
usage_charges = charge.usage_charges()
self.assertEqual(len(usage_charges), 2)
@@ -35,13 +45,19 @@ def test_customize_method_increases_capped_amount(self):
charge = shopify.RecurringApplicationCharge.current()
self.assertEqual(charge.capped_amount, 100)
- self.fake("recurring_application_charges/455696195/customize.json?recurring_application_charge%5Bcapped_amount%5D=200", extension=False, method='PUT', headers={'Content-length':'0', 'Content-type': 'application/json'}, body=self.load_fixture('recurring_application_charge_adjustment'))
- charge.customize(capped_amount= 200)
+ self.fake(
+ "recurring_application_charges/455696195/customize.json?recurring_application_charge%5Bcapped_amount%5D=200",
+ extension=False,
+ method="PUT",
+ headers={"Content-length": "0", "Content-type": "application/json"},
+ body=self.load_fixture("recurring_application_charge_adjustment"),
+ )
+ charge.customize(capped_amount=200)
self.assertTrue(charge.update_capped_amount_url)
def test_destroy_recurring_application_charge(self):
- self.fake('recurring_application_charges')
+ self.fake("recurring_application_charges")
charge = shopify.RecurringApplicationCharge.current()
- self.fake('recurring_application_charges/455696195', method='DELETE', body='{}')
+ self.fake("recurring_application_charges/455696195", method="DELETE", body="{}")
charge.destroy()
diff --git a/test/refund_test.py b/test/refund_test.py
index 5750a1b0..905bbabc 100644
--- a/test/refund_test.py
+++ b/test/refund_test.py
@@ -1,10 +1,11 @@
import shopify
from test.test_helper import TestCase
+
class RefundTest(TestCase):
def setUp(self):
super(RefundTest, self).setUp()
- self.fake("orders/450789469/refunds/509562969", method='GET', body=self.load_fixture('refund'))
+ self.fake("orders/450789469/refunds/509562969", method="GET", body=self.load_fixture("refund"))
def test_should_find_a_specific_refund(self):
refund = shopify.Refund.find(509562969, order_id=450789469)
@@ -15,12 +16,11 @@ def test_calculate_refund_for_order(self):
"orders/450789469/refunds/calculate",
method="POST",
code=201,
- body=self.load_fixture('refund_calculate'),
- headers={'Content-type': 'application/json'},
+ body=self.load_fixture("refund_calculate"),
+ headers={"Content-type": "application/json"},
)
refund = shopify.Refund.calculate(
- order_id=450789469,
- refund_line_items=[{'line_item_id': 518995019, 'quantity': 1}]
+ order_id=450789469, refund_line_items=[{"line_item_id": 518995019, "quantity": 1}]
)
self.assertEqual("suggested_refund", refund.transactions[0].kind)
diff --git a/test/report_test.py b/test/report_test.py
index 4de7fd23..bfb66935 100644
--- a/test/report_test.py
+++ b/test/report_test.py
@@ -1,44 +1,36 @@
import shopify
from test.test_helper import TestCase
-class CustomerSavedSearchTest(TestCase):
+class CustomerSavedSearchTest(TestCase):
def test_get_report(self):
- self.fake('reports/987',
- method='GET',
- code=200,
- body=self.load_fixture('report'))
+ self.fake("reports/987", method="GET", code=200, body=self.load_fixture("report"))
report = shopify.Report.find(987)
self.assertEqual(987, report.id)
def test_get_reports(self):
- self.fake('reports',
- method='GET',
- code=200,
- body=self.load_fixture('reports'))
+ self.fake("reports", method="GET", code=200, body=self.load_fixture("reports"))
reports = shopify.Report.find()
- self.assertEqual('custom_app_reports', reports[0].category)
+ self.assertEqual("custom_app_reports", reports[0].category)
def test_create_report(self):
- self.fake('reports',
- method='POST',
- code=201,
- body=self.load_fixture('report'),
- headers={'Content-type': 'application/json'})
- report = shopify.Report.create({
- "name": "Custom App Report",
- "shopify_ql": "SHOW quantity_count, total_sales BY product_type, vendor, product_title FROM products SINCE -1m UNTIL -0m ORDER BY total_sales DESC"
- })
- self.assertEqual('custom_app_reports', report.category)
+ self.fake(
+ "reports",
+ method="POST",
+ code=201,
+ body=self.load_fixture("report"),
+ headers={"Content-type": "application/json"},
+ )
+ report = shopify.Report.create(
+ {
+ "name": "Custom App Report",
+ "shopify_ql": "SHOW quantity_count, total_sales BY product_type, vendor, product_title FROM products SINCE -1m UNTIL -0m ORDER BY total_sales DESC",
+ }
+ )
+ self.assertEqual("custom_app_reports", report.category)
def test_delete_report(self):
- self.fake('reports/987',
- method='GET',
- code=200,
- body=self.load_fixture('report'))
- self.fake('reports',
- method='DELETE',
- code=200,
- body='[]')
+ self.fake("reports/987", method="GET", code=200, body=self.load_fixture("report"))
+ self.fake("reports", method="DELETE", code=200, body="[]")
report = shopify.Report.find(987)
self.assertTrue(report.destroy)
diff --git a/test/resource_feedback_test.py b/test/resource_feedback_test.py
index 532bc5cb..47ee9f92 100644
--- a/test/resource_feedback_test.py
+++ b/test/resource_feedback_test.py
@@ -2,36 +2,39 @@
import shopify
from test.test_helper import TestCase
+
class ResourceFeedbackTest(TestCase):
def test_get_resource_feedback(self):
- body = json.dumps({ 'resource_feedback': [ { 'resource_type': 'Shop' } ] })
- self.fake('resource_feedback', method='GET', body=body)
+ body = json.dumps({"resource_feedback": [{"resource_type": "Shop"}]})
+ self.fake("resource_feedback", method="GET", body=body)
feedback = shopify.ResourceFeedback.find()
- self.assertEqual('Shop', feedback[0].resource_type)
+ self.assertEqual("Shop", feedback[0].resource_type)
def test_save_with_resource_feedback_endpoint(self):
- body = json.dumps({ 'resource_feedback': {} })
- self.fake('resource_feedback', method='POST', body=body, headers={ 'Content-Type': 'application/json' })
+ body = json.dumps({"resource_feedback": {}})
+ self.fake("resource_feedback", method="POST", body=body, headers={"Content-Type": "application/json"})
shopify.ResourceFeedback().save()
self.assertEqual(body, self.http.request.data.decode("utf-8"))
def test_get_resource_feedback_with_product_id(self):
- body = json.dumps({ 'resource_feedback': [ { 'resource_type': 'Product' } ] })
- self.fake('products/42/resource_feedback', method='GET', body=body)
+ body = json.dumps({"resource_feedback": [{"resource_type": "Product"}]})
+ self.fake("products/42/resource_feedback", method="GET", body=body)
feedback = shopify.ResourceFeedback.find(product_id=42)
- self.assertEqual('Product', feedback[0].resource_type)
+ self.assertEqual("Product", feedback[0].resource_type)
def test_save_with_product_id_resource_feedback_endpoint(self):
- body = json.dumps({ 'resource_feedback': {} })
- self.fake('products/42/resource_feedback', method='POST', body=body, headers={ 'Content-Type': 'application/json' })
+ body = json.dumps({"resource_feedback": {}})
+ self.fake(
+ "products/42/resource_feedback", method="POST", body=body, headers={"Content-Type": "application/json"}
+ )
- feedback = shopify.ResourceFeedback({'product_id':42})
+ feedback = shopify.ResourceFeedback({"product_id": 42})
feedback.save()
self.assertEqual(body, self.http.request.data.decode("utf-8"))
diff --git a/test/session_test.py b/test/session_test.py
index bf415916..8d73e293 100644
--- a/test/session_test.py
+++ b/test/session_test.py
@@ -6,41 +6,41 @@
from six.moves import urllib
from six import u
-class SessionTest(TestCase):
+class SessionTest(TestCase):
@classmethod
def setUpClass(self):
shopify.ApiVersion.define_known_versions()
- shopify.ApiVersion.define_version(shopify.Release('2019-04'))
+ shopify.ApiVersion.define_version(shopify.Release("2019-04"))
@classmethod
def tearDownClass(self):
shopify.ApiVersion.clear_defined_versions()
def test_not_be_valid_without_a_url(self):
- session = shopify.Session("", 'unstable', 'any-token')
+ session = shopify.Session("", "unstable", "any-token")
self.assertFalse(session.valid)
def test_not_be_valid_without_token(self):
- session = shopify.Session("testshop.myshopify.com", 'unstable')
+ session = shopify.Session("testshop.myshopify.com", "unstable")
self.assertFalse(session.valid)
def test_be_valid_with_any_token_and_any_url(self):
- session = shopify.Session("testshop.myshopify.com", 'unstable', "any-token")
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
self.assertTrue(session.valid)
def test_ignore_everything_but_the_subdomain_in_the_shop(self):
- session = shopify.Session("http://user:pass@testshop.notshopify.net/path", 'unstable', "any-token")
+ session = shopify.Session("http://user:pass@testshop.notshopify.net/path", "unstable", "any-token")
self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
def test_append_the_myshopify_domain_if_not_given(self):
- session = shopify.Session("testshop", 'unstable', "any-token")
+ session = shopify.Session("testshop", "unstable", "any-token")
self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
def test_raise_error_if_params_passed_but_signature_omitted(self):
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session("testshop.myshopify.com", 'unstable')
- token = session.request_token({'code':'any_code', 'foo': 'bar', 'timestamp':'1234'})
+ session = shopify.Session("testshop.myshopify.com", "unstable")
+ token = session.request_token({"code": "any_code", "foo": "bar", "timestamp": "1234"})
def test_setup_api_key_and_secret_for_all_sessions(self):
shopify.Session.setup(api_key="My test key", secret="My test secret")
@@ -48,31 +48,31 @@ def test_setup_api_key_and_secret_for_all_sessions(self):
self.assertEqual("My test secret", shopify.Session.secret)
def test_use_https_protocol_by_default_for_all_sessions(self):
- self.assertEqual('https', shopify.Session.protocol)
+ self.assertEqual("https", shopify.Session.protocol)
- def test_temp_reset_shopify_ShopifyResource_site_to_original_value(self):
+ def test_temp_reset_shopify_shopify_resource_site_to_original_value(self):
shopify.Session.setup(api_key="key", secret="secret")
- session1 = shopify.Session('fakeshop.myshopify.com', '2019-04', 'token1')
+ session1 = shopify.Session("fakeshop.myshopify.com", "2019-04", "token1")
shopify.ShopifyResource.activate_session(session1)
assigned_site = ""
- with shopify.Session.temp("testshop.myshopify.com", 'unstable', "any-token"):
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
assigned_site = shopify.ShopifyResource.site
- self.assertEqual('https://testshop.myshopify.com/admin/api/unstable', assigned_site)
- self.assertEqual('https://fakeshop.myshopify.com/admin/api/2019-04', shopify.ShopifyResource.site)
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", assigned_site)
+ self.assertEqual("https://fakeshop.myshopify.com/admin/api/2019-04", shopify.ShopifyResource.site)
def test_myshopify_domain_supports_non_standard_ports(self):
try:
shopify.Session.setup(api_key="key", secret="secret", myshopify_domain="localhost", port=3000)
- session = shopify.Session('fakeshop.localhost:3000', 'unstable', 'token1')
+ session = shopify.Session("fakeshop.localhost:3000", "unstable", "token1")
shopify.ShopifyResource.activate_session(session)
- self.assertEqual('https://fakeshop.localhost:3000/admin/api/unstable', shopify.ShopifyResource.site)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
- session = shopify.Session('fakeshop', 'unstable', 'token1')
+ session = shopify.Session("fakeshop", "unstable", "token1")
shopify.ShopifyResource.activate_session(session)
- self.assertEqual('https://fakeshop.localhost:3000/admin/api/unstable', shopify.ShopifyResource.site)
+ self.assertEqual("https://fakeshop.localhost:3000/admin/api/unstable", shopify.ShopifyResource.site)
finally:
shopify.Session.setup(myshopify_domain="myshopify.com", port=None)
@@ -80,156 +80,242 @@ def test_temp_works_without_currently_active_session(self):
shopify.ShopifyResource.clear_session()
assigned_site = ""
- with shopify.Session.temp("testshop.myshopify.com", 'unstable', 'any-token'):
+ with shopify.Session.temp("testshop.myshopify.com", "unstable", "any-token"):
assigned_site = shopify.ShopifyResource.site
- self.assertEqual('https://testshop.myshopify.com/admin/api/unstable', assigned_site)
- self.assertEqual('https://none/admin/api/unstable', shopify.ShopifyResource.site)
+ self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", assigned_site)
+ self.assertEqual("https://none/admin/api/unstable", shopify.ShopifyResource.site)
- def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri(self):
+ def test_create_permission_url_returns_correct_url_with_redirect_uri(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ permission_url = session.create_permission_url("my_redirect_uri.com")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = ["write_products"]
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products", self.normalize_url(permission_url))
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products",
+ self.normalize_url(permission_url),
+ )
- def test_create_permission_url_returns_correct_url_with_dual_scope_and_redirect_uri(self):
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_dual_scope(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
- scope = ["write_products","write_customers"]
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products%2Cwrite_customers", self.normalize_url(permission_url))
-
- def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_uri(self):
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ scope = ["write_products", "write_customers"]
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_products%2Cwrite_customers",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_empty_scope(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = []
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=", self.normalize_url(permission_url))
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope)
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com",
+ self.normalize_url(permission_url),
+ )
- def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_uri_and_state(self):
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_state(self):
+ shopify.Session.setup(api_key="My_test_key", secret="My test secret")
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ permission_url = session.create_permission_url("my_redirect_uri.com", state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
+ self.normalize_url(permission_url),
+ )
+
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_empty_scope_and_state(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = []
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=&state=mystate", self.normalize_url(permission_url))
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&state=mystate",
+ self.normalize_url(permission_url),
+ )
- def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri_and_state(self):
+ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_scope_and_state(self):
shopify.Session.setup(api_key="My_test_key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
scope = ["write_customers"]
- permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate")
- self.assertEqual("https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_customers&state=mystate", self.normalize_url(permission_url))
+ permission_url = session.create_permission_url("my_redirect_uri.com", scope=scope, state="mystate")
+ self.assertEqual(
+ "https://localhost.myshopify.com/admin/oauth/authorize?client_id=My_test_key&redirect_uri=my_redirect_uri.com&scope=write_customers&state=mystate",
+ self.normalize_url(permission_url),
+ )
def test_raise_exception_if_code_invalid_in_request_token(self):
shopify.Session.setup(api_key="My test key", secret="My test secret")
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
- self.fake(None, url='https://localhost.myshopify.com/admin/oauth/access_token', method='POST', code=404, body='{"error" : "invalid_request"}', has_user_agent=False)
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
+ self.fake(
+ None,
+ url="https://localhost.myshopify.com/admin/oauth/access_token",
+ method="POST",
+ code=404,
+ body='{"error" : "invalid_request"}',
+ has_user_agent=False,
+ )
with self.assertRaises(shopify.ValidationException):
- session.request_token({'code':'any-code', 'timestamp':'1234'})
+ session.request_token({"code": "any-code", "timestamp": "1234"})
self.assertFalse(session.valid)
def test_return_site_for_session(self):
- session = shopify.Session("testshop.myshopify.com", 'unstable', 'any-token')
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
self.assertEqual("https://testshop.myshopify.com/admin/api/unstable", session.site)
def test_hmac_calculation(self):
# Test using the secret and parameter examples given in the Shopify API documentation.
- shopify.Session.secret='hush'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'timestamp': '1337178173',
- 'hmac': '2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2',
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "timestamp": "1337178173",
+ "hmac": "2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2",
}
- self.assertEqual(shopify.Session.calculate_hmac(params), params['hmac'])
+ self.assertEqual(shopify.Session.calculate_hmac(params), params["hmac"])
def test_hmac_calculation_with_ampersand_and_equal_sign_characters(self):
- shopify.Session.secret='secret'
- params = { 'a': '1&b=2', 'c=3&d': '4' }
+ shopify.Session.secret = "secret"
+ params = {"a": "1&b=2", "c=3&d": "4"}
to_sign = "a=1%26b=2&c%3D3%26d=4"
- expected_hmac = hmac.new('secret'.encode(), to_sign.encode(), sha256).hexdigest()
+ expected_hmac = hmac.new("secret".encode(), to_sign.encode(), sha256).hexdigest()
self.assertEqual(shopify.Session.calculate_hmac(params), expected_hmac)
def test_hmac_validation(self):
# Test using the secret and parameter examples given in the Shopify API documentation.
- shopify.Session.secret='hush'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'timestamp': '1337178173',
- 'hmac': u('2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2'),
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "timestamp": "1337178173",
+ "hmac": u("2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"),
}
self.assertTrue(shopify.Session.validate_hmac(params))
def test_parameter_validation_handles_missing_params(self):
# Test using the secret and parameter examples given in the Shopify API documentation.
- shopify.Session.secret='hush'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'code': 'a94a110d86d2452eb3e2af4cfb8a3828',
- 'hmac': u('2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2'),
+ "shop": "some-shop.myshopify.com",
+ "code": "a94a110d86d2452eb3e2af4cfb8a3828",
+ "hmac": u("2cb1a277650a659f1b11e92a4a64275b128e037f2c3390e3c8fd2d8721dac9e2"),
}
self.assertFalse(shopify.Session.validate_params(params))
def test_param_validation_of_param_values_with_lists(self):
- shopify.Session.secret='hush'
+ shopify.Session.secret = "hush"
params = {
- 'shop': 'some-shop.myshopify.com',
- 'ids[]': [
+ "shop": "some-shop.myshopify.com",
+ "ids[]": [
2,
1,
],
- 'hmac': u('b93b9f82996f6f8bf9f1b7bbddec284c8fabacdc4e12dc80550b4705f3003b1e'),
+ "hmac": u("b93b9f82996f6f8bf9f1b7bbddec284c8fabacdc4e12dc80550b4705f3003b1e"),
}
self.assertEqual(True, shopify.Session.validate_hmac(params))
- def test_return_token_if_hmac_is_valid(self):
- shopify.Session.secret='secret'
- params = {'code': 'any-code', 'timestamp': time.time()}
+ def test_return_token_and_scope_if_hmac_is_valid(self):
+ shopify.Session.secret = "secret"
+ params = {"code": "any-code", "timestamp": time.time()}
hmac = shopify.Session.calculate_hmac(params)
- params['hmac'] = hmac
-
- self.fake(None, url='https://localhost.myshopify.com/admin/oauth/access_token', method='POST', body='{"access_token" : "token"}', has_user_agent=False)
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ params["hmac"] = hmac
+
+ self.fake(
+ None,
+ url="https://localhost.myshopify.com/admin/oauth/access_token",
+ method="POST",
+ body='{"access_token" : "token", "scope": "read_products,write_orders"}',
+ has_user_agent=False,
+ )
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
token = session.request_token(params)
self.assertEqual("token", token)
+ self.assertEqual(shopify.ApiAccess("read_products,write_orders"), session.access_scopes)
def test_raise_error_if_hmac_is_invalid(self):
- shopify.Session.secret='secret'
- params = {'code': 'any-code', 'timestamp': time.time()}
- params['hmac'] = 'a94a110d86d2452e92a4a64275b128e9273be3037f2c339eb3e2af4cfb8a3828'
+ shopify.Session.secret = "secret"
+ params = {"code": "any-code", "timestamp": time.time()}
+ params["hmac"] = "a94a110d86d2452e92a4a64275b128e9273be3037f2c339eb3e2af4cfb8a3828"
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
session = session.request_token(params)
def test_raise_error_if_hmac_does_not_match_expected(self):
- shopify.Session.secret='secret'
- params = {'foo': 'hello', 'timestamp': time.time()}
+ shopify.Session.secret = "secret"
+ params = {"foo": "hello", "timestamp": time.time()}
hmac = shopify.Session.calculate_hmac(params)
- params['hmac'] = hmac
- params['bar'] = 'world'
- params['code'] = 'code'
+ params["hmac"] = hmac
+ params["bar"] = "world"
+ params["code"] = "code"
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
session = session.request_token(params)
def test_raise_error_if_timestamp_is_too_old(self):
- shopify.Session.secret='secret'
+ shopify.Session.secret = "secret"
one_day = 24 * 60 * 60
- params = {'code': 'any-code', 'timestamp': time.time()-(2*one_day)}
+ params = {"code": "any-code", "timestamp": time.time() - (2 * one_day)}
hmac = shopify.Session.calculate_hmac(params)
- params['hmac'] = hmac
+ params["hmac"] = hmac
with self.assertRaises(shopify.ValidationException):
- session = shopify.Session('http://localhost.myshopify.com', 'unstable')
+ session = shopify.Session("http://localhost.myshopify.com", "unstable")
session = session.request_token(params)
+ def test_access_scopes_are_nil_by_default(self):
+ session = shopify.Session("testshop.myshopify.com", "unstable", "any-token")
+ self.assertIsNone(session.access_scopes)
+
+ def test_access_scopes_when_valid_scopes_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes="read_products, write_orders",
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
+ def test_access_scopes_set_with_api_access_object_passed_in(self):
+ session = shopify.Session(
+ shop_url="testshop.myshopify.com",
+ version="unstable",
+ token="any-token",
+ access_scopes=shopify.ApiAccess("read_products, write_orders"),
+ )
+
+ expected_access_scopes = shopify.ApiAccess("read_products, write_orders")
+ self.assertEqual(expected_access_scopes, session.access_scopes)
+
def normalize_url(self, url):
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
query = "&".join(sorted(query.split("&")))
return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
+
+ def test_session_with_coerced_version(self):
+ future_version = "2030-01"
+ session = shopify.Session("test.myshopify.com", future_version, "token")
+ self.assertEqual(session.api_version.name, future_version)
+ self.assertEqual(
+ session.api_version.api_path("https://test.myshopify.com"),
+ f"https://test.myshopify.com/admin/api/{future_version}",
+ )
+
+ def test_session_with_invalid_version(self):
+ with self.assertRaises(shopify.VersionNotFoundError):
+ shopify.Session("test.myshopify.com", "invalid-version", "token")
diff --git a/test/session_token_test.py b/test/session_token_test.py
new file mode 100644
index 00000000..0df7147f
--- /dev/null
+++ b/test/session_token_test.py
@@ -0,0 +1,110 @@
+from shopify import session_token
+from test.test_helper import TestCase
+from datetime import datetime, timedelta
+
+import jwt
+import sys
+
+if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
+ import time
+
+
+def timestamp(date):
+ return time.mktime(date.timetuple()) if sys.version_info[0] < 3 else date.timestamp()
+
+
+class TestSessionTokenGetDecodedSessionToken(TestCase):
+ @classmethod
+ def setUpClass(self):
+ self.secret = "API Secret"
+ self.api_key = "API key"
+
+ @classmethod
+ def setUp(self):
+ current_time = datetime.now()
+ self.payload = {
+ "iss": "https://test-shop.myshopify.com/admin",
+ "dest": "https://test-shop.myshopify.com",
+ "aud": self.api_key,
+ "sub": "1",
+ "exp": timestamp((current_time + timedelta(0, 60))),
+ "nbf": timestamp(current_time),
+ "iat": timestamp(current_time),
+ "jti": "4321",
+ "sid": "abc123",
+ }
+
+ @classmethod
+ def build_auth_header(self):
+ mock_session_token = jwt.encode(self.payload, self.secret, algorithm="HS256")
+ return "Bearer {session_token}".format(session_token=mock_session_token)
+
+ def test_raises_if_token_authentication_header_is_not_bearer(self):
+ authorization_header = "Bad auth header"
+
+ with self.assertRaises(session_token.TokenAuthenticationError) as cm:
+ session_token.decode_from_header(authorization_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token", str(cm.exception))
+
+ def test_raises_jwt_error_if_session_token_is_expired(self):
+ self.payload["exp"] = timestamp((datetime.now() + timedelta(0, -11)))
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature has expired", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_alg(self):
+ bad_session_token = jwt.encode(self.payload, None, algorithm="none")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The specified alg value is not allowed", str(cm.exception))
+
+ def test_raises_jwt_error_if_invalid_signature(self):
+ bad_session_token = jwt.encode(self.payload, "bad_secret", algorithm="HS256")
+ invalid_header = "Bearer {session_token}".format(session_token=bad_session_token)
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(invalid_header, api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Signature verification failed", str(cm.exception))
+
+ def test_raises_if_aud_doesnt_match_api_key(self):
+ self.payload["aud"] = "bad audience"
+
+ with self.assertRaises(session_token.SessionTokenError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Audience doesn't match", str(cm.exception))
+
+ def test_raises_if_issuer_hostname_is_invalid(self):
+ self.payload["iss"] = "bad_shop_hostname"
+
+ with self.assertRaises(session_token.InvalidIssuerError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("Invalid issuer", str(cm.exception))
+
+ def test_raises_if_iss_and_dest_dont_match(self):
+ self.payload["dest"] = "bad_shop.myshopify.com"
+
+ with self.assertRaises(session_token.MismatchedHostsError) as cm:
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
+
+ self.assertEqual("The issuer and destination do not match", str(cm.exception))
+
+ def test_returns_decoded_payload(self):
+ decoded_payload = session_token.decode_from_header(
+ self.build_auth_header(), api_key=self.api_key, secret=self.secret
+ )
+
+ self.assertEqual(self.payload, decoded_payload)
+
+ def test_allow_10_seconds_clock_skew_in_nbf(self):
+ self.payload["nbf"] = timestamp((datetime.now() + timedelta(seconds=10)))
+
+ session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py
index e81cfe6a..3d1e1e4d 100644
--- a/test/shipping_zone_test.py
+++ b/test/shipping_zone_test.py
@@ -1,11 +1,11 @@
import shopify
from test.test_helper import TestCase
+
class ShippingZoneTest(TestCase):
def test_get_shipping_zones(self):
- self.fake("shipping_zones", method='GET', body=self.load_fixture('shipping_zones'))
+ self.fake("shipping_zones", method="GET", body=self.load_fixture("shipping_zones"))
shipping_zones = shopify.ShippingZone.find()
- self.assertEqual(1,len(shipping_zones))
- self.assertEqual(shipping_zones[0].name,"Some zone")
- self.assertEqual(3,len(shipping_zones[0].countries))
-
+ self.assertEqual(1, len(shipping_zones))
+ self.assertEqual(shipping_zones[0].name, "Some zone")
+ self.assertEqual(3, len(shipping_zones[0].countries))
diff --git a/test/shop_test.py b/test/shop_test.py
index 2f02a632..3a88a2a6 100644
--- a/test/shop_test.py
+++ b/test/shop_test.py
@@ -1,6 +1,7 @@
import shopify
from test.test_helper import TestCase
+
class ShopTest(TestCase):
def setUp(self):
super(ShopTest, self).setUp()
@@ -8,7 +9,7 @@ def setUp(self):
self.shop = shopify.Shop.current()
def test_current_should_return_current_shop(self):
- self.assertTrue(isinstance(self.shop,shopify.Shop))
+ self.assertTrue(isinstance(self.shop, shopify.Shop))
self.assertEqual("Apple Computers", self.shop.name)
self.assertEqual("apple.myshopify.com", self.shop.myshopify_domain)
self.assertEqual(690933842, self.shop.id)
@@ -25,9 +26,19 @@ def test_get_metafields_for_shop(self):
self.assertTrue(isinstance(field, shopify.Metafield))
def test_add_metafield(self):
- self.fake("metafields", method='POST', code=201, body=self.load_fixture('metafield'), headers={'Content-type': 'application/json'})
-
- field = self.shop.add_metafield( shopify.Metafield({'namespace': "contact", 'key': "email", 'value': "123@example.com", 'value_type': "string"}))
+ self.fake(
+ "metafields",
+ method="POST",
+ code=201,
+ body=self.load_fixture("metafield"),
+ headers={"Content-type": "application/json"},
+ )
+
+ field = self.shop.add_metafield(
+ shopify.Metafield(
+ {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"}
+ )
+ )
self.assertFalse(field.is_new())
self.assertEqual("contact", field.namespace)
diff --git a/test/storefront_access_token_test.py b/test/storefront_access_token_test.py
index c3bfb08e..ce5ef805 100644
--- a/test/storefront_access_token_test.py
+++ b/test/storefront_access_token_test.py
@@ -1,23 +1,33 @@
import shopify
from test.test_helper import TestCase
+
class StorefrontAccessTokenTest(TestCase):
def test_create_storefront_access_token(self):
- self.fake('storefront_access_tokens', method='POST', body=self.load_fixture('storefront_access_token'), headers={ 'Content-type': 'application/json' })
- storefront_access_token = shopify.StorefrontAccessToken.create({'title': 'Test'})
+ self.fake(
+ "storefront_access_tokens",
+ method="POST",
+ body=self.load_fixture("storefront_access_token"),
+ headers={"Content-type": "application/json"},
+ )
+ storefront_access_token = shopify.StorefrontAccessToken.create({"title": "Test"})
self.assertEqual(1, storefront_access_token.id)
self.assertEqual("Test", storefront_access_token.title)
def test_get_and_delete_storefront_access_token(self):
- self.fake('storefront_access_tokens/1', method='GET', code=200, body=self.load_fixture('storefront_access_token'))
+ self.fake(
+ "storefront_access_tokens/1", method="GET", code=200, body=self.load_fixture("storefront_access_token")
+ )
storefront_access_token = shopify.StorefrontAccessToken.find(1)
- self.fake('storefront_access_tokens/1', method='DELETE', code=200, body='destroyed')
+ self.fake("storefront_access_tokens/1", method="DELETE", code=200, body="destroyed")
storefront_access_token.destroy()
- self.assertEqual('DELETE', self.http.request.get_method())
+ self.assertEqual("DELETE", self.http.request.get_method())
def test_get_storefront_access_tokens(self):
- self.fake('storefront_access_tokens', method='GET', code=200, body=self.load_fixture('storefront_access_tokens'))
+ self.fake(
+ "storefront_access_tokens", method="GET", code=200, body=self.load_fixture("storefront_access_tokens")
+ )
tokens = shopify.StorefrontAccessToken.find()
self.assertEqual(2, len(tokens))
diff --git a/test/tender_transaction_test.py b/test/tender_transaction_test.py
index 9336196b..fe73c633 100644
--- a/test/tender_transaction_test.py
+++ b/test/tender_transaction_test.py
@@ -1,10 +1,11 @@
import shopify
from test.test_helper import TestCase
+
class TenderTransactionTest(TestCase):
def setUp(self):
super(TenderTransactionTest, self).setUp()
- self.fake("tender_transactions", method='GET', body=self.load_fixture('tender_transactions'))
+ self.fake("tender_transactions", method="GET", body=self.load_fixture("tender_transactions"))
def test_should_load_all_tender_transactions(self):
tender_transactions = shopify.TenderTransaction.find()
diff --git a/test/test_helper.py b/test/test_helper.py
index 7dc8644f..666ac792 100644
--- a/test/test_helper.py
+++ b/test/test_helper.py
@@ -5,11 +5,11 @@
from pyactiveresource.testing import http_fake
import shopify
-class TestCase(unittest.TestCase):
+class TestCase(unittest.TestCase):
def setUp(self):
ActiveResource.site = None
- ActiveResource.headers=None
+ ActiveResource.headers = None
shopify.ShopifyResource.clear_session()
shopify.ShopifyResource.site = "https://this-is-my-test-show.myshopify.com/admin/api/unstable"
@@ -18,47 +18,42 @@ def setUp(self):
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'
- self.fake('apis',
- url='https://app.shopify.com/services/apis.json',
- method='GET',
- code=200,
- response_headers={'Content-type': 'application/json'},
- body=self.load_fixture('api_version'),
- has_user_agent=False
- )
+ 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:
+ 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')
+ 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']):
+ if "extension" in kwargs and not kwargs["extension"]:
extension = ""
else:
- extension = ".%s" % (kwargs.pop('extension', 'json'))
- if kwargs.get('url'):
- url = kwargs.get('url')
- else:
- url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension)
+ 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
+ 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'])
+ headers.update(kwargs["headers"])
except KeyError:
- pass
+ pass
- code = kwargs.pop('code', 200)
+ code = kwargs.pop("code", 200)
self.http.respond_to(
- method, url, headers, body=body, code=code,
- response_headers=kwargs.pop('response_headers', None))
+ method, url, headers, body=body, code=code, response_headers=kwargs.pop("response_headers", None)
+ )
diff --git a/test/transaction_test.py b/test/transaction_test.py
index 79335eee..e02fae00 100644
--- a/test/transaction_test.py
+++ b/test/transaction_test.py
@@ -1,10 +1,11 @@
import shopify
from test.test_helper import TestCase
+
class TransactionTest(TestCase):
def setUp(self):
super(TransactionTest, self).setUp()
- self.fake("orders/450789469/transactions/389404469", method='GET', body=self.load_fixture('transaction'))
+ self.fake("orders/450789469/transactions/389404469", method="GET", body=self.load_fixture("transaction"))
def test_should_find_a_specific_transaction(self):
transaction = shopify.Transaction.find(389404469, order_id=450789469)
diff --git a/test/transactions_test.py b/test/transactions_test.py
index 62044238..9b0d54b7 100644
--- a/test/transactions_test.py
+++ b/test/transactions_test.py
@@ -3,10 +3,9 @@
class TransactionsTest(TestCase):
- prefix = '/admin/api/unstable/shopify_payments/balance'
+ prefix = "/admin/api/unstable/shopify_payments/balance"
def test_get_payouts_transactions(self):
- self.fake('transactions', method='GET', prefix=self.prefix,
- body=self.load_fixture('payouts_transactions'))
+ self.fake("transactions", method="GET", prefix=self.prefix, body=self.load_fixture("payouts_transactions"))
transactions = shopify.Transactions.find()
self.assertGreater(len(transactions), 0)
diff --git a/test/usage_charge_test.py b/test/usage_charge_test.py
index ab341fb3..a816636b 100644
--- a/test/usage_charge_test.py
+++ b/test/usage_charge_test.py
@@ -1,16 +1,28 @@
import shopify
from test.test_helper import TestCase
+
class UsageChargeTest(TestCase):
def test_create_usage_charge(self):
- self.fake("recurring_application_charges/654381177/usage_charges", method='POST', body=self.load_fixture('usage_charge'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "recurring_application_charges/654381177/usage_charges",
+ method="POST",
+ body=self.load_fixture("usage_charge"),
+ headers={"Content-type": "application/json"},
+ )
- charge = shopify.UsageCharge({'price': 9.0, 'description': '1000 emails', 'recurring_application_charge_id': 654381177})
+ charge = shopify.UsageCharge(
+ {"price": 9.0, "description": "1000 emails", "recurring_application_charge_id": 654381177}
+ )
charge.save()
- self.assertEqual('1000 emails', charge.description)
+ self.assertEqual("1000 emails", charge.description)
def test_get_usage_charge(self):
- self.fake("recurring_application_charges/654381177/usage_charges/359376002", method='GET', body=self.load_fixture('usage_charge'))
+ self.fake(
+ "recurring_application_charges/654381177/usage_charges/359376002",
+ method="GET",
+ body=self.load_fixture("usage_charge"),
+ )
- charge = shopify.UsageCharge.find(359376002, recurring_application_charge_id= 654381177)
- self.assertEqual('1000 emails', charge.description)
+ charge = shopify.UsageCharge.find(359376002, recurring_application_charge_id=654381177)
+ self.assertEqual("1000 emails", charge.description)
diff --git a/test/user_test.py b/test/user_test.py
index 8b1bb3a8..efb90d30 100644
--- a/test/user_test.py
+++ b/test/user_test.py
@@ -1,10 +1,10 @@
import shopify
from test.test_helper import TestCase
-class UserTest(TestCase):
+class UserTest(TestCase):
def test_get_all_users(self):
- self.fake('users', body=self.load_fixture('users'))
+ self.fake("users", body=self.load_fixture("users"))
users = shopify.User.find()
self.assertEqual(2, len(users))
@@ -12,14 +12,14 @@ def test_get_all_users(self):
self.assertEqual("Jobs", users[0].last_name)
def test_get_user(self):
- self.fake('users/799407056', body=self.load_fixture('user'))
+ self.fake("users/799407056", body=self.load_fixture("user"))
user = shopify.User.find(799407056)
self.assertEqual("Steve", user.first_name)
self.assertEqual("Jobs", user.last_name)
def test_get_current_user(self):
- self.fake('users/current', body=self.load_fixture('user'))
+ self.fake("users/current", body=self.load_fixture("user"))
user = shopify.User.current()
self.assertEqual("Steve", user.first_name)
diff --git a/test/utils/shop_url_test.py b/test/utils/shop_url_test.py
new file mode 100644
index 00000000..ea7a77e9
--- /dev/null
+++ b/test/utils/shop_url_test.py
@@ -0,0 +1,47 @@
+from shopify.utils import shop_url
+from test.test_helper import TestCase
+
+
+class TestSanitizeShopDomain(TestCase):
+ def test_returns_hostname_for_good_shop_domains(self):
+ good_shop_domains = [
+ "my-shop",
+ "my-shop.myshopify.com",
+ "http://my-shop.myshopify.com",
+ "https://my-shop.myshopify.com",
+ ]
+ sanitized_shops = [shop_url.sanitize_shop_domain(shop_domain) for shop_domain in good_shop_domains]
+
+ self.assertTrue(all(shop == "my-shop.myshopify.com" for shop in sanitized_shops))
+
+ def test_returns_none_for_bad_shop_domains(self):
+ bad_shop_domains = [
+ "myshop.com",
+ "myshopify.com",
+ "shopify.com",
+ "two words",
+ "store.myshopify.com.evil.com",
+ "/foo/bar",
+ "/foo.myshopify.io.evil.ru",
+ "%0a123.myshopify.io ",
+ "foo.bar.myshopify.io",
+ ]
+ sanitized_shops = [shop_url.sanitize_shop_domain(shop_domain) for shop_domain in bad_shop_domains]
+
+ self.assertTrue(all(shop_domain is None for shop_domain in sanitized_shops))
+
+ def test_returns_hostname_for_custom_shop_domains(self):
+ custom_shop_domains = [
+ "my-shop",
+ "my-shop.myshopify.io",
+ "http://my-shop.myshopify.io",
+ "https://my-shop.myshopify.io",
+ ]
+ sanitized_shops = [
+ shop_url.sanitize_shop_domain(shop_domain, "myshopify.io") for shop_domain in custom_shop_domains
+ ]
+
+ self.assertTrue(all(shop == "my-shop.myshopify.io" for shop in sanitized_shops))
+
+ def test_returns_none_for_none_type(self):
+ self.assertIsNone(shop_url.sanitize_shop_domain(None))
diff --git a/test/variant_test.py b/test/variant_test.py
index ebfdd8c0..63ecb639 100644
--- a/test/variant_test.py
+++ b/test/variant_test.py
@@ -1,34 +1,49 @@
import shopify
from test.test_helper import TestCase
-class VariantTest(TestCase):
+class VariantTest(TestCase):
def test_get_variants(self):
- self.fake("products/632910392/variants", method='GET', body=self.load_fixture('variants'))
- v = shopify.Variant.find(product_id = 632910392)
+ self.fake("products/632910392/variants", method="GET", body=self.load_fixture("variants"))
+ v = shopify.Variant.find(product_id=632910392)
def test_get_variant_namespaced(self):
- self.fake("products/632910392/variants/808950810", method='GET', body=self.load_fixture('variant'))
- v = shopify.Variant.find(808950810, product_id = 632910392)
+ self.fake("products/632910392/variants/808950810", method="GET", body=self.load_fixture("variant"))
+ v = shopify.Variant.find(808950810, product_id=632910392)
def test_update_variant_namespace(self):
- self.fake("products/632910392/variants/808950810", method='GET', body=self.load_fixture('variant'))
- v = shopify.Variant.find(808950810, product_id = 632910392)
+ self.fake("products/632910392/variants/808950810", method="GET", body=self.load_fixture("variant"))
+ v = shopify.Variant.find(808950810, product_id=632910392)
- self.fake("products/632910392/variants/808950810", method='PUT', body=self.load_fixture('variant'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "products/632910392/variants/808950810",
+ method="PUT",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
v.save()
def test_create_variant(self):
- self.fake("products/632910392/variants", method='POST', body=self.load_fixture('variant'), headers={'Content-type': 'application/json'})
- v = shopify.Variant({'product_id':632910392})
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
+ v = shopify.Variant({"product_id": 632910392})
v.save()
def test_create_variant_then_add_parent_id(self):
- self.fake("products/632910392/variants", method='POST', body=self.load_fixture('variant'), headers={'Content-type': 'application/json'})
+ self.fake(
+ "products/632910392/variants",
+ method="POST",
+ body=self.load_fixture("variant"),
+ headers={"Content-type": "application/json"},
+ )
v = shopify.Variant()
v.product_id = 632910392
v.save()
-
+
def test_get_variant(self):
- self.fake("variants/808950810", method='GET', body=self.load_fixture('variant'))
+ self.fake("variants/808950810", method="GET", body=self.load_fixture("variant"))
v = shopify.Variant.find(808950810)
diff --git a/tox.ini b/tox.ini
index 47520dac..1523475c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py27, py34, py35, py36
+envlist = py27, py34, py35, py36, py38, py39
skip_missing_interpreters = true
[testenv]