From f8925071a5297b85ca3517ef8c81b880df75162f Mon Sep 17 00:00:00 2001 From: ShayneP Date: Thu, 6 Jun 2019 16:06:48 -0400 Subject: [PATCH 001/259] GraphQL resource in shopify_python_api --- CHANGELOG | 3 +++ shopify/resources/__init__.py | 1 + shopify/resources/graphql.py | 15 +++++++++++++++ shopify/version.py | 2 +- 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 shopify/resources/graphql.py diff --git a/CHANGELOG b/CHANGELOG index edad087e..62fc516d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 5.1.0 +* Added support for GraphQL queries + == Version 5.0.1 * Fixing missing class variable causing exception when creating a session without a token diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 010ed440..8233cb1f 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -71,5 +71,6 @@ from .publication import Publication from .collection_publication import CollectionPublication from .product_publication import ProductPublication +from .graphql import GraphQL from ..base import ShopifyResource diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py new file mode 100644 index 00000000..78d1eba0 --- /dev/null +++ b/shopify/resources/graphql.py @@ -0,0 +1,15 @@ +from ..base import ShopifyResource +from graphqlclient import GraphQLClient +import ast + +class GraphQL(ShopifyResource): + + @classmethod + def execute(cls, query): + client = GraphQLClient(cls.site + "/graphql.json") + client.inject_token( + cls.headers['X-Shopify-Access-Token'], + 'X-Shopify-Access-Token' + ) + result = client.execute(query) + return ast.literal_eval(result) \ No newline at end of file diff --git a/shopify/version.py b/shopify/version.py index 96b4b92a..ab6719c4 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '5.0.1' +VERSION = '5.1.0' From 6fd6e2e1be16b91df2b0ac3c72e510df36a90fa2 Mon Sep 17 00:00:00 2001 From: ShayneP Date: Fri, 7 Jun 2019 00:17:01 -0400 Subject: [PATCH 002/259] Add docs + tests --- README.md | 18 ++++++++++++++ shopify/resources/graphql.py | 46 +++++++++++++++++++++++++----------- test/fixtures/graphql.json | 27 +++++++++++++++++++++ test/graphql_test.py | 34 ++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 test/fixtures/graphql.json create mode 100644 test/graphql_test.py diff --git a/README.md b/README.md index 98b901a4..566e5a30 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,24 @@ open up an interactive console to use the API with a shop. shopify_api.py help ``` +### GraphQL + +This library also supports Shopify's new [GraphQL API](https://help.shopify.com/en/api/graphql-admin-api) via a dependency on the [python-graphql-client](https://github.com/prisma/python-graphql-client) library. The authentication process (steps 1-5 under Getting Started) is identical. Once your session is activated, simply construct a new graphql client and use `execute` to execute the query. + +``` +client = shopify.GraphQL() + query = ''' + { + shop { + name + id + } + } + ''' + result = client.execute(query) +``` + + ## Using Development Version The development version can be built using diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py index 78d1eba0..b518be31 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -1,15 +1,33 @@ +import shopify from ..base import ShopifyResource -from graphqlclient import GraphQLClient -import ast - -class GraphQL(ShopifyResource): - - @classmethod - def execute(cls, query): - client = GraphQLClient(cls.site + "/graphql.json") - client.inject_token( - cls.headers['X-Shopify-Access-Token'], - 'X-Shopify-Access-Token' - ) - result = client.execute(query) - return ast.literal_eval(result) \ No newline at end of file +from six.moves import urllib +import json + +class GraphQL(): + + def __init__(self): + self.endpoint = (shopify.ShopifyResource.get_site() + "/graphql.json") + self.headers = shopify.ShopifyResource.get_headers() + + def merge_headers(self, *headers): + merged_headers = {} + for header in headers: + merged_headers.update(header) + return merged_headers + + def execute(self, query, variables=None): + endpoint = self.endpoint + default_headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} + headers = self.merge_headers(default_headers, self.headers) + data = {'query': query, + 'variables': variables} + + req = urllib.request.Request(self.endpoint, json.dumps(data).encode('utf-8'), headers) + + try: + response = urllib.request.urlopen(req) + return response.read().decode('utf-8') + except urllib.error.HTTPError as e: + print((e.read())) + print('') + raise e diff --git a/test/fixtures/graphql.json b/test/fixtures/graphql.json new file mode 100644 index 00000000..27310115 --- /dev/null +++ b/test/fixtures/graphql.json @@ -0,0 +1,27 @@ +{ + "shop": { + "name": "Apple Computers", + "city": "Cupertino", + "address1": "1 Infinite Loop", + "zip": "95014", + "created_at": "2007-12-31T19:00:00-05:00", + "shop_owner": "Steve Jobs", + "plan_name": "enterprise", + "public": false, + "country": "US", + "money_with_currency_format": "$ {{amount}} USD", + "money_format": "$ {{amount}}", + "domain": "shop.apple.com", + "taxes_included": null, + "id": 690933842, + "timezone": "(GMT-05:00) Eastern Time (US & Canada)", + "tax_shipping": null, + "phone": null, + "currency": "USD", + "myshopify_domain": "apple.myshopify.com", + "source": null, + "province": "CA", + "email": "steve@apple.com" + } +} + diff --git a/test/graphql_test.py b/test/graphql_test.py new file mode 100644 index 00000000..e6115f46 --- /dev/null +++ b/test/graphql_test.py @@ -0,0 +1,34 @@ +import shopify +import json +from test.test_helper import 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.ShopifyResource.activate_session(shopify_session) + client = shopify.GraphQL() + self.fake( + 'graphql', + method='POST', + code=201, + headers={ + 'X-Shopify-Access-Token': 'token', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }) + query = ''' + { + shop { + name + id + } + } + ''' + self.result = client.execute(query) + + + def test_fetch_shop_with_graphql(self): + self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers') \ No newline at end of file From 6fcc1bd15bfaaacdb0ee2999626f102f63e0d1b7 Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Wed, 12 Jun 2019 15:00:44 -0400 Subject: [PATCH 003/259] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 566e5a30..49596650 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ open up an interactive console to use the API with a shop. ### GraphQL -This library also supports Shopify's new [GraphQL API](https://help.shopify.com/en/api/graphql-admin-api) via a dependency on the [python-graphql-client](https://github.com/prisma/python-graphql-client) library. The authentication process (steps 1-5 under Getting Started) is identical. Once your session is activated, simply construct a new graphql client and use `execute` to execute the query. +This library also supports Shopify's new [GraphQL API](https://help.shopify.com/en/api/graphql-admin-api). The authentication process (steps 1-5 under Getting Started) is identical. Once your session is activated, simply construct a new graphql client and use `execute` to execute the query. ``` client = shopify.GraphQL() From 748fc93d09a3e4467cd0e605a855b84cf029f415 Mon Sep 17 00:00:00 2001 From: Shayne Parmelee Date: Thu, 13 Jun 2019 12:51:20 -0400 Subject: [PATCH 004/259] v5.1.0 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 62fc516d..cd84c684 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ == Version 5.1.0 -* Added support for GraphQL queries +* 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 From b2ba2c30dd7532c9804a9175d051ff5baa6f8d62 Mon Sep 17 00:00:00 2001 From: Tyler Ball Date: Tue, 25 Jun 2019 15:24:31 -0400 Subject: [PATCH 005/259] Fix setting site with basic auth URL --- README.md | 10 ++++++---- shopify/base.py | 5 +++++ test/base_test.py | 11 ++++++++--- test/session_test.py | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 49596650..45111bc4 100644 --- a/README.md +++ b/README.md @@ -85,15 +85,17 @@ service. pyactiveresource has to be configured with a fully authorized URL of a particular store first. To obtain that URL you can follow these steps: -1. First create a new application in either the partners admin or - your store admin. For a private App you'll need the API_KEY and - the PASSWORD otherwise you'll need the API_KEY and SHARED_SECRET. +1. First create a new application in either the partners admin or your store + admin. You will need an API_VERSION equal to a valid version string of a + [Shopify API Version](https://help.shopify.com/en/api/versioning). For a + private App you'll need the API_KEY and the PASSWORD otherwise you'll need + the API_KEY and SHARED_SECRET. 2. For a private App you just need to set the base site url as follows: ```python - shop_url = "https://%s:%s@SHOP_NAME.myshopify.com/admin" % (API_KEY, PASSWORD) + shop_url = "https://%s:%s@SHOP_NAME.myshopify.com/admin/api/%s" % (API_KEY, PASSWORD, API_VERSION) shopify.ShopifyResource.set_site(shop_url) ``` diff --git a/shopify/base.py b/shopify/base.py index c9021529..6cfee0ea 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -77,6 +77,11 @@ def set_site(cls, value): ShopifyResource._site = cls._threadlocal.site = value if value is not None: parts = urllib.parse.urlparse(value) + host = parts.hostname + if parts.port: + host += ":" + str(parts.port) + new_site = urllib.parse.urlunparse((parts.scheme, host, parts.path, '', '', '')) + ShopifyResource._site = cls._threadlocal.site = new_site if parts.username: cls.user = urllib.parse.unquote(parts.username) if parts.password: diff --git a/test/base_test.py b/test/base_test.py index 9e78bdbc..88478570 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -98,13 +98,18 @@ def testFunc(): t2.start() t2.join() - def test_setting_site_without_token(self): + def test_setting_with_user_and_pass_strips_them(self): + shopify.ShopifyResource.clear_session() self.fake( 'shop', - url='https://user:pass@this-is-my-test-show.myshopify.com/admin/api/unstable/shop.json', + url='https://this-is-my-test-show.myshopify.com/admin/shop.json', method='GET', body=self.load_fixture('shop'), headers={'Authorization': u'Basic dXNlcjpwYXNz'} ) - shopify.ShopifyResource.set_site('https://user:pass@this-is-my-test-show.myshopify.com/admin/api/unstable') + 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) diff --git a/test/session_test.py b/test/session_test.py index d76c0f97..bf415916 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -84,7 +84,7 @@ def test_temp_works_without_currently_active_session(self): 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://none/admin/api/unstable', shopify.ShopifyResource.site) def test_create_permission_url_returns_correct_url_with_single_scope_and_redirect_uri(self): shopify.Session.setup(api_key="My_test_key", secret="My test secret") From 5c26003f715496c3bd4684a97239ee363c43c30e Mon Sep 17 00:00:00 2001 From: Tyler Ball Date: Wed, 26 Jun 2019 11:46:12 -0400 Subject: [PATCH 006/259] add shipit.yml --- shipit.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 shipit.yml diff --git a/shipit.yml b/shipit.yml new file mode 100644 index 00000000..a024f7af --- /dev/null +++ b/shipit.yml @@ -0,0 +1,9 @@ +dependencies: + pre: + - pip install twine + +deploy: + override: + - assert-egg-version-tag setup.py + - python setup.py register sdist + - twine upload dist/* From 586af8542e2a4ff64f0c39d73b352a01373f6e37 Mon Sep 17 00:00:00 2001 From: Tyler Ball Date: Wed, 26 Jun 2019 11:47:43 -0400 Subject: [PATCH 007/259] v5.1.1 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cd84c684..d0878e29 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 5.1.1 +* Fix initializing API with basic auth URL. + == Version 5.1.0 * Added support for GraphQL queries with a GraphQL resource diff --git a/shopify/version.py b/shopify/version.py index ab6719c4..c765b0cd 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '5.1.0' +VERSION = '5.1.1' From 61ad0ff4c129d772951dfac7bf55236a53a9e386 Mon Sep 17 00:00:00 2001 From: GhostApps Date: Wed, 14 Aug 2019 13:59:56 +0100 Subject: [PATCH 008/259] Add 2019-07 and 2019-10 API versions --- shopify/api_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index b48f94df..1f96a5e4 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -26,6 +26,8 @@ def define_version(cls, version): def define_known_versions(cls): cls.define_version(Unstable()) cls.define_version(Release('2019-04')) + cls.define_version(Release('2019-07')) + cls.define_version(Release('2019-10')) @classmethod def clear_defined_versions(cls): From 73b1d56f1e5d12251ad238605001c71f5ef46ae9 Mon Sep 17 00:00:00 2001 From: GhostApps Date: Wed, 14 Aug 2019 13:59:56 +0100 Subject: [PATCH 009/259] Add 2019-07 and 2019-10 API versions --- shopify/api_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index b48f94df..1f96a5e4 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -26,6 +26,8 @@ def define_version(cls, version): def define_known_versions(cls): cls.define_version(Unstable()) cls.define_version(Release('2019-04')) + cls.define_version(Release('2019-07')) + cls.define_version(Release('2019-10')) @classmethod def clear_defined_versions(cls): From 41b4d086aa16fb6fe9b9345fb47f6c57e7309647 Mon Sep 17 00:00:00 2001 From: GhostApps Date: Wed, 14 Aug 2019 14:21:07 +0100 Subject: [PATCH 010/259] Fix shopify_api.py to work with API versions, allow user to configure API version and define a default if not set --- scripts/shopify_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 1fa8ba33..5441f7a5 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -90,6 +90,7 @@ def help(cls, task=None): class Tasks(object): _shop_config_dir = os.path.join(os.environ["HOME"], ".shopify", "shops") _default_symlink = os.path.join(_shop_config_dir, "default") + _default_api_version = "2019-04" @classmethod @usage("list") @@ -116,6 +117,7 @@ def add(cls, connection): 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 os.path.isdir(cls._shop_config_dir): os.makedirs(cls._shop_config_dir) with open(filename, 'w') as f: @@ -231,7 +233,7 @@ def _get_config_filename(cls, connection): @classmethod def _session_from_config(cls, config): - session = shopify.Session(config.get("domain")) + session = shopify.Session(config.get("domain"), config.get("api_version", cls._default_api_version)) session.protocol = config.get("protocol", "https") session.api_key = config.get("api_key") session.token = config.get("password") From 71541122f2598a75f6a278b3f4b926a185370629 Mon Sep 17 00:00:00 2001 From: Jon G Date: Tue, 10 Sep 2019 10:34:42 -0400 Subject: [PATCH 011/259] add version 2020-01 to known versions --- shopify/api_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index 1f96a5e4..276d2889 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -28,6 +28,7 @@ def define_known_versions(cls): cls.define_version(Release('2019-04')) cls.define_version(Release('2019-07')) cls.define_version(Release('2019-10')) + cls.define_version(Release('2020-01')) @classmethod def clear_defined_versions(cls): From 2aa6178179b1b1f4f5c8944448a436d1b83314d8 Mon Sep 17 00:00:00 2001 From: Jon G Date: Tue, 10 Sep 2019 10:37:45 -0400 Subject: [PATCH 012/259] Packaging for version 5.1.2 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d0878e29..77ee9df5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 5.1.2 +* Add version 2020-01 to known ApiVersions. This version will not be usable until October 2019. + == Version 5.1.1 * Fix initializing API with basic auth URL. diff --git a/shopify/version.py b/shopify/version.py index c765b0cd..39841c6b 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '5.1.1' +VERSION = '5.1.2' From 68ce40962fb71cb0d793219454d12d2c7a57a66e Mon Sep 17 00:00:00 2001 From: Tyler Ball Date: Tue, 10 Sep 2019 11:08:01 -0400 Subject: [PATCH 013/259] fix shipit --- shipit.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/shipit.yml b/shipit.yml index a024f7af..9795b304 100644 --- a/shipit.yml +++ b/shipit.yml @@ -1,7 +1,3 @@ -dependencies: - pre: - - pip install twine - deploy: override: - assert-egg-version-tag setup.py From 7b11cc2fd61b28496221fab1a37d74221928224b Mon Sep 17 00:00:00 2001 From: Tyler Ball Date: Tue, 10 Sep 2019 11:17:16 -0400 Subject: [PATCH 014/259] [ci skip] fix shipit again --- shipit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shipit.yml b/shipit.yml index 9795b304..7c001209 100644 --- a/shipit.yml +++ b/shipit.yml @@ -1,5 +1,5 @@ deploy: override: - assert-egg-version-tag setup.py - - python setup.py register sdist - - twine upload dist/* + - python setup.py sdist + - twine upload --repository-url https://upload.pypi.org/legacy/ dist/* From 4f54fc4d9a5a30e12aa52133f75c4d78a801b1e2 Mon Sep 17 00:00:00 2001 From: kirk Date: Mon, 9 Dec 2019 11:06:43 -0800 Subject: [PATCH 015/259] add the current working directory to the sys paths --- scripts/shopify_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 5441f7a5..26f718d3 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -13,6 +13,8 @@ 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() From 0307220a3592ac36ca78fb9f0af5d31a7731d348 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 6 Jan 2020 13:00:29 -0500 Subject: [PATCH 016/259] Add COC to project --- .github/CODE_OF_CONDUCT.md | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5656d370 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others’ private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at opensource@shopify.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project’s leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org From 164c7e288fcdd93feca8bf3cc7faec5722d2395d Mon Sep 17 00:00:00 2001 From: Efe Mert Demir Date: Fri, 23 Aug 2019 14:02:34 +0300 Subject: [PATCH 017/259] Cursor Pagination --- setup.py | 2 +- shipit.yml | 2 +- shopify/__init__.py | 1 + shopify/base.py | 46 +++++ shopify/collection.py | 143 ++++++++++++++ shopify/resources/customer.py | 4 +- shopify/resources/customer_saved_search.py | 2 +- test/fixtures/products.json | 206 +++++++++++++++++++++ test/fixtures/transactions.json | 29 +++ test/order_test.py | 2 +- test/pagination_test.py | 111 +++++++++++ test/test_helper.py | 3 +- 12 files changed, 544 insertions(+), 7 deletions(-) create mode 100644 shopify/collection.py create mode 100644 test/fixtures/products.json create mode 100644 test/fixtures/transactions.json create mode 100644 test/pagination_test.py diff --git a/setup.py b/setup.py index 230b94d4..47334b33 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ scripts=['scripts/shopify_api.py'], license='MIT License', install_requires=[ - 'pyactiveresource>=2.1.2', + 'pyactiveresource>=2.2.0', 'PyYAML', 'six', ], diff --git a/shipit.yml b/shipit.yml index 7c001209..e56fb968 100644 --- a/shipit.yml +++ b/shipit.yml @@ -2,4 +2,4 @@ deploy: override: - assert-egg-version-tag setup.py - python setup.py sdist - - twine upload --repository-url https://upload.pypi.org/legacy/ dist/* + - twine upload --repository-url https://upload.pypi.org/legacy/ -u shopify -p $PYPI_PASSWORD_SHOPIFY dist/* diff --git a/shopify/__init__.py b/shopify/__init__.py index b58f754a..b10d9a48 100644 --- a/shopify/__init__.py +++ b/shopify/__init__.py @@ -3,3 +3,4 @@ from shopify.resources import * from shopify.limits import Limits from shopify.api_version import * +from shopify.collection import PaginatedIterator diff --git a/shopify/base.py b/shopify/base.py index 6cfee0ea..bcab899a 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -8,6 +8,8 @@ from six.moves import urllib import six +from shopify.collection import PaginatedCollection +from pyactiveresource.collection import Collection # Store the response from the last request in the connection object class ShopifyConnection(pyactiveresource.connection.Connection): @@ -202,3 +204,47 @@ def clear_session(cls): cls.password = None cls.version = None cls.headers.pop('X-Shopify-Access-Token', None) + + @classmethod + def find(cls, id_=None, from_=None, **kwargs): + """Checks the resulting collection for pagination metadata.""" + + collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, + **kwargs) + + # pyactiveresource currently sends all headers from the response with + # the collection. + if isinstance(collection, Collection) and \ + "headers" in collection.metadata: + headers = collection.metadata["headers"] + if "Link" in headers: + pagination = cls._parse_pagination(headers["Link"]) + return PaginatedCollection(collection, metadata={ + "pagination": pagination, + "resource_class": cls + }) + + return collection + + @classmethod + def _parse_pagination(cls, data): + """Parses a Link header into a dict for cursor-based pagination. + + Args: + data: The Link header value as a string. + Returns: + A dict with rel names as keys and URLs as values. + """ + + # Example Link header: + # ; rel="previous", + # ; rel="next" + + values = data.split(", ") + + result = {} + for value in values: + link, rel = value.split("; ") + result[rel.split('"')[1]] = link[1:-1] + + return result diff --git a/shopify/collection.py b/shopify/collection.py new file mode 100644 index 00000000..923e3ceb --- /dev/null +++ b/shopify/collection.py @@ -0,0 +1,143 @@ +from pyactiveresource.collection import Collection +from six.moves.urllib.parse import urlparse, parse_qs +import cgi + +class PaginatedCollection(Collection): + """ + A subclass of Collection which allows cycling through pages of + data through cursor-based pagination. + + :next_page_url contains a url for fetching the next page + :previous_page_url contains a url for fetching the previous page + + You can use next_page_url and previous_page_url to fetch the next page + of data by calling Resource.find(from_=page.next_page_url) + """ + + def __init__(self, *args, **kwargs): + """If given a Collection object as an argument, inherit its metadata.""" + + metadata = kwargs.pop("metadata", None) + obj = args[0] + if isinstance(obj, Collection): + if metadata: + metadata.update(obj.metadata) + else: + metadata = obj.metadata + super(PaginatedCollection, self).__init__(obj, metadata=metadata) + else: + super(PaginatedCollection, self).__init__(metadata=metadata or {}, + *args, **kwargs) + if not ("pagination" in self.metadata and "resource_class" in self.metadata): + raise AttributeError("Cursor-based pagination requires \"pagination\" and \"resource_class\" attributes in the metadata.") + + self.next_page_url = self.metadata["pagination"].get('next', None) + self.previous_page_url = self.metadata["pagination"].get('previous', None) + + self._next = None + self._previous = None + self._current_iter = None + self._no_iter_next = kwargs.pop("no_iter_next", False) + + def has_previous(self): + """Returns true if the current page has any previous pages before it. + """ + return bool(self.previous_page_url) + + def has_next(self): + """Returns true if the current page has any pages beyond the current position. + """ + return bool(self.next_page_url) + + def previous(self, no_cache=False): + """Returns the previous page of items. + + Args: + no_cache: If true the page will not be cached. + Returns: + A PaginatedCollection object with the new data set. + """ + if self._previous: + return self._previous + elif not self.has_previous(): + raise IndexError("No previous page") + return self.__fetch_page(self.previous_page_url, no_cache) + + def next(self, no_cache=False): + """Returns the next page of items. + + Args: + no_cache: If true the page will not be cached. + Returns: + A PaginatedCollection object with the new data set. + """ + if self._next: + return self._next + elif not self.has_next(): + raise IndexError("No next page") + return self.__fetch_page(self.next_page_url, no_cache) + + def __fetch_page(self, url, no_cache=False): + next = self.metadata["resource_class"].find(from_=url) + if not no_cache: + self._next = next + self._next._previous = self + next._no_iter_next = self._no_iter_next + return next + + def __iter__(self): + """Iterates through all items, also fetching other pages.""" + for item in super(PaginatedCollection, self).__iter__(): + yield item + + if self._no_iter_next: + return + + try: + if not self._current_iter: + self._current_iter = self + self._current_iter = self.next() + + for item in self._current_iter: + yield item + except IndexError: + return + + def __len__(self): + """If fetched count all the pages.""" + + if self._next: + count = len(self._next) + else: + count = 0 + return count + super(PaginatedCollection, self).__len__() + + +class PaginatedIterator(object): + """ + This class implements an iterator over paginated collections which aims to + be more memory-efficient by not keeping more than one page in memory at a + time. + + >>> from shopify import Product, PaginatedIterator + >>> for page in PaginatedIterator(Product.find()): + ... for item in page: + ... do_something(item) + ... + # every page and the page items are iterated + """ + def __init__(self, collection): + if not isinstance(collection, PaginatedCollection): + raise TypeError("PaginatedIterator expects a PaginatedCollection instance") + self.collection = collection + self.collection._no_iter_next = True + + def __iter__(self): + """Iterate over pages, returning one page at a time.""" + current_page = self.collection + while True: + yield current_page + try: + current_page = current_page.next(no_cache=True) + except IndexError: + return diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py index c1df5e8f..ff2e6e8d 100644 --- a/shopify/resources/customer.py +++ b/shopify/resources/customer.py @@ -17,9 +17,9 @@ def search(cls, **kwargs): limit: Amount of results (default: 50) (maximum: 250) fields: comma-seperated list of fields to include in the response Returns: - An array of customers. + A Collection of customers. """ - return cls._build_list(cls.get("search", **kwargs)) + return cls._build_collection(cls.get("search", **kwargs)) def send_invite(self, customer_invite = CustomerInvite()): resource = self.post("send_invite", customer_invite.encode()) diff --git a/shopify/resources/customer_saved_search.py b/shopify/resources/customer_saved_search.py index 88c74acc..78f54a76 100644 --- a/shopify/resources/customer_saved_search.py +++ b/shopify/resources/customer_saved_search.py @@ -5,4 +5,4 @@ class CustomerSavedSearch(ShopifyResource): def customers(cls, **kwargs): - return Customer._build_list(cls.get("customers", **kwargs)) + return Customer._build_collection(cls.get("customers", **kwargs)) diff --git a/test/fixtures/products.json b/test/fixtures/products.json new file mode 100644 index 00000000..4258cf6a --- /dev/null +++ b/test/fixtures/products.json @@ -0,0 +1,206 @@ +[ + { + "product_type": "Cult Products", + "handle": "ipod-nano", + "created_at": "2011-10-20T14:05:13-04:00", + "body_html": "

It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.

", + "title": "IPod Nano - 8GB", + "template_suffix": null, + "updated_at": "2011-10-20T14:05:13-04:00", + "id": 1, + "tags": "Emotive, Flash Memory, MP3, Music", + "images": [ + { + "position": 1, + "created_at": "2011-10-20T14:05:13-04:00", + "product_id": 1, + "updated_at": "2011-10-20T14:05:13-04:00", + "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0", + "id": 850703190 + } + ], + "variants": [ + { + "position": 1, + "price": "199.00", + "product_id": 1, + "created_at": "2011-10-20T14:05:13-04:00", + "requires_shipping": true, + "title": "Pink", + "inventory_quantity": 10, + "compare_at_price": null, + "inventory_policy": "continue", + "updated_at": "2011-10-20T14:05:13-04:00", + "inventory_management": "shopify", + "id": 808950810, + "taxable": true, + "grams": 200, + "sku": "IPOD2008PINK", + "option1": "Pink", + "fulfillment_service": "manual", + "option2": null, + "option3": null + } + ], + "vendor": "Apple", + "published_at": "2007-12-31T19:00:00-05:00", + "options": [ + { + "name": "Title" + } + ] + }, + { + "product_type": "Cult Products", + "handle": "ipod-nano", + "created_at": "2011-10-20T14:05:13-04:00", + "body_html": "

It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.

", + "title": "IPod Nano - 8GB", + "template_suffix": null, + "updated_at": "2011-10-20T14:05:13-04:00", + "id": 2, + "tags": "Emotive, Flash Memory, MP3, Music", + "images": [ + { + "position": 1, + "created_at": "2011-10-20T14:05:13-04:00", + "product_id": 2, + "updated_at": "2011-10-20T14:05:13-04:00", + "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0", + "id": 850703190 + } + ], + "variants": [ + { + "position": 1, + "price": "199.00", + "product_id": 2, + "created_at": "2011-10-20T14:05:13-04:00", + "requires_shipping": true, + "title": "Pink", + "inventory_quantity": 10, + "compare_at_price": null, + "inventory_policy": "continue", + "updated_at": "2011-10-20T14:05:13-04:00", + "inventory_management": "shopify", + "id": 808950810, + "taxable": true, + "grams": 200, + "sku": "IPOD2008PINK", + "option1": "Pink", + "fulfillment_service": "manual", + "option2": null, + "option3": null + } + ], + "vendor": "Apple", + "published_at": "2007-12-31T19:00:00-05:00", + "options": [ + { + "name": "Title" + } + ] + }, + { + "product_type": "Cult Products", + "handle": "ipod-nano", + "created_at": "2011-10-20T14:05:13-04:00", + "body_html": "

It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.

", + "title": "IPod Nano - 8GB", + "template_suffix": null, + "updated_at": "2011-10-20T14:05:13-04:00", + "id": 3, + "tags": "Emotive, Flash Memory, MP3, Music", + "images": [ + { + "position": 1, + "created_at": "2011-10-20T14:05:13-04:00", + "product_id": 2, + "updated_at": "2011-10-20T14:05:13-04:00", + "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0", + "id": 850703190 + } + ], + "variants": [ + { + "position": 1, + "price": "199.00", + "product_id": 2, + "created_at": "2011-10-20T14:05:13-04:00", + "requires_shipping": true, + "title": "Pink", + "inventory_quantity": 10, + "compare_at_price": null, + "inventory_policy": "continue", + "updated_at": "2011-10-20T14:05:13-04:00", + "inventory_management": "shopify", + "id": 808950810, + "taxable": true, + "grams": 200, + "sku": "IPOD2008PINK", + "option1": "Pink", + "fulfillment_service": "manual", + "option2": null, + "option3": null + } + ], + "vendor": "Apple", + "published_at": "2007-12-31T19:00:00-05:00", + "options": [ + { + "name": "Title" + } + ] + }, + { + "product_type": "Cult Products", + "handle": "ipod-nano", + "created_at": "2011-10-20T14:05:13-04:00", + "body_html": "

It's the small iPod with one very big idea: Video. Now the world's most popular music player, available in 4GB and 8GB models, lets you enjoy TV shows, movies, video podcasts, and more. The larger, brighter display means amazing picture quality. In six eye-catching colors, iPod nano is stunning all around. And with models starting at just $149, little speaks volumes.

", + "title": "IPod Nano - 8GB", + "template_suffix": null, + "updated_at": "2011-10-20T14:05:13-04:00", + "id": 4, + "tags": "Emotive, Flash Memory, MP3, Music", + "images": [ + { + "position": 1, + "created_at": "2011-10-20T14:05:13-04:00", + "product_id": 4, + "updated_at": "2011-10-20T14:05:13-04:00", + "src": "http://static.shopify.com/s/files/1/6909/3384/products/ipod-nano.png?0", + "id": 850703190 + } + ], + "variants": [ + { + "position": 1, + "price": "199.00", + "product_id": 4, + "created_at": "2011-10-20T14:05:13-04:00", + "requires_shipping": true, + "title": "Pink", + "inventory_quantity": 10, + "compare_at_price": null, + "inventory_policy": "continue", + "updated_at": "2011-10-20T14:05:13-04:00", + "inventory_management": "shopify", + "id": 808950810, + "taxable": true, + "grams": 200, + "sku": "IPOD2008PINK", + "option1": "Pink", + "fulfillment_service": "manual", + "option2": null, + "option3": null + } + ], + "vendor": "Apple", + "published_at": "2007-12-31T19:00:00-05:00", + "options": [ + { + "name": "Title" + } + ] + } +] diff --git a/test/fixtures/transactions.json b/test/fixtures/transactions.json new file mode 100644 index 00000000..b712d649 --- /dev/null +++ b/test/fixtures/transactions.json @@ -0,0 +1,29 @@ +[ + { + "amount": "409.94", + "authorization": "authorization-key", + "created_at": "2005-08-01T11:57:11-04:00", + "gateway": "bogus", + "id": 389404469, + "kind": "authorization", + "location_id": null, + "message": null, + "order_id": 450789469, + "parent_id": null, + "status": "success", + "test": false, + "user_id": null, + "device_id": null, + "receipt": { + "testcase": true, + "authorization": "123456" + }, + "payment_details": { + "avs_result_code": null, + "credit_card_bin": null, + "cvv_result_code": null, + "credit_card_number": "XXXX-XXXX-XXXX-4242", + "credit_card_company": "Visa" + } + } +] diff --git a/test/order_test.py b/test/order_test.py index d70645c4..8b173c00 100644 --- a/test/order_test.py +++ b/test/order_test.py @@ -44,6 +44,6 @@ def test_get_order(self): def test_get_order_transaction(self): 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('transaction')) + self.fake('orders/450789469/transactions', method='GET', body=self.load_fixture('transactions')) transactions = order.transactions() self.assertEqual("409.94", transactions[0].amount) diff --git a/test/pagination_test.py b/test/pagination_test.py new file mode 100644 index 00000000..8539d4a7 --- /dev/null +++ b/test/pagination_test.py @@ -0,0 +1,111 @@ +import shopify +import json +from test.test_helper import TestCase + +class PaginationTest(TestCase): + + def setUp(self): + super(PaginationTest, self).setUp() + prefix = self.http.site + "/admin/api/unstable" + + self.fixture = json.loads(self.load_fixture('products')) + 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": self.fixture[:2] }), + response_headers=next_headers) + self.fake("products", + url=prefix + "/products.json?limit=2&page_info=FOOBAR", + body=json.dumps({ "products": self.fixture[2:4] }), + response_headers=prev_headers) + self.fake("products", + url=prefix + "/products.json?limit=2&page_info=BAZQUUX", + body=json.dumps({ "products": self.fixture[:2] }), + response_headers=next_headers) + + def test_paginated_collection(self): + items = shopify.Product.find(limit=2) + self.assertIsInstance(items, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection") + self.assertEqual(len(items), 2, "find() result has incorrect length") + + def test_pagination_next(self): + c = shopify.Product.find(limit=2) + self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect") + n = c.next() + self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect") + self.assertIsInstance(n, shopify.collection.PaginatedCollection, + "next() result is not PaginatedCollection") + self.assertEqual(len(n), 2, "next() collection has incorrect length") + self.assertIn("pagination", n.metadata) + self.assertIn("previous", n.metadata["pagination"], + "next() collection doesn't have a previous page") + + with self.assertRaises(IndexError, msg="next() did not raise with no next page"): + n.next() + + def test_pagination_previous(self): + c = shopify.Product.find(limit=2) + self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect") + self.assertTrue(c.has_next()) + n = c.next() + self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect") + + p = n.previous() + + self.assertIsInstance(p, shopify.collection.PaginatedCollection, + "previous() result is not PaginatedCollection") + self.assertEqual(len(p), 4, # cached + "previous() collection has incorrect length") + self.assertIn("pagination", p.metadata) + self.assertIn("next", p.metadata["pagination"], + "previous() collection doesn't have a next page") + + with self.assertRaises(IndexError, msg="previous() did not raise with no previous page"): + p.previous() + + def test_paginated_collection_iterator(self): + c = shopify.Product.find(limit=2) + + i = iter(c) + self.assertEqual(next(i).id, 1) + self.assertEqual(next(i).id, 2) + self.assertEqual(next(i).id, 3) + self.assertEqual(next(i).id, 4) + with self.assertRaises(StopIteration): + next(i) + + def test_paginated_collection_no_cache(self): + c = shopify.Product.find(limit=2) + + n = c.next(no_cache=True) + self.assertIsNone(c._next, "no_cache=True still caches") + self.assertIsNone(n._previous, "no_cache=True still caches") + + p = n.previous(no_cache=True) + self.assertIsNone(p._next, "no_cache=True still caches") + self.assertIsNone(n._previous, "no_cache=True still caches") + + def test_paginated_iterator(self): + c = shopify.Product.find(limit=2) + + i = iter(shopify.PaginatedIterator(c)) + + first_page = iter(next(i)) + self.assertEqual(next(first_page).id, 1) + self.assertEqual(next(first_page).id, 2) + with self.assertRaises(StopIteration): + next(first_page) + + second_page = iter(next(i)) + self.assertEqual(next(second_page).id, 3) + self.assertEqual(next(second_page).id, 4) + with self.assertRaises(StopIteration): + next(second_page) + + with self.assertRaises(StopIteration): + next(i) diff --git a/test/test_helper.py b/test/test_helper.py index 5eaa7566..dfb80968 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -55,4 +55,5 @@ def fake(self, endpoint, **kwargs): code = kwargs.pop('code', 200) self.http.respond_to( - method, url, headers, body=body, code=code) + method, url, headers, body=body, code=code, + response_headers=kwargs.pop('response_headers', None)) From fa6893c7ecf80aff95a8ff1a475eca4d2b9d8bf6 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 20 Jan 2020 22:29:26 -0500 Subject: [PATCH 018/259] Compatibility for tests --- test/pagination_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/pagination_test.py b/test/pagination_test.py index 8539d4a7..cce39837 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -7,8 +7,8 @@ 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()) - self.fixture = json.loads(self.load_fixture('products')) self.next_page_url = prefix + "/products.json?limit=2&page_info=FOOBAR" self.prev_page_url = prefix + "/products.json?limit=2&page_info=BAZQUUX" @@ -17,15 +17,15 @@ def setUp(self): self.fake("products", url=prefix + "/products.json?limit=2", - body=json.dumps({ "products": self.fixture[: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": self.fixture[2:4] }), + 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": self.fixture[:2] }), + body=json.dumps({ "products": fixture[:2] }), response_headers=next_headers) def test_paginated_collection(self): From fd3e39155bb22da6f62c49531990b5662d878f7e Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 21 Jan 2020 11:05:11 -0500 Subject: [PATCH 019/259] rename paging methods to be more clear --- shopify/collection.py | 16 ++++++++-------- test/pagination_test.py | 35 ++++++++++++++++++----------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/shopify/collection.py b/shopify/collection.py index 923e3ceb..5fa4a684 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -39,17 +39,17 @@ def __init__(self, *args, **kwargs): self._current_iter = None self._no_iter_next = kwargs.pop("no_iter_next", False) - def has_previous(self): + def has_previous_page(self): """Returns true if the current page has any previous pages before it. """ return bool(self.previous_page_url) - def has_next(self): + def has_next_page(self): """Returns true if the current page has any pages beyond the current position. """ return bool(self.next_page_url) - def previous(self, no_cache=False): + def previous_page(self, no_cache=False): """Returns the previous page of items. Args: @@ -59,11 +59,11 @@ def previous(self, no_cache=False): """ if self._previous: return self._previous - elif not self.has_previous(): + elif not self.has_previous_page(): raise IndexError("No previous page") return self.__fetch_page(self.previous_page_url, no_cache) - def next(self, no_cache=False): + def next_page(self, no_cache=False): """Returns the next page of items. Args: @@ -73,7 +73,7 @@ def next(self, no_cache=False): """ if self._next: return self._next - elif not self.has_next(): + elif not self.has_next_page(): raise IndexError("No next page") return self.__fetch_page(self.next_page_url, no_cache) @@ -96,7 +96,7 @@ def __iter__(self): try: if not self._current_iter: self._current_iter = self - self._current_iter = self.next() + self._current_iter = self.next_page() for item in self._current_iter: yield item @@ -138,6 +138,6 @@ def __iter__(self): while True: yield current_page try: - current_page = current_page.next(no_cache=True) + current_page = current_page.next_page(no_cache=True) except IndexError: return diff --git a/test/pagination_test.py b/test/pagination_test.py index cce39837..461e8e04 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -33,40 +33,41 @@ def test_paginated_collection(self): self.assertIsInstance(items, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection") self.assertEqual(len(items), 2, "find() result has incorrect length") - def test_pagination_next(self): + def test_pagination_next_page(self): c = shopify.Product.find(limit=2) self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect") - n = c.next() + 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() result is not PaginatedCollection") - self.assertEqual(len(n), 2, "next() collection has incorrect length") + "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() collection doesn't have a previous page") + "next_page() collection doesn't have a previous page") - with self.assertRaises(IndexError, msg="next() did not raise with no next page"): - n.next() + with self.assertRaises(IndexError, msg="next_page() did not raise with no next page"): + n.next_page() def test_pagination_previous(self): c = shopify.Product.find(limit=2) self.assertEqual(c.next_page_url, self.next_page_url, "next url is incorrect") - self.assertTrue(c.has_next()) - n = c.next() + self.assertTrue(c.has_next_page()) + n = c.next_page() self.assertEqual(n.previous_page_url, self.prev_page_url, "prev url is incorrect") + self.assertTrue(n.has_previous_page()) - p = n.previous() + p = n.previous_page() self.assertIsInstance(p, shopify.collection.PaginatedCollection, - "previous() result is not PaginatedCollection") + "previous_page() result is not PaginatedCollection") self.assertEqual(len(p), 4, # cached - "previous() collection has incorrect length") + "previous_page() collection has incorrect length") self.assertIn("pagination", p.metadata) self.assertIn("next", p.metadata["pagination"], - "previous() collection doesn't have a next page") + "previous_page() collection doesn't have a next page") - with self.assertRaises(IndexError, msg="previous() did not raise with no previous page"): - p.previous() + with self.assertRaises(IndexError, msg="previous_page() did not raise with no previous page"): + p.previous_page() def test_paginated_collection_iterator(self): c = shopify.Product.find(limit=2) @@ -82,11 +83,11 @@ def test_paginated_collection_iterator(self): def test_paginated_collection_no_cache(self): c = shopify.Product.find(limit=2) - n = c.next(no_cache=True) + n = c.next_page(no_cache=True) self.assertIsNone(c._next, "no_cache=True still caches") self.assertIsNone(n._previous, "no_cache=True still caches") - p = n.previous(no_cache=True) + p = n.previous_page(no_cache=True) self.assertIsNone(p._next, "no_cache=True still caches") self.assertIsNone(n._previous, "no_cache=True still caches") From c6ddb85114b60a61a653775af420a7604162d27d Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 21 Jan 2020 12:09:16 -0500 Subject: [PATCH 020/259] Release v6.0.0 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 77ee9df5..d5d304ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 6.0.0 +* Add Cursor pagination support + == Version 5.1.2 * Add version 2020-01 to known ApiVersions. This version will not be usable until October 2019. diff --git a/shopify/version.py b/shopify/version.py index 39841c6b..3eedc841 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '5.1.2' +VERSION = '6.0.0' From 3cec005f5af28622fba023c67f739784a23f98a4 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 21 Jan 2020 12:19:46 -0500 Subject: [PATCH 021/259] Adding a short note about cursor pagination usage --- README.md | 15 +++++++++++++++ RELEASING | 11 +---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 45111bc4..24b188b6 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,21 @@ pip install detox detox ``` +## Relative Cursor Pagination +Cursor based pagination support has been added in 6.0.0. + +``` +import shopify + +page1 = shopify.Product.find() +if page1.has_next_page(): + page2 = page1.next_page() + +# to persist across requests you can use next_page_url and previous_page_url +next_url = page1.next_page_url +page2 = shopify.Product.find(from_=next_url) +``` + ## Limitations Currently there is no support for: diff --git a/RELEASING b/RELEASING index d81ce509..68ccd2b0 100644 --- a/RELEASING +++ b/RELEASING @@ -15,13 +15,4 @@ Releasing shopify_python_api 6. Push the changes to github git push --tags origin master -7. Configure your credentials for pypi in $HOME/.pypirc. Do not specify a repository URL. - (see https://docs.python.org/2/distutils/packageindex.html#the-pypirc-file for further instructions) - -8. Create the source package using sdist - python setup.py sdist - -8. Upload the source package to pypi using twine – you need to be a package owner in pypi to do this - twine upload dist/ShopifyAPI-x.y.z.tar.gz - -If you need access to pypi (in general, this is only for Shopify staff members) ping @jamiemtdwyer. +7. Shipit! From 632bf0e594bdc6f3e5e92b904b2272fc4a0eb15a Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 27 Jan 2020 10:10:50 -0500 Subject: [PATCH 022/259] Cast all collections to PaginatedCollection --- shopify/base.py | 42 +++--------------------------------------- shopify/collection.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/shopify/base.py b/shopify/base.py index bcab899a..65349e66 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -208,43 +208,7 @@ def clear_session(cls): @classmethod def find(cls, id_=None, from_=None, **kwargs): """Checks the resulting collection for pagination metadata.""" - - collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, - **kwargs) - - # pyactiveresource currently sends all headers from the response with - # the collection. - if isinstance(collection, Collection) and \ - "headers" in collection.metadata: - headers = collection.metadata["headers"] - if "Link" in headers: - pagination = cls._parse_pagination(headers["Link"]) - return PaginatedCollection(collection, metadata={ - "pagination": pagination, - "resource_class": cls - }) - + collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, **kwargs) + if isinstance(collection, Collection) and "headers" in collection.metadata: + return PaginatedCollection(collection, metadata={"resource_class": cls}) return collection - - @classmethod - def _parse_pagination(cls, data): - """Parses a Link header into a dict for cursor-based pagination. - - Args: - data: The Link header value as a string. - Returns: - A dict with rel names as keys and URLs as values. - """ - - # Example Link header: - # ; rel="previous", - # ; rel="next" - - values = data.split(", ") - - result = {} - for value in values: - link, rel = value.split("; ") - result[rel.split('"')[1]] = link[1:-1] - - return result diff --git a/shopify/collection.py b/shopify/collection.py index 5fa4a684..aafe49bc 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -26,11 +26,12 @@ def __init__(self, *args, **kwargs): metadata = obj.metadata super(PaginatedCollection, self).__init__(obj, metadata=metadata) else: - super(PaginatedCollection, self).__init__(metadata=metadata or {}, - *args, **kwargs) - if not ("pagination" in self.metadata and "resource_class" in self.metadata): + super(PaginatedCollection, self).__init__(metadata=metadata or {}, *args, **kwargs) + + if not ("resource_class" in self.metadata): raise AttributeError("Cursor-based pagination requires \"pagination\" and \"resource_class\" attributes 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) @@ -39,6 +40,18 @@ def __init__(self, *args, **kwargs): self._current_iter = None self._no_iter_next = kwargs.pop("no_iter_next", False) + def __parse_pagination(self): + headers = self.metadata["headers"] + if "Link" not in headers: + return {} + data = headers["Link"] + values = data.split(", ") + result = {} + for value in values: + link, rel = value.split("; ") + result[rel.split('"')[1]] = link[1:-1] + return result + def has_previous_page(self): """Returns true if the current page has any previous pages before it. """ From 40c31f8b2ba2faa47bba2d42a605317ca4ec1f22 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 27 Jan 2020 12:23:40 -0500 Subject: [PATCH 023/259] minimizing some code and add a test for non-paginated --- shopify/collection.py | 6 ++---- test/pagination_test.py | 7 +++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/shopify/collection.py b/shopify/collection.py index aafe49bc..ccab068c 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -41,11 +41,9 @@ def __init__(self, *args, **kwargs): self._no_iter_next = kwargs.pop("no_iter_next", False) def __parse_pagination(self): - headers = self.metadata["headers"] - if "Link" not in headers: + if "headers" not in self.metadata or "Link" not in self.metadata["headers"]: return {} - data = headers["Link"] - values = data.split(", ") + values = self.metadata["headers"]["Link"].split(", ") result = {} for value in values: link, rel = value.split("; ") diff --git a/test/pagination_test.py b/test/pagination_test.py index 461e8e04..3ee05ad0 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -28,6 +28,13 @@ def setUp(self): 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')) + 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") + def test_paginated_collection(self): items = shopify.Product.find(limit=2) self.assertIsInstance(items, shopify.collection.PaginatedCollection, "find() result is not PaginatedCollection") From 80f76ded778d858de75e88035215127798961008 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 27 Jan 2020 12:24:43 -0500 Subject: [PATCH 024/259] Update the error messag --- shopify/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/collection.py b/shopify/collection.py index ccab068c..a604b1ac 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -29,7 +29,7 @@ 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 \"pagination\" and \"resource_class\" attributes 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) From cd049439a62b84475b3693f82d455fb342f4abc7 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 27 Jan 2020 16:39:21 -0500 Subject: [PATCH 025/259] updating version --- CHANGELOG | 4 ++++ shopify/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d5d304ea..01a6d654 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== Version 6.0.1 +* Made the collection access more consistent so that there is no confusion +between a collection and a paginated collection + == Version 6.0.0 * Add Cursor pagination support diff --git a/shopify/version.py b/shopify/version.py index 3eedc841..2959161c 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '6.0.0' +VERSION = '6.0.1' From 0a181c9b3c355965b9366867053f4480d79f3228 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 6 Feb 2020 12:19:34 -0500 Subject: [PATCH 026/259] Adding some flexibility into the pagination --- shopify/base.py | 2 +- shopify/collection.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/shopify/base.py b/shopify/base.py index 65349e66..d14401e3 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -210,5 +210,5 @@ def find(cls, id_=None, from_=None, **kwargs): """Checks the resulting collection for pagination metadata.""" collection = super(ShopifyResource, cls).find(id_=id_, from_=from_, **kwargs) if isinstance(collection, Collection) and "headers" in collection.metadata: - return PaginatedCollection(collection, metadata={"resource_class": cls}) + return PaginatedCollection(collection, metadata={"resource_class": cls}, **kwargs) return collection diff --git a/shopify/collection.py b/shopify/collection.py index a604b1ac..69d9db79 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -41,11 +41,15 @@ def __init__(self, *args, **kwargs): self._no_iter_next = kwargs.pop("no_iter_next", False) def __parse_pagination(self): - if "headers" not in self.metadata or "Link" not in self.metadata["headers"]: + if "headers" not in self.metadata: return {} - values = self.metadata["headers"]["Link"].split(", ") + + values = self.metadata["headers"].get("Link", self.metadata["headers"].get("link", None)) + if values is None: + return {} + result = {} - for value in values: + for value in values.split(", "): link, rel = value.split("; ") result[rel.split('"')[1]] = link[1:-1] return result From 2282850aedee4342edb42458d66be434689d5ee8 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 11 Feb 2020 14:25:06 -0500 Subject: [PATCH 027/259] Change the default of no_iter_next --- shopify/collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/collection.py b/shopify/collection.py index 69d9db79..b254218f 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -38,7 +38,7 @@ def __init__(self, *args, **kwargs): self._next = None self._previous = None self._current_iter = None - self._no_iter_next = kwargs.pop("no_iter_next", False) + self._no_iter_next = kwargs.pop("no_iter_next", True) def __parse_pagination(self): if "headers" not in self.metadata: From aec873c05ea684a920405ece25d4128d318cf4aa Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 11 Feb 2020 14:31:48 -0500 Subject: [PATCH 028/259] Update the tests --- test/pagination_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/pagination_test.py b/test/pagination_test.py index 3ee05ad0..6b00bb78 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -82,8 +82,6 @@ def test_paginated_collection_iterator(self): i = iter(c) self.assertEqual(next(i).id, 1) self.assertEqual(next(i).id, 2) - self.assertEqual(next(i).id, 3) - self.assertEqual(next(i).id, 4) with self.assertRaises(StopIteration): next(i) From 16053bb8c6a27a0af992e4d6efc5bfdbb7931136 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 11 Feb 2020 14:42:20 -0500 Subject: [PATCH 029/259] Release v7.0.0 --- CHANGELOG | 7 +++++++ shopify/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 01a6d654..d244051f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +== Version 7.0.0 +* Made no_iter_next default to True on collection so that by default it only +fetches a single page +* Passes kwargs to paginated collections so that attributes can be set with +find() +* Allow case insensitive check for the link header for cursor pagination. + == Version 6.0.1 * Made the collection access more consistent so that there is no confusion between a collection and a paginated collection diff --git a/shopify/version.py b/shopify/version.py index 2959161c..3e75d642 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '6.0.1' +VERSION = '7.0.0' From 7f9b1326241b974b6d7ca06cdf68c8e75fc82af0 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Wed, 26 Feb 2020 14:45:14 -0500 Subject: [PATCH 030/259] Fixing small string interpolation issue --- shopify/resources/event.py | 2 +- test/event_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 test/event_test.py diff --git a/shopify/resources/event.py b/shopify/resources/event.py index 5d266d0b..0fc1e6ff 100644 --- a/shopify/resources/event.py +++ b/shopify/resources/event.py @@ -7,6 +7,6 @@ class Event(ShopifyResource): def _prefix(cls, options={}): resource = options.get("resource") if resource: - return "%s/s/%s" % (cls.site, resource, options["resource_id"]) + return "%s/%s/%s" % (cls.site, resource, options["resource_id"]) else: return cls.site diff --git a/test/event_test.py b/test/event_test.py new file mode 100644 index 00000000..67e80335 --- /dev/null +++ b/test/event_test.py @@ -0,0 +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}) + 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) + From 9f7c90f333204d6ae3e2894d9bdd2510d4d58e84 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 16 Mar 2020 11:15:53 -0400 Subject: [PATCH 031/259] Updating version: --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d244051f..9b865e44 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index 3e75d642..1009ee41 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '7.0.0' +VERSION = '7.0.1' From 34b4c6129527c7e5dba836519fe38248541899f3 Mon Sep 17 00:00:00 2001 From: Juanjo Date: Tue, 24 Mar 2020 13:11:49 +0100 Subject: [PATCH 032/259] Solves API version 2019-10 fails with variant updates --- shopify/resources/product.py | 12 ++++++++++++ shopify/resources/variant.py | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index e364ae07..b310c804 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -30,3 +30,15 @@ def remove_from_collection(self, collection): def add_variant(self, variant): variant.attributes['product_id'] = self.id return variant.save() + + def save(self): + # github issue 347 + # todo: how to get the api version without split & strip + api_version = ShopifyResource._site.split('/')[-1].strip('-') + start_api_version = '201910' + if api_version >= start_api_version: + for variant in self.variants: + del variant.attributes['inventory_quantity'] + del variant.attributes['old_inventory_quantity'] + + return super(ShopifyResource, self).save() diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py index 01a72bbd..57e5ad8a 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -16,4 +16,13 @@ def _prefix(cls, options={}): def save(self): if 'product_id' not in self._prefix_options: self._prefix_options['product_id'] = self.product_id + + # github issue 347 + # todo: how to get the api version without split & strip + api_version = ShopifyResource._site.split('/')[-1].strip('-') + start_api_version = '201910' + if api_version >= start_api_version: + del self.attributes['inventory_quantity'] + del self.attributes['old_inventory_quantity'] + return super(ShopifyResource, self).save() From 2b7c0c6cb081f6435d05b42040e5649244bfb366 Mon Sep 17 00:00:00 2001 From: Juanjo Date: Tue, 24 Mar 2020 13:30:53 +0100 Subject: [PATCH 033/259] check first if attribute exists --- shopify/resources/product.py | 6 ++++-- shopify/resources/variant.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index b310c804..4219b6e2 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -38,7 +38,9 @@ def save(self): start_api_version = '201910' if api_version >= start_api_version: for variant in self.variants: - del variant.attributes['inventory_quantity'] - del variant.attributes['old_inventory_quantity'] + if variant.attributes.get('inventory_quantity'): + del variant.attributes['inventory_quantity'] + if variant.attributes.get('old_inventory_quantity'): + del variant.attributes['old_inventory_quantity'] return super(ShopifyResource, self).save() diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py index 57e5ad8a..9d3db0c3 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -22,7 +22,9 @@ def save(self): api_version = ShopifyResource._site.split('/')[-1].strip('-') start_api_version = '201910' if api_version >= start_api_version: - del self.attributes['inventory_quantity'] - del self.attributes['old_inventory_quantity'] + if self.attributes.get('inventory_quantity'): + del self.attributes['inventory_quantity'] + if self.attributes.get('old_inventory_quantity'): + del self.attributes['old_inventory_quantity'] return super(ShopifyResource, self).save() From 4bfd71eb0d930ec8ca68758d9ea53d95ea6933b6 Mon Sep 17 00:00:00 2001 From: Juanjo Date: Tue, 24 Mar 2020 14:13:02 +0100 Subject: [PATCH 034/259] change dict.get for string in dict to avoid use 0 = False --- shopify/resources/product.py | 5 ++--- shopify/resources/variant.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index 4219b6e2..827700b4 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -38,9 +38,8 @@ def save(self): start_api_version = '201910' if api_version >= start_api_version: for variant in self.variants: - if variant.attributes.get('inventory_quantity'): + if 'inventory_quantity' in variant.attributes: del variant.attributes['inventory_quantity'] - if variant.attributes.get('old_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/variant.py b/shopify/resources/variant.py index 9d3db0c3..cff4f829 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -22,9 +22,9 @@ def save(self): api_version = ShopifyResource._site.split('/')[-1].strip('-') start_api_version = '201910' if api_version >= start_api_version: - if self.attributes.get('inventory_quantity'): + if 'inventory_quantity' in self.attributes: del self.attributes['inventory_quantity'] - if self.attributes.get('old_inventory_quantity'): + if 'old_inventory_quantity' in self.attributes: del self.attributes['old_inventory_quantity'] return super(ShopifyResource, self).save() From b27cfdfb9278aa22b9cf25856cdc816cbef287f8 Mon Sep 17 00:00:00 2001 From: Juanjo Date: Wed, 1 Apr 2020 13:08:25 +0200 Subject: [PATCH 035/259] get api version from path when no session is used --- shopify/base.py | 22 +++++++++++----------- shopify/resources/product.py | 7 +++---- shopify/resources/variant.py | 7 +++---- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/shopify/base.py b/shopify/base.py index d14401e3..f8a7853e 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -12,6 +12,8 @@ from pyactiveresource.collection import Collection # Store the response from the last request in the connection object + + class ShopifyConnection(pyactiveresource.connection.Connection): response = None @@ -29,6 +31,8 @@ def _open(self, *args, **kwargs): return self.response # Inherit from pyactiveresource's metaclass in order to use ShopifyConnection + + class ShopifyResourceMeta(ResourceMeta): @property @@ -139,17 +143,12 @@ def set_prefix_source(cls, value): prefix_source = property(get_prefix_source, set_prefix_source, None, 'prefix for lookups for this type of object.') - def get_version(cls): - return getattr(cls._threadlocal, 'version', ShopifyResource._version) - - def set_version(cls, value): - ShopifyResource._version = cls._threadlocal.version = value - - version = property(get_version, set_version, None, - 'Shopify Api Version') def get_version(cls): - return getattr(cls._threadlocal, 'version', ShopifyResource._version) + if hasattr(cls._threadlocal, 'version') or ShopifyResource._version: + return getattr(cls._threadlocal, 'version', ShopifyResource._version) + else: + return ShopifyResource._site.split('/')[-1] def set_version(cls, value): ShopifyResource._version = cls._threadlocal.version = value @@ -164,14 +163,15 @@ 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') + '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 diff --git a/shopify/resources/product.py b/shopify/resources/product.py index 827700b4..06bd29f0 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -32,11 +32,10 @@ def add_variant(self, variant): return variant.save() def save(self): - # github issue 347 - # todo: how to get the api version without split & strip - api_version = ShopifyResource._site.split('/')[-1].strip('-') start_api_version = '201910' - if api_version >= start_api_version: + api_version = ShopifyResource.version + if api_version and ( + api_version.strip('-') >= start_api_version) and api_version != 'unstable': for variant in self.variants: if 'inventory_quantity' in variant.attributes: del variant.attributes['inventory_quantity'] diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py index cff4f829..c0a38e6c 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -17,11 +17,10 @@ def save(self): if 'product_id' not in self._prefix_options: self._prefix_options['product_id'] = self.product_id - # github issue 347 - # todo: how to get the api version without split & strip - api_version = ShopifyResource._site.split('/')[-1].strip('-') start_api_version = '201910' - if api_version >= start_api_version: + 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: From 6e27f7d73a07f7791f5f81471d4a96be20935025 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Wed, 1 Apr 2020 09:12:30 -0400 Subject: [PATCH 036/259] Releasing v7.0.2 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9b865e44..d3feeb3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 7.0.2 +- bug fix for variant updates after the 2019-10 api version + == Version 7.0.1 - bug fix for string interpolation diff --git a/shopify/version.py b/shopify/version.py index 1009ee41..ee3e2e68 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '7.0.1' +VERSION = '7.0.2' From fb98173b5d614cd60343419f9a59cac07e4c9f1a Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Sat, 11 Apr 2020 14:12:53 +0000 Subject: [PATCH 037/259] Fix deprecation warning regarding invalid escape sequences. --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 276d2889..f9614b68 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -52,7 +52,7 @@ def __eq__(self, other): class Release(ApiVersion): - FORMAT = re.compile('^\d{4}-\d{2}$') + FORMAT = re.compile(r'^\d{4}-\d{2}$') API_PREFIX = '/admin/api' def __init__(self, version_number): From 1a5f1f6c2423ec1e1d2b70792e9c02732d13420b Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 13 Apr 2020 09:58:26 -0400 Subject: [PATCH 038/259] Fix for version getter --- shopify/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/base.py b/shopify/base.py index f8a7853e..f2d9eb06 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -147,7 +147,7 @@ def set_prefix_source(cls, value): def get_version(cls): if hasattr(cls._threadlocal, 'version') or ShopifyResource._version: return getattr(cls._threadlocal, 'version', ShopifyResource._version) - else: + elif ShopifyResource._site is not None: return ShopifyResource._site.split('/')[-1] def set_version(cls, value): From 5c06043b33143cb23a131534870cef509c1d88d1 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 13 Apr 2020 12:09:52 -0400 Subject: [PATCH 039/259] Releasing 7.0.3 --- CHANGELOG | 4 ++++ shopify/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d3feeb3f..d74b8b6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index ee3e2e68..3530b14b 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '7.0.2' +VERSION = '7.0.3' From 8d8bed424c4e4027b82271725fbccf7a55fce13b Mon Sep 17 00:00:00 2001 From: Pierre Grimaud Date: Sun, 19 Apr 2020 03:01:33 +0200 Subject: [PATCH 040/259] Fix typos --- CHANGELOG | 10 +++++----- CONTRIBUTING.md | 2 +- shopify/resources/customer.py | 2 +- shopify/resources/gift_card.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d74b8b6f..06a0f3b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -202,19 +202,19 @@ variable to simplify session saving and resuming == Version 0.4.0 -* Using setup.py no longer requires all dependancies -* More compatiblity fixes for using the latest pyactiveresource +* 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 -* Compatiblity 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 erros are cleared on save from previous save attempt. +* Resource errors are cleared on save from previous save attempt. * Made the library thread-safe using thread-local connections. == Version 0.2.1 @@ -253,7 +253,7 @@ object to be created on each resource. == Version 0.1.3 -* Fixed the automatic download of dependancies. +* Fixed the automatic download of dependencies. * Updated the README instructions. == Version 0.1.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f28367bd..490f988b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,6 @@ Submitting Issues Please open an issue here if you encounter a specific bug with this API client library or if something is documented here https://docs.shopify.com/api but is missing from this package. -General questions about the Shopify API and usage of this package (not neccessarily a bug) should be posted on the [Shopify forums](https://ecommerce.shopify.com/c/shopify-apis-and-technology). +General questions about the Shopify API and usage of this package (not necessarily a bug) should be posted on the [Shopify forums](https://ecommerce.shopify.com/c/shopify-apis-and-technology). When in doubt, post on the forum first. You'll likely have your questions answered more quickly if you post there; more people monitor the forum than Github. diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py index ff2e6e8d..ca4a51f2 100644 --- a/shopify/resources/customer.py +++ b/shopify/resources/customer.py @@ -15,7 +15,7 @@ def search(cls, **kwargs): query: Text to search for customers page: Page to show (default: 1) limit: Amount of results (default: 50) (maximum: 250) - fields: comma-seperated list of fields to include in the response + fields: comma-separated list of fields to include in the response Returns: A Collection of customers. """ diff --git a/shopify/resources/gift_card.py b/shopify/resources/gift_card.py index 7c790053..d933ac01 100644 --- a/shopify/resources/gift_card.py +++ b/shopify/resources/gift_card.py @@ -17,7 +17,7 @@ def search(cls, **kwargs): query: Text to search for gift cards page: Page to show (default: 1) limit: Amount of results (default: 50) (maximum: 250) - fields: comma-seperated list of fields to include in the response + fields: comma-separated list of fields to include in the response Returns: An array of gift cards. """ From f82cf279e8642ae3b995a4232cb67cebc211a8e5 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Mon, 27 Apr 2020 14:31:20 -0400 Subject: [PATCH 041/259] Update API versions --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index f9614b68..1e5eacf1 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -25,10 +25,10 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release('2019-04')) cls.define_version(Release('2019-07')) cls.define_version(Release('2019-10')) cls.define_version(Release('2020-01')) + cls.define_version(Release('2020-04')) @classmethod def clear_defined_versions(cls): From eaf4684ae4da4e9cfe8fe459b76e70c1610a34b9 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 28 Apr 2020 09:15:42 -0400 Subject: [PATCH 042/259] Release v8.0.0 --- CHANGELOG | 4 ++++ shopify/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 06a0f3b8..8dce80fd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index 3530b14b..7dc1ecd3 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '7.0.3' +VERSION = '8.0.0' From 18c2147c17c0abc95de3216625835655dc8f293a Mon Sep 17 00:00:00 2001 From: Thomas Slade Date: Thu, 21 May 2020 11:35:20 -0400 Subject: [PATCH 043/259] Adds billing example to readme --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 24b188b6..b0b7da5b 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,35 @@ these steps: shopify.ShopifyResource.clear_session() ``` +### Billing +_Note: Your application must be public to test the billing process. To test on a development store use the `'test': true` flag_ + +1. Create charge after session has been activated + ```python + application_charge = shopify.ApplicationCharge.create( + { + 'name': 'My public app', + 'price': 123, + 'test': True, + 'return_url': 'https://domain.com/approve' + } + ) + # Redirect user to confirmation_url so they can approve the charge + ``` + +2. After approving the charge, the user is redirected to `return_url` with `charge_id` + `GET` parameter + ```python + charge = shopify.ApplicationCharge.find(charge_id) + shopify.ApplicationCharge.activate(charge) + ``` + +3. Check that `activated_charge` status is `active` + ```python + activated_charge = shopify.ApplicationCharge.find(charge_id) + has_been_billed = activated_charge.status == 'active' + ``` + ### Advanced Usage It is recommended to have at least a basic grasp on the principles of [ActiveResource](https://apidock.com/rails/ActiveResource/Base). The [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which this package relies heavily upon is a port of rails/ActiveResource to Python. From cea8636fd79896372c4370ebe32804967d8a4af6 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 26 May 2020 14:13:57 -0400 Subject: [PATCH 044/259] Create SECURITY.md --- SECURITY.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..2d13b5e1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,59 @@ +# Security Policy + +## Supported versions + +### New features + +New features will only be added to the master branch and will not be made available in point releases. + +### Bug fixes + +Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. + +### Security issues + +Only the latest release series will receive patches and new versions in case of a security issue. + +### Severe security issues + +For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. + +### Unsupported Release Series + +When a release series is no longer supported, it's your own responsibility to deal with bugs and security issues. If you are not comfortable maintaining your own versions, you should upgrade to a supported version. + +## Reporting a bug + +All security bugs in shopify repositories should be reported to [our hackerone program](https://hackerone.com/shopify) +Shopify's whitehat program is our way to reward security researchers for finding serious security vulnerabilities in the In Scope properties listed at the bottom of this page, including our core application (all functionality associated with a Shopify store, particularly your-store.myshopify.com/admin) and certain ancillary applications. + +## Disclosure Policy + +We look forward to working with all security researchers and strive to be respectful, always assume the best and treat others as peers. We expect the same in return from all participants. To achieve this, our team strives to: + +- Reply to all reports within one business day and triage within two business days (if applicable) +- Be as transparent as possible, answering all inquires about our report decisions and adding hackers to duplicate HackerOne reports +- Award bounties within a week of resolution (excluding extenuating circumstances) +- Only close reports as N/A when the issue reported is included in Known Issues, Ineligible Vulnerabilities Types or lacks evidence of a vulnerability + +**The following rules must be followed in order for any rewards to be paid:** + +- You may only test against shops you have created which include your HackerOne YOURHANDLE @ wearehackerone.com registered email address. +- You must not attempt to gain access to, or interact with, any shops other than those created by you. +- The use of commercial scanners is prohibited (e.g., Nessus). +- Rules for reporting must be followed. +- Do not disclose any issues publicly before they have been resolved. +- Shopify reserves the right to modify the rules for this program or deem any submissions invalid at any time. Shopify may cancel the whitehat program without notice at any time. +- Contacting Shopify Support over chat, email or phone about your HackerOne report is not allowed. We may disqualify you from receiving a reward, or from participating in the program altogether. +- You are not an employee of Shopify; employees should report bugs to the internal bug bounty program. +- You hereby represent, warrant and covenant that any content you submit to Shopify is an original work of authorship and that you are legally entitled to grant the rights and privileges conveyed by these terms. You further represent, warrant and covenant that the consent of no other person or entity is or will be necessary for Shopify to use the submitted content. +- By submitting content to Shopify, you irrevocably waive all moral rights which you may have in the content. +- All content submitted by you to Shopify under this program is licensed under the MIT License. +- You must report any discovered vulnerability to Shopify as soon as you have validated the vulnerability. +- Failure to follow any of the foregoing rules will disqualify you from participating in this program. + +** Please see our [Hackerone Profile](https://hackerone.com/shopify) for full details + +## Receiving Security Updates + +To recieve all general updates to vulnerabilities, please subscribe to our hackerone [Hacktivity](https://hackerone.com/shopify/hacktivity) From 288b30be9bd1c051cfefed4d6e898478a1be4f9c Mon Sep 17 00:00:00 2001 From: Radek Novacek Date: Thu, 4 Jun 2020 13:48:39 +0200 Subject: [PATCH 045/259] Handle saving of product with no variants specified --- shopify/resources/product.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index 06bd29f0..e08c1086 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -36,9 +36,10 @@ def save(self): api_version = ShopifyResource.version if api_version and ( api_version.strip('-') >= start_api_version) and api_version != 'unstable': - 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 'variants' in self.product.attributes: + for variant in self.variants: + if 'inventory_quantity' in variant.attributes: + del variant.attributes['inventory_quantity'] + if 'old_inventory_quantity' in variant.attributes: + del variant.attributes['old_inventory_quantity'] return super(ShopifyResource, self).save() From 195e614e8421face1741ffeca4eb5c0ebbd7c7a5 Mon Sep 17 00:00:00 2001 From: Anmol Gulati Date: Fri, 5 Jun 2020 18:20:22 +0530 Subject: [PATCH 046/259] Add fulfillment orders resource --- shopify/resources/__init__.py | 2 +- shopify/resources/fulfillment.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 8233cb1f..ce91cf3c 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -37,7 +37,7 @@ from .page import Page from .country import Country from .refund import Refund -from .fulfillment import Fulfillment +from .fulfillment import Fulfillment, FulfillmentOrders from .fulfillment_service import FulfillmentService from .carrier_service import CarrierService from .transaction import Transaction diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index 6d79fdaa..c0b5e7e7 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -12,3 +12,7 @@ def complete(self): def open(self): self._load_attributes_from_response(self.post("open")) + + +class FulfillmentOrders(ShopifyResource): + _prefix_source = "/orders/$order_id/" From 0fdefb3f6647276dacc02b7076bde71273f3d088 Mon Sep 17 00:00:00 2001 From: Andrew Swait Date: Sun, 5 Jul 2020 11:41:37 +0100 Subject: [PATCH 047/259] Update README.md docs: fix indentation error in GraphQL example --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b0b7da5b..9d0d2bfd 100644 --- a/README.md +++ b/README.md @@ -335,15 +335,15 @@ This library also supports Shopify's new [GraphQL API](https://help.shopify.com/ ``` client = shopify.GraphQL() - query = ''' - { - shop { - name - id - } +query = ''' + { + shop { + name + id } - ''' - result = client.execute(query) + } +''' +result = client.execute(query) ``` From add3879a0363c119f4759cbd7ec49e11e7645e89 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 21 Jul 2020 09:49:32 -0400 Subject: [PATCH 048/259] Update API version --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 1e5eacf1..c4b85ae9 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -25,10 +25,10 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release('2019-07')) cls.define_version(Release('2019-10')) cls.define_version(Release('2020-01')) cls.define_version(Release('2020-04')) + cls.define_version(Release('2020-07')) @classmethod def clear_defined_versions(cls): From 2a4fdf9cf81b89133feddfd35e5f2222d0521f92 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Tue, 21 Jul 2020 09:56:20 -0400 Subject: [PATCH 049/259] Release v8.0.1 --- CHANGELOG | 4 ++++ shopify/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8dce80fd..b4671bc5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== Version 8.0.1 +- release api version 2020-07 +- deprecate api version 2019-07 + == Version 8.0.0 - release api version 2020-04 - deprecate api version 2019-04 diff --git a/shopify/version.py b/shopify/version.py index 7dc1ecd3..06657676 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.0.0' +VERSION = '8.0.1' From 9e289611ab9f7fffba8bdb828ba27755690eb669 Mon Sep 17 00:00:00 2001 From: Alexander Sfakianos Date: Mon, 10 Aug 2020 19:29:38 +0000 Subject: [PATCH 050/259] Amended pull request #389 to allow saving of products with no variants --- shopify/resources/product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index e08c1086..b8bc3c70 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -36,7 +36,7 @@ def save(self): api_version = ShopifyResource.version if api_version and ( api_version.strip('-') >= start_api_version) and api_version != 'unstable': - if 'variants' in self.product.attributes: + if 'variants' in self.attributes: for variant in self.variants: if 'inventory_quantity' in variant.attributes: del variant.attributes['inventory_quantity'] From 997c1a02ca52b9498c12e401e2c259cdd6c347e9 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 13 Aug 2020 14:01:26 -0400 Subject: [PATCH 051/259] Release 8.0.2 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b4671bc5..f0bfacec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 8.0.2 +- Patch for product updating with variants + == Version 8.0.1 - release api version 2020-07 - deprecate api version 2019-07 diff --git a/shopify/version.py b/shopify/version.py index 06657676..980383a8 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.0.1' +VERSION = '8.0.2' From 16208583c9b4f04f8559e2519c590565c2ea420a Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Tue, 22 Sep 2020 10:22:45 -0400 Subject: [PATCH 052/259] Add dev.yml for setup --- dev.yml | 12 ++++++++++++ requirements.txt | 1 + 2 files changed, 13 insertions(+) create mode 100644 dev.yml create mode 100644 requirements.txt diff --git a/dev.yml b/dev.yml new file mode 100644 index 00000000..6c37588a --- /dev/null +++ b/dev.yml @@ -0,0 +1,12 @@ +--- +name: shopify-python-api + +type: python + +up: + - python: 3.8.0 + - pip: + - requirements.txt + +commands: + test: python setup.py test diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..49fe098d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +setuptools From d38a529bc1c36dde0136a6b7b1e8bdd7b31d90e8 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Tue, 22 Sep 2020 10:49:51 -0400 Subject: [PATCH 053/259] Update Release notes regarding testing against stable API --- RELEASING | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/RELEASING b/RELEASING index 68ccd2b0..f84fa314 100644 --- a/RELEASING +++ b/RELEASING @@ -1,18 +1,20 @@ Releasing shopify_python_api -1. Check the Semantic Versioning page for info on how to version the new release: http://semver.org +1. Verify that the examples in the README are still valid against the latest stable API release. -2. Update version in shopify/version.py +2. Check the Semantic Versioning page for info on how to version the new release: http://semver.org -3. Update CHANGELOG entry for the release. +3. Update version in shopify/version.py -4. Commit the changes +4. Update CHANGELOG entry for the release. + +5. Commit the changes git commit -m "Release vX.Y.Z" -5. Tag the release with the version +6. Tag the release with the version git tag -m "Release X.Y.Z" vX.Y.Z -6. Push the changes to github +7. Push the changes to github git push --tags origin master -7. Shipit! +8. Shipit! From fe0f67c44830542b523dc75f690887e40b9a257c Mon Sep 17 00:00:00 2001 From: Chase Anderson Date: Tue, 22 Sep 2020 09:42:26 -0600 Subject: [PATCH 054/259] Corrected use of renamed function `build_collection` that was `build_list` in GiftCard class --- shopify/resources/gift_card.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/resources/gift_card.py b/shopify/resources/gift_card.py index d933ac01..6067c7f3 100644 --- a/shopify/resources/gift_card.py +++ b/shopify/resources/gift_card.py @@ -21,7 +21,7 @@ def search(cls, **kwargs): Returns: An array of gift cards. """ - return cls._build_list(cls.get("search", **kwargs)) + return cls._build_collection(cls.get("search", **kwargs)) def add_adjustment(self, adjustment): """ From 2ad8ae789d84c123d5f9831ef0404394599d0050 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Tue, 22 Sep 2020 12:04:32 -0400 Subject: [PATCH 055/259] Add issue and PR templates --- .github/ISSUE_TEMPLATE.md | 37 ++++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 21 ++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..752d42cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,37 @@ +# 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. +1. +1. + +## 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). + + + +## Specifications + +- `shopify_python_api` version: +- `pyactiveresource` version: +- Shopify API version used (e.g. `'2020-07'`): diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..31aa54dd --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,21 @@ + + +### WHY are these changes introduced? + +Fixes #0000 + + + +### WHAT is this pull request doing? + + From bf2c34bcc1698a1db580955dba32a4482b6f3524 Mon Sep 17 00:00:00 2001 From: Chase Anderson Date: Tue, 22 Sep 2020 10:56:20 -0600 Subject: [PATCH 056/259] Made gift_cards test fixture contain gift cards (emphasis on plurality), and added test for gift card search --- test/fixtures/gift_cards_search.json | 20 ++++++++++++++++++++ test/gift_card_test.py | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 test/fixtures/gift_cards_search.json diff --git a/test/fixtures/gift_cards_search.json b/test/fixtures/gift_cards_search.json new file mode 100644 index 00000000..ebdc4adc --- /dev/null +++ b/test/fixtures/gift_cards_search.json @@ -0,0 +1,20 @@ +{ + "gift_cards":[{ + "api_client_id": null, + "balance": "10.00", + "created_at": "2020-05-11T10:16:40+10:00", + "currency": "USD", + "customer_id": null, + "disabled_at": null, + "expires_on": null, + "id": 4208209, + "initial_value": "25.00", + "line_item_id": null, + "note": "balance10", + "template_suffix": null, + "updated_at": "2020-05-11T15:13:15+10:00", + "user_id": 123456, + "last_characters":"c294", + "order_id":null + }] +} diff --git a/test/gift_card_test.py b/test/gift_card_test.py index 703feda5..ca209090 100644 --- a/test/gift_card_test.py +++ b/test/gift_card_test.py @@ -33,3 +33,9 @@ def test_adjust_gift_card(self): })) 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')) + + results = shopify.GiftCard.search(query='balance:10') + self.assertEqual(results[0].balance, "10.00") \ No newline at end of file From 1788b36fbce8a10c0b7e8a59c0dfc837c45192bd Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Tue, 22 Sep 2020 16:39:00 -0400 Subject: [PATCH 057/259] Release v8.0.3 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f0bfacec..21d54aee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 8.0.3 +- Patch for replacing call to _build_list() with _build_collection() in gift_card.py + == Version 8.0.2 - Patch for product updating with variants diff --git a/shopify/version.py b/shopify/version.py index 980383a8..f961caf7 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.0.2' +VERSION = '8.0.3' From 434e35a7d49f102241f7592da61187d533472ed3 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Wed, 23 Sep 2020 10:28:38 -0400 Subject: [PATCH 058/259] Add support for upcoming 2020-10 release, drop 2019-10 --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index c4b85ae9..39ae8fb8 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -25,10 +25,10 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release('2019-10')) cls.define_version(Release('2020-01')) cls.define_version(Release('2020-04')) cls.define_version(Release('2020-07')) + cls.define_version(Release('2020-10')) @classmethod def clear_defined_versions(cls): From ce9b61dc130997246b95c2dd74811fa098800bda Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 1 Oct 2020 12:40:59 -0400 Subject: [PATCH 059/259] Updating README Updating readme authentication process and graphql access Also fixing the console setup --- README.md | 372 +++++++++++------------------------------------------- 1 file changed, 75 insertions(+), 297 deletions(-) diff --git a/README.md b/README.md index 9d0d2bfd..986ca216 100644 --- a/README.md +++ b/README.md @@ -3,266 +3,119 @@ # Shopify API -The ShopifyAPI library allows Python developers to programmatically -access the admin section of stores. - -The API is accessed using pyactiveresource in order to provide an -interface similar to the -[ruby Shopify API](https://github.com/shopify/shopify_api) gem. -The data itself is sent as XML over HTTP to communicate with Shopify, -which provides a web service that follows the REST principles as -much as possible. - -## !! Breaking change notice for version 5.0.0 !! - -### Changes to ShopifyAPI::Session -Session creation requires a `version` to be set, see [Api Versioning at Shopify](https://help.shopify.com/en/api/versioning). - -To upgrade your use of ShopifyAPI you will need to make the following changes. - -```python -shopify.Session(domain, token) -``` -is now -```python -shopify.Session(domain, version, token) -``` - -The `version` argument takes a string for any known version and correctly coerce it to a `shopify.ApiVersion`. You can find the currently defined versions [here](https://github.com/Shopify/shopify_python_api/blob/master/shopify/api_version.py#L27). - -For example if you want to use the `2019-04` version you would create a session like this: -```python -session = shopify.Session(domain, '2019-04', token) -``` -if you want to use the `unstable` version you would create a session like this: -```python -session = shopify.Session(domain, 'unstable', token) -``` - -### URLs that have not changed - -- OAuth URLs for `authorize`, getting the `access_token` from a code, and using a `refresh_token` have _not_ changed. - - get: `/admin/oauth/authorize` - - post: `/admin/oauth/access_token` - - get: `/admin/oauth/access_scopes` -- URLs for the merchant’s web admin have _not_ changed. For example: to send the merchant to the product page the url is still `/admin/product/` +The ShopifyAPI library allows Python developers to programmatically access the admin section of stores. ## Usage ### Requirements - -All API usage happens through Shopify applications, created by -either shop owners for their own shops, or by Shopify Partners for -use by other shop owners: - -* Shop owners can create applications for themselves through their -own admin: -* Shopify Partners create applications through their admin: - - -For more information and detailed documentation about the API visit - +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. ### Installation -To easily install or upgrade to the latest release, use -[pip](http://www.pip-installer.org/) +To easily install or upgrade to the latest release, use [pip](http://www.pip-installer.org/). ```shell pip install --upgrade ShopifyAPI ``` -or [easy_install](http://packages.python.org/distribute/easy_install.html) - -```shell -easy_install -U ShopifyAPI -``` - ### Getting Started +#### Public and Custom Apps -ShopifyAPI uses pyactiveresource to communicate with the REST web -service. pyactiveresource has to be configured with a fully authorized -URL of a particular store first. To obtain that URL you can follow -these steps: - -1. First create a new application in either the partners admin or your store - admin. You will need an API_VERSION equal to a valid version string of a - [Shopify API Version](https://help.shopify.com/en/api/versioning). For a - private App you'll need the API_KEY and the PASSWORD otherwise you'll need - the API_KEY and SHARED_SECRET. - -2. For a private App you just need to set the base site url as - follows: - - ```python - shop_url = "https://%s:%s@SHOP_NAME.myshopify.com/admin/api/%s" % (API_KEY, PASSWORD, API_VERSION) - shopify.ShopifyResource.set_site(shop_url) - ``` - - That's it you're done, skip to step 6 and start using the API! - For a partner App you will need to supply two parameters to the - Session class before you instantiate it: - +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. We then need to supply these keys to the Shopify Session Class so that it knows how to authenticate. ```python - shopify.Session.setup(api_key=API_KEY, secret=SHARED_SECRET) + 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: -3. In order to access a shop's data, apps need an access token from that - specific shop. This is a two-stage process. Before interacting with - a shop for the first time an app should redirect the user to the - following URL: - - `GET https://SHOP_NAME.myshopify.com/admin/oauth/authorize` - - with the following parameters: - -* ``client_id``– Required – The API key for your app - -* ``scope`` – Required – The list of required scopes (explained here: http://docs.shopify.com/api/tutorials/oauth) - -* ``redirect_uri`` – Required – The URL where you want to redirect the users after they authorize the client. The complete URL specified here must be identical to one of the Application Redirect URLs set in the App's section of the Partners dashboard. Note: in older applications, this parameter was optional, and redirected to the Application Callback URL when no other value was specified. - -* ``state`` – Optional – A randomly selected value provided by your application, which is unique for each authorization request. During the OAuth callback phase, your application must check that this value matches the one you provided during authorization. [This mechanism is important for the security of your application](https://tools.ietf.org/html/rfc6819#section-3.6). - - We've added the create_permision_url method to make this easier, first - instantiate your session object: - - ```python - session = shopify.Session("SHOP_NAME.myshopify.com") - ``` - - Then call: - - ```python - scope=["write_products"] - permission_url = session.create_permission_url(scope) - ``` - - or if you want a custom redirect_uri: - - ```python - permission_url = session.create_permission_url(scope, "https://my_redirect_uri.com") - ``` - -4. Once authorized, the shop redirects the owner to the return URL of your application with a parameter named 'code'. This is a temporary token that the app can exchange for a permanent access token. - - Before you proceed, make sure your application performs the following security checks. If any of the checks fails, your application must reject the request with an error, and must not proceed further. - - * Ensure the provided ``state`` is the same one that your application provided to Shopify during Step 3. - * Ensure the provided hmac is valid. The hmac is signed by Shopify as explained below, in the Verification section. - * Ensure the provided hostname parameter is a valid hostname, ends with myshopify.com, and does not contain characters other than letters (a-z), numbers (0-9), dots, and hyphens. - - If all security checks pass, the authorization code can be exchanged once for a permanent access token. The exchange is made with a request to the shop. - - ``` - POST https://SHOP_NAME.myshopify.com/admin/oauth/access_token - ``` - - with the following parameters: - - ``` - * client_id – Required – The API key for your app - * client_secret – Required – The shared secret for your app - * code – Required – The code you received in step 3 - ``` - - and you'll get your permanent access token back in the response. - - There is a method to make the request and get the token for you. Pass - all the params received from the previous call (shop, code, timestamp, - signature) as a dictionary and the method will verify - the params, extract the temp code and then request your token: +``` +shop_url = "SHOP_NAME.myshopify.com" +api_version = '2020-10' +state = binascii.b2a_hex(os.urandom(15)).decode("utf-8") +redirect_uri = "http://myapp.com/auth/shopify/callback" +scopes = ['read_products', 'read_orders'] - ```python - token = session.request_token(params) - ``` +newSession = shopify.Session(shop_url, api_version) +auth_url = newSession.create_permission_url(scopes, redirect_uri, state) +# redirect to auth_url +``` - This method will save the token to the session object - and return it. For future sessions simply pass the token when - creating the session object. +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: ```python - session = shopify.Session("SHOP_NAME.myshopify.com", token) + session = shopify.Session(shop_url, api_version) + access_token = session.request_token(request_params) + # you should save the access token now for future use. ``` -5. The session must be activated before use: +1. Now you're ready to make authorized API requests to your shop!: ```python + session = shopify.Session(shop_url, api_version, access_token) shopify.ShopifyResource.activate_session(session) - ``` - -6. Now you're ready to make authorized API requests to your shop! - Data is returned as [ActiveResource](https://github.com/Shopify/pyactiveresource) instances: + + shop = shopify.Shop.current() # Get the current shop + product = shopify.Product.find(179761209) # Get a specific product - ```python - # Get the current shop - shop = shopify.Shop.current() - - # Get a specific product - product = shopify.Product.find(179761209) - - # Create a new product - new_product = shopify.Product() - new_product.title = "Burton Custom Freestyle 151" - new_product.product_type = "Snowboard" - new_product.vendor = "Burton" - success = new_product.save() #returns false if the record is invalid - # or - if new_product.errors: - #something went wrong, see new_product.errors.full_messages() for example - - # Update a product - product.handle = "burton-snowboard" - product.save() - - # Remove a product - product.destroy() + # execute a graphQL call + shopify.GraphQL().execute("{ shop { name id } }") ``` - Alternatively, you can use temp to initialize a Session and execute a command which also handles temporarily setting ActiveResource::Base.site: + Alternatively, you can use temp to initialize a Session and execute a command: ```python - with shopify.Session.temp("SHOP_NAME.myshopify.com", token): + with shopify.Session.temp(shop_url, api_version, token): product = shopify.Product.find() ``` -7. If you want to work with another shop, you'll first need to clear the session:: +1. It is best practice to clear your session when you're done: ```python shopify.ShopifyResource.clear_session() ``` + +#### Private Apps +Private apps are a bit quicker to use because OAuth is not needed. You can create the private app in the Shopify Merchant Admin. You can use the Private App password as your `access_token`: + +##### With full session +```python +session = shopify.Session(shop_url, api_version, private_app_password) +shopify.ShopifyResource.activate_session(session) +``` + +##### With temporary session + +```python +with shopify.Session.temp(shop_url, api_version, private_app_password): + shopify.GraphQL().execute("{ shop { name id } }") +``` ### Billing _Note: Your application must be public to test the billing process. To test on a development store use the `'test': true` flag_ 1. Create charge after session has been activated ```python - application_charge = shopify.ApplicationCharge.create( - { - 'name': 'My public app', - 'price': 123, - 'test': True, - 'return_url': 'https://domain.com/approve' - } - ) - # Redirect user to confirmation_url so they can approve the charge + application_charge = shopify.ApplicationCharge.create({ + 'name': 'My public app', + 'price': 123, + 'test': True, + 'return_url': 'https://domain.com/approve' + }) + # Redirect user to application_charge.confirmation_url so they can approve the charge ``` - -2. After approving the charge, the user is redirected to `return_url` with `charge_id` - `GET` parameter +1. After approving the charge, the user is redirected to `return_url` with `charge_id` parameter ```python charge = shopify.ApplicationCharge.find(charge_id) shopify.ApplicationCharge.activate(charge) ``` - -3. Check that `activated_charge` status is `active` +1. Check that `activated_charge` status is `active` ```python activated_charge = shopify.ApplicationCharge.find(charge_id) has_been_billed = activated_charge.status == 'active' ``` ### Advanced Usage -It is recommended to have at least a basic grasp on the principles of [ActiveResource](https://apidock.com/rails/ActiveResource/Base). The [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which this package relies heavily upon is a port of rails/ActiveResource to Python. +It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which this package relies heavily upon is a port of rails/ActiveResource to Python. Instances of `pyactiveresource` resources map to RESTful resources in the Shopify API. @@ -273,129 +126,57 @@ product = shopify.Product() product.title = "Shopify Logo T-Shirt" product.id # => 292082188312 product.save() # => True - shopify.Product.exists(product.id) # => True - product = shopify.Product.find(292082188312) # Resource holding our newly created Product object # Inspect attributes with product.attributes - product.price = 19.99 product.save() # => True product.destroy() # Delete the resource from the remote server (i.e. Shopify) ``` -The [tests for this package](https://github.com/Shopify/shopify_python_api/tree/master/test) also serve to provide advanced examples of usage. - ### Prefix options -Some resources such as `Fulfillment` are prefixed by a parent resource in the Shopify API. - -e.g. `orders/450789469/fulfillments/255858046` +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. -In order to interact with these resources, you must specify the identifier of the parent resource in your request. - -e.g. `shopify.Fulfillment.find(255858046, order_id=450789469)` +```python +shopify.Fulfillment.find(255858046, order_id=450789469) +``` ### Console - This package also includes the `shopify_api.py` script to make it easy to open up an interactive console to use the API with a shop. - -1. Obtain a private API key and password to use with your shop - (step 2 in "Getting Started") - -2. Use the `shopify_api.py` script to save the credentials for the - shop to quickly log in. The script uses [PyYAML](http://pyyaml.org/) to save - and load connection configurations in the same format as the ruby - shopify\_api. - - ```shell - shopify_api.py add yourshopname - ``` - - Follow the prompts for the shop domain, API key and password. - -3. Start the console for the connection. - - ```shell - shopify_api.py console - ``` - -4. To see the full list of commands, type: - - ```shell - shopify_api.py help - ``` +1. Obtain a private API key and password to use with your shop (step 2 in "Getting Started") +1. Save your default credentials: `shopify_api.py add yourshopname` +1. Start the console for the connection: `shopify_api.py console` +1. To see the full list of commands, type: `shopify_api.py help` ### GraphQL -This library also supports Shopify's new [GraphQL API](https://help.shopify.com/en/api/graphql-admin-api). The authentication process (steps 1-5 under Getting Started) is identical. Once your session is activated, simply construct a new graphql client and use `execute` to execute the query. +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. -``` -client = shopify.GraphQL() -query = ''' - { - shop { - name - id - } - } -''' -result = client.execute(query) +```python +result = shopify.GraphQL().execute('{ shop { name id } }') ``` ## Using Development Version -The development version can be built using - +#### Building and installing dev version ```shell python setup.py sdist -``` - -then the package can be installed using pip - -```shell pip install --upgrade dist/ShopifyAPI-*.tar.gz ``` -or easy_install - -```shell -easy_install -U dist/ShopifyAPI-*.tar.gz -``` - -Note Use the `bin/shopify_api.py` script when running from the source tree. -It will add the lib directory to start of sys.path, so the installed -version won't be used. - -To run tests, simply open up the project directory in a terminal and run: +**Note** Use the `bin/shopify_api.py` script when running from the source tree. It will add the lib directory to start of sys.path, so the installed version won't be used. +#### Running Tests ```shell pip install setuptools --upgrade python setup.py test ``` -Alternatively, use [tox](http://tox.readthedocs.org/en/latest/) to -sequentially test against different versions of Python in isolated -environments: - -```shell -pip install tox -tox -``` - -See the tox documentation for help on running only specific environments -at a time. The related tool [detox](https://pypi.python.org/pypi/detox) -can be used to run tests in these environments in parallel: - -```shell -pip install detox -detox -``` - ## Relative Cursor Pagination Cursor based pagination support has been added in 6.0.0. @@ -419,10 +200,7 @@ Currently there is no support for: * persistent connections ## Additional Resources - -* [Shopify API](http://api.shopify.com) <= Read the tech docs! +* [Partners Dashboard](https://partners.shopify.com) +* [developers.shopify.com](https://developers.shopify.com) +* [Shopify.dev](https://shopify.dev) <= Read the tech docs! * [Ask questions on the Shopify forums](http://ecommerce.shopify.com/c/shopify-apis-and-technology) <= Ask questions on the forums! - -## Copyright - -Copyright (c) 2012 "Shopify inc.". See LICENSE for details. From ba670732f5a34684eb22e83bc147695eca3f74cd Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 1 Oct 2020 12:41:34 -0400 Subject: [PATCH 060/259] Fixing console api version setting --- scripts/shopify_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 26f718d3..60f48065 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -92,7 +92,7 @@ def help(cls, task=None): class Tasks(object): _shop_config_dir = os.path.join(os.environ["HOME"], ".shopify", "shops") _default_symlink = os.path.join(_shop_config_dir, "default") - _default_api_version = "2019-04" + _default_api_version = "unstable" @classmethod @usage("list") @@ -120,6 +120,9 @@ def add(cls, connection): 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: From ea2b6516b41db6dd4955c2ca06686ec120520cc6 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 1 Oct 2020 13:22:11 -0400 Subject: [PATCH 061/259] Formatting --- README.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 986ca216..f3402ebb 100644 --- a/README.md +++ b/README.md @@ -23,28 +23,29 @@ pip install --upgrade ShopifyAPI 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. We then need to supply these keys to the Shopify Session Class so that it knows how to authenticate. - ```python - shopify.Session.setup(api_key=API_KEY, secret=API_SECRET) - ``` + + ```python + 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: -``` -shop_url = "SHOP_NAME.myshopify.com" -api_version = '2020-10' -state = binascii.b2a_hex(os.urandom(15)).decode("utf-8") -redirect_uri = "http://myapp.com/auth/shopify/callback" -scopes = ['read_products', 'read_orders'] - -newSession = shopify.Session(shop_url, api_version) -auth_url = newSession.create_permission_url(scopes, redirect_uri, state) -# redirect to auth_url -``` + ```python + shop_url = "SHOP_NAME.myshopify.com" + api_version = '2020-10' + state = binascii.b2a_hex(os.urandom(15)).decode("utf-8") + redirect_uri = "http://myapp.com/auth/shopify/callback" + scopes = ['read_products', 'read_orders'] + + newSession = shopify.Session(shop_url, api_version) + auth_url = newSession.create_permission_url(scopes, redirect_uri, state) + # redirect to auth_url + ``` 1. Once the merchant accepts, the shop redirects the owner to the `redirect_uri` of your application with a parameter named 'code'. This is a temporary token that the app can exchange for a permanent access token. You should compare the state you provided above with the one you 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: ```python session = shopify.Session(shop_url, api_version) - access_token = session.request_token(request_params) + access_token = session.request_token(request_params) # request_token will validate hmac and timing attacks # you should save the access token now for future use. ``` @@ -53,7 +54,7 @@ auth_url = newSession.create_permission_url(scopes, redirect_uri, state) ```python session = shopify.Session(shop_url, api_version, access_token) shopify.ShopifyResource.activate_session(session) - + shop = shopify.Shop.current() # Get the current shop product = shopify.Product.find(179761209) # Get a specific product @@ -73,7 +74,7 @@ auth_url = newSession.create_permission_url(scopes, redirect_uri, state) ```python shopify.ShopifyResource.clear_session() ``` - + #### Private Apps Private apps are a bit quicker to use because OAuth is not needed. You can create the private app in the Shopify Merchant Admin. You can use the Private App password as your `access_token`: From f9265c4bf670fa15455ba3d897cd9e6a4f1eee61 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 1 Oct 2020 15:15:21 -0400 Subject: [PATCH 062/259] Review changes --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f3402ebb..415d5b68 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ pip install --upgrade ShopifyAPI product = shopify.Product.find() ``` -1. It is best practice to clear your session when you're done: +1. It is best practice to clear your session when you're done. A temporary session does this automatically: ```python shopify.ShopifyResource.clear_session() @@ -82,6 +82,8 @@ Private apps are a bit quicker to use because OAuth is not needed. You can creat ```python session = shopify.Session(shop_url, api_version, private_app_password) shopify.ShopifyResource.activate_session(session) +# ... +shopify.ShopifyResource.clear_session() ``` ##### With temporary session @@ -92,7 +94,7 @@ with shopify.Session.temp(shop_url, api_version, private_app_password): ``` ### Billing -_Note: Your application must be public to test the billing process. To test on a development store use the `'test': true` flag_ +_Note: Your application must be public to test the billing process. To test on a development store use the `'test': True` flag_ 1. Create charge after session has been activated ```python @@ -116,7 +118,7 @@ _Note: Your application must be public to test the billing process. To test on a ``` ### Advanced Usage -It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which this package relies heavily upon is a port of rails/ActiveResource to Python. +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. @@ -139,15 +141,14 @@ product.destroy() ### 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. +Some resources such as `Fulfillment` are prefixed by a parent resource in the Shopify API (e.g. `orders/450789469/fulfillments/255858046`). In order to interact with these resources, you must specify the identifier of the parent resource in your request. ```python shopify.Fulfillment.find(255858046, order_id=450789469) ``` ### Console -This package also includes the `shopify_api.py` script to make it easy to -open up an interactive console to use the API with a shop. +This package also includes the `shopify_api.py` script to make it easy to open an interactive console to use the API with a shop. 1. Obtain a private API key and password to use with your shop (step 2 in "Getting Started") 1. Save your default credentials: `shopify_api.py add yourshopname` 1. Start the console for the connection: `shopify_api.py console` From ea211ca6439c5478f8e76c9dba10528c5e0bc6a3 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Thu, 1 Oct 2020 16:53:09 -0400 Subject: [PATCH 063/259] Update the byline --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 415d5b68..990a4f7b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ +# Shopify API + [![Build Status](https://travis-ci.org/Shopify/shopify_python_api.svg?branch=master)](https://travis-ci.org/Shopify/shopify_python_api) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) -# Shopify API - -The ShopifyAPI library allows Python developers to programmatically access the admin section of stores. +The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library ## Usage From e986fe6489240610868c84ed0026d155a3b61152 Mon Sep 17 00:00:00 2001 From: Tim Anema Date: Fri, 2 Oct 2020 11:03:30 -0400 Subject: [PATCH 064/259] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 990a4f7b..5f31a693 100644 --- a/README.md +++ b/README.md @@ -204,5 +204,5 @@ Currently there is no support for: ## Additional Resources * [Partners Dashboard](https://partners.shopify.com) * [developers.shopify.com](https://developers.shopify.com) -* [Shopify.dev](https://shopify.dev) <= Read the tech docs! -* [Ask questions on the Shopify forums](http://ecommerce.shopify.com/c/shopify-apis-and-technology) <= Ask questions on the forums! +* [Shopify.dev](https://shopify.dev) +* [Ask questions on the Shopify forums](http://ecommerce.shopify.com/c/shopify-apis-and-technology) From ca558ef2df49123587a5a2fafa00a0681625bb82 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Tue, 13 Oct 2020 09:48:11 -0400 Subject: [PATCH 065/259] Release v8.0.4 --- CHANGELOG | 4 ++++ shopify/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 21d54aee..de7ae9d2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index f961caf7..f68dd5f5 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.0.3' +VERSION = '8.0.4' From f78708392c3f22e107bb476a7862ede947bdd432 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Wed, 14 Oct 2020 09:09:38 -0400 Subject: [PATCH 066/259] Add missing changelog entry for FulfillmentOrder resource --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index de7ae9d2..0918bdc2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ == 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 From 8fb55f29aa03eca55055465f2b9410f8da3b87b1 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Wed, 14 Oct 2020 09:11:47 -0400 Subject: [PATCH 067/259] Add changelog reminder to pull request template To help avoid missing entries such as https://github.com/Shopify/shopify_python_api/pull/421 --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 31aa54dd..9042906a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,3 +19,7 @@ Fixes #0000 + +### Checklist + +- [ ] I have updated the CHANGELOG (if applicable) From c60e6ec9da17bbddfefcfcbc12ce6b151fe4a70a Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 19 Oct 2020 09:36:05 -0400 Subject: [PATCH 068/259] Add link to Shopify Python guide --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9042906a..0bef098d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,3 +23,4 @@ Fixes #0000 ### Checklist - [ ] I have updated the CHANGELOG (if applicable) +- [ ] I have followed the [Shopify Python](https://github.com/Shopify/shopify_python) guide From 86b3dde028f1954aba2be20e41628a760de396cd Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 5 Nov 2020 15:35:48 +0530 Subject: [PATCH 069/259] Add support for Shopify Payments resources --- shopify/resources/__init__.py | 4 + shopify/resources/balance.py | 6 + shopify/resources/disputes.py | 6 + shopify/resources/payouts.py | 6 + shopify/resources/transactions.py | 6 + test/balance_test.py | 13 + test/disputes_test.py | 17 + test/fixtures/balance.json | 8 + test/fixtures/disputes.json | 102 ++++++ test/fixtures/payouts.json | 118 +++++++ test/fixtures/payouts_transactions.json | 404 ++++++++++++++++++++++++ test/payouts_test.py | 18 ++ test/transactions_test.py | 12 + 13 files changed, 720 insertions(+) create mode 100644 shopify/resources/balance.py create mode 100644 shopify/resources/disputes.py create mode 100644 shopify/resources/payouts.py create mode 100644 shopify/resources/transactions.py create mode 100644 test/balance_test.py create mode 100644 test/disputes_test.py create mode 100644 test/fixtures/balance.json create mode 100644 test/fixtures/disputes.json create mode 100644 test/fixtures/payouts.json create mode 100644 test/fixtures/payouts_transactions.json create mode 100644 test/payouts_test.py create mode 100644 test/transactions_test.py diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index ce91cf3c..8f978535 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -45,6 +45,10 @@ from .image import Image from .variant import Variant from .order import Order +from .balance import Balance +from .disputes import Disputes +from .payouts import Payouts +from .transactions import Transactions from .order_risk import OrderRisk from .policy import Policy from .smart_collection import SmartCollection diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py new file mode 100644 index 00000000..3d9121c5 --- /dev/null +++ b/shopify/resources/balance.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Balance(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/disputes.py b/shopify/resources/disputes.py new file mode 100644 index 00000000..f098fd0f --- /dev/null +++ b/shopify/resources/disputes.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Disputes(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/payouts.py b/shopify/resources/payouts.py new file mode 100644 index 00000000..dea162d8 --- /dev/null +++ b/shopify/resources/payouts.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Payouts(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/transactions.py b/shopify/resources/transactions.py new file mode 100644 index 00000000..90cb884f --- /dev/null +++ b/shopify/resources/transactions.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Transactions(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/balance/" diff --git a/test/balance_test.py b/test/balance_test.py new file mode 100644 index 00000000..ad6719dc --- /dev/null +++ b/test/balance_test.py @@ -0,0 +1,13 @@ +import shopify +from test.test_helper import TestCase + + +class BalanceTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_balance(self): + self.fake('balance', method='GET', prefix=self.prefix, body=self.load_fixture('balance')) + balance = shopify.Balance.find() + self.assertGreater(len(balance), 0) + self.assertEqual('USD', balance['currency']) + self.assertEqual('53.99', balance['amount']) diff --git a/test/disputes_test.py b/test/disputes_test.py new file mode 100644 index 00000000..44dbb689 --- /dev/null +++ b/test/disputes_test.py @@ -0,0 +1,17 @@ +import shopify +from test.test_helper import TestCase + + +class DisputeTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_dispute(self): + 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('disputes')) + disputes = shopify.Disputes.find(1052608616) + self.assertEqual('won', disputes.status) diff --git a/test/fixtures/balance.json b/test/fixtures/balance.json new file mode 100644 index 00000000..8053cb99 --- /dev/null +++ b/test/fixtures/balance.json @@ -0,0 +1,8 @@ +{ + "balance": [ + { + "currency": "USD", + "amount": "53.99" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/disputes.json b/test/fixtures/disputes.json new file mode 100644 index 00000000..656d27ff --- /dev/null +++ b/test/fixtures/disputes.json @@ -0,0 +1,102 @@ +{ + "disputes": [ + { + "id": 1052608616, + "order_id": null, + "type": "chargeback", + "amount": "100.00", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 815713555, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "credit_not_processed", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 782360659, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 670893524, + "order_id": 625362839, + "type": "inquiry", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 598735659, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 85190714, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "under_review", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": "2020-11-04T19:00:00-05:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 35982383, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "subscription_canceled", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/payouts.json b/test/fixtures/payouts.json new file mode 100644 index 00000000..b8e247b7 --- /dev/null +++ b/test/fixtures/payouts.json @@ -0,0 +1,118 @@ +{ + "payouts": [ + { + "id": 854088011, + "status": "scheduled", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 512467833, + "status": "failed", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 39438702, + "status": "in_transit", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 710174591, + "status": "paid", + "date": "2019-12-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 974708905, + "status": "paid", + "date": "2019-11-13", + "currency": "CAD", + "amount": "51.69", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "6.46", + "charges_gross_amount": "58.15", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 623721858, + "status": "paid", + "date": "2019-11-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + } + ] +} \ No newline at end of file diff --git a/test/fixtures/payouts_transactions.json b/test/fixtures/payouts_transactions.json new file mode 100644 index 00000000..aa70797c --- /dev/null +++ b/test/fixtures/payouts_transactions.json @@ -0,0 +1,404 @@ +{ + "transactions": [ + { + "id": 699519475, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-50.00", + "fee": "0.00", + "net": "-50.00", + "source_id": 460709370, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-05T19:52:08-05:00" + }, + { + "id": 77412310, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "50.00", + "fee": "0.00", + "net": "50.00", + "source_id": 374511569, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-05T19:52:08-05:00" + }, + { + "id": 1006917261, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-3.45", + "fee": "0.00", + "net": "-3.45", + "source_id": 1006917261, + "source_type": "Payments::Refund", + "source_order_id": 217130470, + "source_order_transaction_id": 1006917261, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 777128868, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-8.05", + "fee": "0.00", + "net": "-8.05", + "source_id": 777128868, + "source_type": "Payments::Refund", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 758509248, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-1.50", + "fee": "-0.25", + "net": "-1.75", + "source_id": 764194150, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 746296004, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "10.00", + "fee": "2.00", + "net": "8.00", + "source_id": 746296004, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 515523000, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.65", + "net": "10.85", + "source_id": 1006917261, + "source_type": "Payments::Refund", + "source_order_id": 217130470, + "source_order_transaction_id": 1006917261, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 482793472, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "0.45", + "fee": "0.00", + "net": "0.45", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 382557793, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "0.20", + "fee": "0.00", + "net": "0.20", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 201521674, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-2.00", + "fee": "0.00", + "net": "-2.00", + "source_id": 971443537, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 461790020, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 620327031, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.63", + "net": "10.87", + "source_id": 620327031, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 726130462, + "type": "dispute", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-11.50", + "fee": "15.00", + "net": "-26.50", + "source_id": 598735659, + "source_type": "Payments::Dispute", + "source_order_id": 625362839, + "source_order_transaction_id": 897736458, + "processed_at": "2020-10-25T20:52:08-04:00" + }, + { + "id": 996672915, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-100.00", + "fee": "0.00", + "net": "-100.00", + "source_id": 996672915, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 843310825, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.63", + "net": "10.87", + "source_id": 843310825, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 897736458, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 841651232, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-100.00", + "fee": "0.00", + "net": "-100.00", + "source_id": 841651232, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 717600021, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "100.00", + "fee": "0.00", + "net": "100.00", + "source_id": 717600021, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 427940661, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "150.00", + "fee": "0.00", + "net": "150.00", + "source_id": 427940661, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 400852343, + "type": "reserve", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-42.00", + "fee": "0.00", + "net": "-42.00", + "source_id": null, + "source_type": null, + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 381560291, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-150.00", + "fee": "0.00", + "net": "-150.00", + "source_id": 381560291, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 357948134, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "10.00", + "fee": "0.46", + "net": "9.54", + "source_id": 971443537, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 461790020, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 250467535, + "type": "reserve", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "42.00", + "fee": "0.00", + "net": "42.00", + "source_id": null, + "source_type": null, + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 217609728, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.00", + "net": "11.50", + "source_id": 930299385, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 348327371, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 138130604, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "150.00", + "fee": "0.00", + "net": "150.00", + "source_id": 138130604, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 567994517, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.65", + "net": "10.85", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2014-01-21T13:05:38-05:00" + }, + { + "id": 854848137, + "type": "payout", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-41.90", + "fee": "0.00", + "net": "-41.90", + "source_id": 623721858, + "source_type": "payout", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2012-11-11T19:00:00-05:00" + } + ] +} \ No newline at end of file diff --git a/test/payouts_test.py b/test/payouts_test.py new file mode 100644 index 00000000..e59c8abe --- /dev/null +++ b/test/payouts_test.py @@ -0,0 +1,18 @@ +import shopify +from test.test_helper import TestCase + + +class PayoutsTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_payouts(self): + 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('payouts')) + payouts = shopify.Payouts.find(623721858) + self.assertEqual('paid', payouts.status) + self.assertEqual('41.90', payouts.amount) diff --git a/test/transactions_test.py b/test/transactions_test.py new file mode 100644 index 00000000..62044238 --- /dev/null +++ b/test/transactions_test.py @@ -0,0 +1,12 @@ +import shopify +from test.test_helper import TestCase + + +class TransactionsTest(TestCase): + 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')) + transactions = shopify.Transactions.find() + self.assertGreater(len(transactions), 0) From 992705fb37299f432ee37a501e127c4f668f34d0 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 9 Nov 2020 12:46:29 +0530 Subject: [PATCH 070/259] Add pluralized endpoint for balance --- shopify/resources/balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py index 3d9121c5..819a8ea5 100644 --- a/shopify/resources/balance.py +++ b/shopify/resources/balance.py @@ -4,3 +4,4 @@ class Balance(ShopifyResource, mixins.Metafields): _prefix_source = "/shopify_payments/" + _singular = _plural = 'balance' From f6d3fe33bd1ce4c8aa18d9c4a43aaca4fa5d51d0 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 9 Nov 2020 13:05:09 +0530 Subject: [PATCH 071/259] Fix balance test assert --- test/balance_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/balance_test.py b/test/balance_test.py index ad6719dc..71636716 100644 --- a/test/balance_test.py +++ b/test/balance_test.py @@ -9,5 +9,3 @@ def test_get_balance(self): self.fake('balance', method='GET', prefix=self.prefix, body=self.load_fixture('balance')) balance = shopify.Balance.find() self.assertGreater(len(balance), 0) - self.assertEqual('USD', balance['currency']) - self.assertEqual('53.99', balance['amount']) From a61b0bb11b11e701cb7c4ccc20343660b197f2b4 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 10 Nov 2020 15:06:56 +0530 Subject: [PATCH 072/259] Separate single doc tests --- test/disputes_test.py | 2 +- test/fixtures/dispute.json | 16 ++++++++++++++++ test/fixtures/payout.json | 21 +++++++++++++++++++++ test/payouts_test.py | 2 +- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/dispute.json create mode 100644 test/fixtures/payout.json diff --git a/test/disputes_test.py b/test/disputes_test.py index 44dbb689..7484b304 100644 --- a/test/disputes_test.py +++ b/test/disputes_test.py @@ -12,6 +12,6 @@ def test_get_dispute(self): def test_get_one_dispute(self): self.fake('disputes/1052608616', method='GET', - prefix=self.prefix, body=self.load_fixture('disputes')) + prefix=self.prefix, body=self.load_fixture('dispute')) disputes = shopify.Disputes.find(1052608616) self.assertEqual('won', disputes.status) diff --git a/test/fixtures/dispute.json b/test/fixtures/dispute.json new file mode 100644 index 00000000..1723dd42 --- /dev/null +++ b/test/fixtures/dispute.json @@ -0,0 +1,16 @@ +{ + "dispute": { + "id": 1052608616, + "order_id": null, + "type": "chargeback", + "amount": "100.00", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + } +} \ No newline at end of file diff --git a/test/fixtures/payout.json b/test/fixtures/payout.json new file mode 100644 index 00000000..4c12b1e2 --- /dev/null +++ b/test/fixtures/payout.json @@ -0,0 +1,21 @@ +{ + "payout": { + "id": 623721858, + "status": "paid", + "date": "2019-11-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + } +} \ No newline at end of file diff --git a/test/payouts_test.py b/test/payouts_test.py index e59c8abe..a2478889 100644 --- a/test/payouts_test.py +++ b/test/payouts_test.py @@ -12,7 +12,7 @@ def test_get_payouts(self): def test_get_one_payout(self): self.fake('payouts/623721858', method='GET', - prefix=self.prefix, body=self.load_fixture('payouts')) + prefix=self.prefix, body=self.load_fixture('payout')) payouts = shopify.Payouts.find(623721858) self.assertEqual('paid', payouts.status) self.assertEqual('41.90', payouts.amount) From fcec0dda93ac029e5c80f07c8199146533276dc1 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Tue, 10 Nov 2020 13:50:10 -0500 Subject: [PATCH 073/259] Revert "[Feature] Add support for Shopify Payments resources" --- shopify/resources/__init__.py | 4 - shopify/resources/balance.py | 7 - shopify/resources/disputes.py | 6 - shopify/resources/payouts.py | 6 - shopify/resources/transactions.py | 6 - test/balance_test.py | 11 - test/disputes_test.py | 17 - test/fixtures/balance.json | 8 - test/fixtures/dispute.json | 16 - test/fixtures/disputes.json | 102 ------ test/fixtures/payout.json | 21 -- test/fixtures/payouts.json | 118 ------- test/fixtures/payouts_transactions.json | 404 ------------------------ test/payouts_test.py | 18 -- test/transactions_test.py | 12 - 15 files changed, 756 deletions(-) delete mode 100644 shopify/resources/balance.py delete mode 100644 shopify/resources/disputes.py delete mode 100644 shopify/resources/payouts.py delete mode 100644 shopify/resources/transactions.py delete mode 100644 test/balance_test.py delete mode 100644 test/disputes_test.py delete mode 100644 test/fixtures/balance.json delete mode 100644 test/fixtures/dispute.json delete mode 100644 test/fixtures/disputes.json delete mode 100644 test/fixtures/payout.json delete mode 100644 test/fixtures/payouts.json delete mode 100644 test/fixtures/payouts_transactions.json delete mode 100644 test/payouts_test.py delete mode 100644 test/transactions_test.py diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 8f978535..ce91cf3c 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -45,10 +45,6 @@ from .image import Image from .variant import Variant from .order import Order -from .balance import Balance -from .disputes import Disputes -from .payouts import Payouts -from .transactions import Transactions from .order_risk import OrderRisk from .policy import Policy from .smart_collection import SmartCollection diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py deleted file mode 100644 index 819a8ea5..00000000 --- a/shopify/resources/balance.py +++ /dev/null @@ -1,7 +0,0 @@ -from ..base import ShopifyResource -from shopify import mixins - - -class Balance(ShopifyResource, mixins.Metafields): - _prefix_source = "/shopify_payments/" - _singular = _plural = 'balance' diff --git a/shopify/resources/disputes.py b/shopify/resources/disputes.py deleted file mode 100644 index f098fd0f..00000000 --- a/shopify/resources/disputes.py +++ /dev/null @@ -1,6 +0,0 @@ -from ..base import ShopifyResource -from shopify import mixins - - -class Disputes(ShopifyResource, mixins.Metafields): - _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/payouts.py b/shopify/resources/payouts.py deleted file mode 100644 index dea162d8..00000000 --- a/shopify/resources/payouts.py +++ /dev/null @@ -1,6 +0,0 @@ -from ..base import ShopifyResource -from shopify import mixins - - -class Payouts(ShopifyResource, mixins.Metafields): - _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/transactions.py b/shopify/resources/transactions.py deleted file mode 100644 index 90cb884f..00000000 --- a/shopify/resources/transactions.py +++ /dev/null @@ -1,6 +0,0 @@ -from ..base import ShopifyResource -from shopify import mixins - - -class Transactions(ShopifyResource, mixins.Metafields): - _prefix_source = "/shopify_payments/balance/" diff --git a/test/balance_test.py b/test/balance_test.py deleted file mode 100644 index 71636716..00000000 --- a/test/balance_test.py +++ /dev/null @@ -1,11 +0,0 @@ -import shopify -from test.test_helper import TestCase - - -class BalanceTest(TestCase): - prefix = '/admin/api/unstable/shopify_payments' - - def test_get_balance(self): - 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/disputes_test.py b/test/disputes_test.py deleted file mode 100644 index 7484b304..00000000 --- a/test/disputes_test.py +++ /dev/null @@ -1,17 +0,0 @@ -import shopify -from test.test_helper import TestCase - - -class DisputeTest(TestCase): - prefix = '/admin/api/unstable/shopify_payments' - - def test_get_dispute(self): - 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')) - disputes = shopify.Disputes.find(1052608616) - self.assertEqual('won', disputes.status) diff --git a/test/fixtures/balance.json b/test/fixtures/balance.json deleted file mode 100644 index 8053cb99..00000000 --- a/test/fixtures/balance.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "balance": [ - { - "currency": "USD", - "amount": "53.99" - } - ] -} \ No newline at end of file diff --git a/test/fixtures/dispute.json b/test/fixtures/dispute.json deleted file mode 100644 index 1723dd42..00000000 --- a/test/fixtures/dispute.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "dispute": { - "id": 1052608616, - "order_id": null, - "type": "chargeback", - "amount": "100.00", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "won", - "evidence_due_by": "2013-07-03T19:00:00-04:00", - "evidence_sent_on": "2013-07-04T07:00:00-04:00", - "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 deleted file mode 100644 index 656d27ff..00000000 --- a/test/fixtures/disputes.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "disputes": [ - { - "id": 1052608616, - "order_id": null, - "type": "chargeback", - "amount": "100.00", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "won", - "evidence_due_by": "2013-07-03T19:00:00-04:00", - "evidence_sent_on": "2013-07-04T07:00:00-04:00", - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 815713555, - "order_id": 625362839, - "type": "chargeback", - "amount": "11.50", - "currency": "USD", - "reason": "credit_not_processed", - "network_reason_code": "4827", - "status": "needs_response", - "evidence_due_by": "2020-11-17T19:00:00-05:00", - "evidence_sent_on": null, - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 782360659, - "order_id": 625362839, - "type": "chargeback", - "amount": "11.50", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "won", - "evidence_due_by": "2013-07-03T19:00:00-04:00", - "evidence_sent_on": "2013-07-04T07:00:00-04:00", - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 670893524, - "order_id": 625362839, - "type": "inquiry", - "amount": "11.50", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "needs_response", - "evidence_due_by": "2020-11-17T19:00:00-05:00", - "evidence_sent_on": null, - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 598735659, - "order_id": 625362839, - "type": "chargeback", - "amount": "11.50", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "needs_response", - "evidence_due_by": "2020-11-17T19:00:00-05:00", - "evidence_sent_on": null, - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 85190714, - "order_id": 625362839, - "type": "chargeback", - "amount": "11.50", - "currency": "USD", - "reason": "fraudulent", - "network_reason_code": "4827", - "status": "under_review", - "evidence_due_by": "2020-11-17T19:00:00-05:00", - "evidence_sent_on": "2020-11-04T19:00:00-05:00", - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - }, - { - "id": 35982383, - "order_id": 625362839, - "type": "chargeback", - "amount": "11.50", - "currency": "USD", - "reason": "subscription_canceled", - "network_reason_code": "4827", - "status": "needs_response", - "evidence_due_by": "2020-11-17T19:00:00-05:00", - "evidence_sent_on": null, - "finalized_on": null, - "initiated_at": "2013-05-03T20:00:00-04:00" - } - ] -} \ No newline at end of file diff --git a/test/fixtures/payout.json b/test/fixtures/payout.json deleted file mode 100644 index 4c12b1e2..00000000 --- a/test/fixtures/payout.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "payout": { - "id": 623721858, - "status": "paid", - "date": "2019-11-12", - "currency": "USD", - "amount": "41.90", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "44.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - } -} \ No newline at end of file diff --git a/test/fixtures/payouts.json b/test/fixtures/payouts.json deleted file mode 100644 index b8e247b7..00000000 --- a/test/fixtures/payouts.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "payouts": [ - { - "id": 854088011, - "status": "scheduled", - "date": "2019-11-01", - "currency": "USD", - "amount": "43.12", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "45.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - }, - { - "id": 512467833, - "status": "failed", - "date": "2019-11-01", - "currency": "USD", - "amount": "43.12", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "45.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - }, - { - "id": 39438702, - "status": "in_transit", - "date": "2019-11-01", - "currency": "USD", - "amount": "43.12", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "45.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - }, - { - "id": 710174591, - "status": "paid", - "date": "2019-12-12", - "currency": "USD", - "amount": "41.90", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "44.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - }, - { - "id": 974708905, - "status": "paid", - "date": "2019-11-13", - "currency": "CAD", - "amount": "51.69", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "6.46", - "charges_gross_amount": "58.15", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - }, - { - "id": 623721858, - "status": "paid", - "date": "2019-11-12", - "currency": "USD", - "amount": "41.90", - "summary": { - "adjustments_fee_amount": "0.12", - "adjustments_gross_amount": "2.13", - "charges_fee_amount": "1.32", - "charges_gross_amount": "44.52", - "refunds_fee_amount": "-0.23", - "refunds_gross_amount": "-3.54", - "reserved_funds_fee_amount": "0.00", - "reserved_funds_gross_amount": "0.00", - "retried_payouts_fee_amount": "0.00", - "retried_payouts_gross_amount": "0.00" - } - } - ] -} \ No newline at end of file diff --git a/test/fixtures/payouts_transactions.json b/test/fixtures/payouts_transactions.json deleted file mode 100644 index aa70797c..00000000 --- a/test/fixtures/payouts_transactions.json +++ /dev/null @@ -1,404 +0,0 @@ -{ - "transactions": [ - { - "id": 699519475, - "type": "debit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-50.00", - "fee": "0.00", - "net": "-50.00", - "source_id": 460709370, - "source_type": "adjustment", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2020-11-05T19:52:08-05:00" - }, - { - "id": 77412310, - "type": "credit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "50.00", - "fee": "0.00", - "net": "50.00", - "source_id": 374511569, - "source_type": "Payments::Balance::AdjustmentReversal", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2020-11-05T19:52:08-05:00" - }, - { - "id": 1006917261, - "type": "refund", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-3.45", - "fee": "0.00", - "net": "-3.45", - "source_id": 1006917261, - "source_type": "Payments::Refund", - "source_order_id": 217130470, - "source_order_transaction_id": 1006917261, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 777128868, - "type": "refund", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-8.05", - "fee": "0.00", - "net": "-8.05", - "source_id": 777128868, - "source_type": "Payments::Refund", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 758509248, - "type": "adjustment", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-1.50", - "fee": "-0.25", - "net": "-1.75", - "source_id": 764194150, - "source_type": "charge", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 746296004, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "10.00", - "fee": "2.00", - "net": "8.00", - "source_id": 746296004, - "source_type": "charge", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 515523000, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "11.50", - "fee": "0.65", - "net": "10.85", - "source_id": 1006917261, - "source_type": "Payments::Refund", - "source_order_id": 217130470, - "source_order_transaction_id": 1006917261, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 482793472, - "type": "adjustment", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "0.45", - "fee": "0.00", - "net": "0.45", - "source_id": 204289877, - "source_type": "charge", - "source_order_id": 217130470, - "source_order_transaction_id": 567994517, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 382557793, - "type": "adjustment", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "0.20", - "fee": "0.00", - "net": "0.20", - "source_id": 204289877, - "source_type": "charge", - "source_order_id": 217130470, - "source_order_transaction_id": 567994517, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 201521674, - "type": "refund", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-2.00", - "fee": "0.00", - "net": "-2.00", - "source_id": 971443537, - "source_type": "charge", - "source_order_id": 625362839, - "source_order_transaction_id": 461790020, - "processed_at": "2020-11-04T19:52:08-05:00" - }, - { - "id": 620327031, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "11.50", - "fee": "0.63", - "net": "10.87", - "source_id": 620327031, - "source_type": "charge", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 726130462, - "type": "dispute", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-11.50", - "fee": "15.00", - "net": "-26.50", - "source_id": 598735659, - "source_type": "Payments::Dispute", - "source_order_id": 625362839, - "source_order_transaction_id": 897736458, - "processed_at": "2020-10-25T20:52:08-04:00" - }, - { - "id": 996672915, - "type": "debit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-100.00", - "fee": "0.00", - "net": "-100.00", - "source_id": 996672915, - "source_type": "Payments::Balance::AdjustmentReversal", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 843310825, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "11.50", - "fee": "0.63", - "net": "10.87", - "source_id": 843310825, - "source_type": "charge", - "source_order_id": 625362839, - "source_order_transaction_id": 897736458, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 841651232, - "type": "debit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-100.00", - "fee": "0.00", - "net": "-100.00", - "source_id": 841651232, - "source_type": "Payments::Balance::AdjustmentReversal", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 717600021, - "type": "credit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "100.00", - "fee": "0.00", - "net": "100.00", - "source_id": 717600021, - "source_type": "adjustment", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 427940661, - "type": "credit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "150.00", - "fee": "0.00", - "net": "150.00", - "source_id": 427940661, - "source_type": "Payments::Balance::AdjustmentReversal", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 400852343, - "type": "reserve", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-42.00", - "fee": "0.00", - "net": "-42.00", - "source_id": null, - "source_type": null, - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 381560291, - "type": "debit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-150.00", - "fee": "0.00", - "net": "-150.00", - "source_id": 381560291, - "source_type": "adjustment", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 357948134, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "10.00", - "fee": "0.46", - "net": "9.54", - "source_id": 971443537, - "source_type": "charge", - "source_order_id": 625362839, - "source_order_transaction_id": 461790020, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 250467535, - "type": "reserve", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "42.00", - "fee": "0.00", - "net": "42.00", - "source_id": null, - "source_type": null, - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 217609728, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "11.50", - "fee": "0.00", - "net": "11.50", - "source_id": 930299385, - "source_type": "charge", - "source_order_id": 625362839, - "source_order_transaction_id": 348327371, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 138130604, - "type": "credit", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "150.00", - "fee": "0.00", - "net": "150.00", - "source_id": 138130604, - "source_type": "Payments::Balance::AdjustmentReversal", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2016-08-04T18:07:57-04:00" - }, - { - "id": 567994517, - "type": "charge", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "11.50", - "fee": "0.65", - "net": "10.85", - "source_id": 204289877, - "source_type": "charge", - "source_order_id": 217130470, - "source_order_transaction_id": 567994517, - "processed_at": "2014-01-21T13:05:38-05:00" - }, - { - "id": 854848137, - "type": "payout", - "test": false, - "payout_id": 623721858, - "payout_status": "paid", - "currency": "USD", - "amount": "-41.90", - "fee": "0.00", - "net": "-41.90", - "source_id": 623721858, - "source_type": "payout", - "source_order_id": null, - "source_order_transaction_id": null, - "processed_at": "2012-11-11T19:00:00-05:00" - } - ] -} \ No newline at end of file diff --git a/test/payouts_test.py b/test/payouts_test.py deleted file mode 100644 index a2478889..00000000 --- a/test/payouts_test.py +++ /dev/null @@ -1,18 +0,0 @@ -import shopify -from test.test_helper import TestCase - - -class PayoutsTest(TestCase): - prefix = '/admin/api/unstable/shopify_payments' - - def test_get_payouts(self): - 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')) - payouts = shopify.Payouts.find(623721858) - self.assertEqual('paid', payouts.status) - self.assertEqual('41.90', payouts.amount) diff --git a/test/transactions_test.py b/test/transactions_test.py deleted file mode 100644 index 62044238..00000000 --- a/test/transactions_test.py +++ /dev/null @@ -1,12 +0,0 @@ -import shopify -from test.test_helper import TestCase - - -class TransactionsTest(TestCase): - 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')) - transactions = shopify.Transactions.find() - self.assertGreater(len(transactions), 0) From 98e889cdb227c84d80b5f6d25f293509b768ad5b Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 11 Nov 2020 08:46:29 -0500 Subject: [PATCH 074/259] Revert "Revert "[Feature] Add support for Shopify Payments resources"" --- shopify/resources/__init__.py | 4 + shopify/resources/balance.py | 7 + shopify/resources/disputes.py | 6 + shopify/resources/payouts.py | 6 + shopify/resources/transactions.py | 6 + test/balance_test.py | 11 + test/disputes_test.py | 17 + test/fixtures/balance.json | 8 + test/fixtures/dispute.json | 16 + test/fixtures/disputes.json | 102 ++++++ test/fixtures/payout.json | 21 ++ test/fixtures/payouts.json | 118 +++++++ test/fixtures/payouts_transactions.json | 404 ++++++++++++++++++++++++ test/payouts_test.py | 18 ++ test/transactions_test.py | 12 + 15 files changed, 756 insertions(+) create mode 100644 shopify/resources/balance.py create mode 100644 shopify/resources/disputes.py create mode 100644 shopify/resources/payouts.py create mode 100644 shopify/resources/transactions.py create mode 100644 test/balance_test.py create mode 100644 test/disputes_test.py create mode 100644 test/fixtures/balance.json create mode 100644 test/fixtures/dispute.json create mode 100644 test/fixtures/disputes.json create mode 100644 test/fixtures/payout.json create mode 100644 test/fixtures/payouts.json create mode 100644 test/fixtures/payouts_transactions.json create mode 100644 test/payouts_test.py create mode 100644 test/transactions_test.py diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index ce91cf3c..8f978535 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -45,6 +45,10 @@ from .image import Image from .variant import Variant from .order import Order +from .balance import Balance +from .disputes import Disputes +from .payouts import Payouts +from .transactions import Transactions from .order_risk import OrderRisk from .policy import Policy from .smart_collection import SmartCollection diff --git a/shopify/resources/balance.py b/shopify/resources/balance.py new file mode 100644 index 00000000..819a8ea5 --- /dev/null +++ b/shopify/resources/balance.py @@ -0,0 +1,7 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Balance(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" + _singular = _plural = 'balance' diff --git a/shopify/resources/disputes.py b/shopify/resources/disputes.py new file mode 100644 index 00000000..f098fd0f --- /dev/null +++ b/shopify/resources/disputes.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Disputes(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/payouts.py b/shopify/resources/payouts.py new file mode 100644 index 00000000..dea162d8 --- /dev/null +++ b/shopify/resources/payouts.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Payouts(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/" diff --git a/shopify/resources/transactions.py b/shopify/resources/transactions.py new file mode 100644 index 00000000..90cb884f --- /dev/null +++ b/shopify/resources/transactions.py @@ -0,0 +1,6 @@ +from ..base import ShopifyResource +from shopify import mixins + + +class Transactions(ShopifyResource, mixins.Metafields): + _prefix_source = "/shopify_payments/balance/" diff --git a/test/balance_test.py b/test/balance_test.py new file mode 100644 index 00000000..71636716 --- /dev/null +++ b/test/balance_test.py @@ -0,0 +1,11 @@ +import shopify +from test.test_helper import TestCase + + +class BalanceTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_balance(self): + 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/disputes_test.py b/test/disputes_test.py new file mode 100644 index 00000000..7484b304 --- /dev/null +++ b/test/disputes_test.py @@ -0,0 +1,17 @@ +import shopify +from test.test_helper import TestCase + + +class DisputeTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_dispute(self): + 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')) + disputes = shopify.Disputes.find(1052608616) + self.assertEqual('won', disputes.status) diff --git a/test/fixtures/balance.json b/test/fixtures/balance.json new file mode 100644 index 00000000..8053cb99 --- /dev/null +++ b/test/fixtures/balance.json @@ -0,0 +1,8 @@ +{ + "balance": [ + { + "currency": "USD", + "amount": "53.99" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/dispute.json b/test/fixtures/dispute.json new file mode 100644 index 00000000..1723dd42 --- /dev/null +++ b/test/fixtures/dispute.json @@ -0,0 +1,16 @@ +{ + "dispute": { + "id": 1052608616, + "order_id": null, + "type": "chargeback", + "amount": "100.00", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "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 new file mode 100644 index 00000000..656d27ff --- /dev/null +++ b/test/fixtures/disputes.json @@ -0,0 +1,102 @@ +{ + "disputes": [ + { + "id": 1052608616, + "order_id": null, + "type": "chargeback", + "amount": "100.00", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 815713555, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "credit_not_processed", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 782360659, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "won", + "evidence_due_by": "2013-07-03T19:00:00-04:00", + "evidence_sent_on": "2013-07-04T07:00:00-04:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 670893524, + "order_id": 625362839, + "type": "inquiry", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 598735659, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 85190714, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "fraudulent", + "network_reason_code": "4827", + "status": "under_review", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": "2020-11-04T19:00:00-05:00", + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + }, + { + "id": 35982383, + "order_id": 625362839, + "type": "chargeback", + "amount": "11.50", + "currency": "USD", + "reason": "subscription_canceled", + "network_reason_code": "4827", + "status": "needs_response", + "evidence_due_by": "2020-11-17T19:00:00-05:00", + "evidence_sent_on": null, + "finalized_on": null, + "initiated_at": "2013-05-03T20:00:00-04:00" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/payout.json b/test/fixtures/payout.json new file mode 100644 index 00000000..4c12b1e2 --- /dev/null +++ b/test/fixtures/payout.json @@ -0,0 +1,21 @@ +{ + "payout": { + "id": 623721858, + "status": "paid", + "date": "2019-11-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + } +} \ No newline at end of file diff --git a/test/fixtures/payouts.json b/test/fixtures/payouts.json new file mode 100644 index 00000000..b8e247b7 --- /dev/null +++ b/test/fixtures/payouts.json @@ -0,0 +1,118 @@ +{ + "payouts": [ + { + "id": 854088011, + "status": "scheduled", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 512467833, + "status": "failed", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 39438702, + "status": "in_transit", + "date": "2019-11-01", + "currency": "USD", + "amount": "43.12", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "45.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 710174591, + "status": "paid", + "date": "2019-12-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 974708905, + "status": "paid", + "date": "2019-11-13", + "currency": "CAD", + "amount": "51.69", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "6.46", + "charges_gross_amount": "58.15", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + }, + { + "id": 623721858, + "status": "paid", + "date": "2019-11-12", + "currency": "USD", + "amount": "41.90", + "summary": { + "adjustments_fee_amount": "0.12", + "adjustments_gross_amount": "2.13", + "charges_fee_amount": "1.32", + "charges_gross_amount": "44.52", + "refunds_fee_amount": "-0.23", + "refunds_gross_amount": "-3.54", + "reserved_funds_fee_amount": "0.00", + "reserved_funds_gross_amount": "0.00", + "retried_payouts_fee_amount": "0.00", + "retried_payouts_gross_amount": "0.00" + } + } + ] +} \ No newline at end of file diff --git a/test/fixtures/payouts_transactions.json b/test/fixtures/payouts_transactions.json new file mode 100644 index 00000000..aa70797c --- /dev/null +++ b/test/fixtures/payouts_transactions.json @@ -0,0 +1,404 @@ +{ + "transactions": [ + { + "id": 699519475, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-50.00", + "fee": "0.00", + "net": "-50.00", + "source_id": 460709370, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-05T19:52:08-05:00" + }, + { + "id": 77412310, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "50.00", + "fee": "0.00", + "net": "50.00", + "source_id": 374511569, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-05T19:52:08-05:00" + }, + { + "id": 1006917261, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-3.45", + "fee": "0.00", + "net": "-3.45", + "source_id": 1006917261, + "source_type": "Payments::Refund", + "source_order_id": 217130470, + "source_order_transaction_id": 1006917261, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 777128868, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-8.05", + "fee": "0.00", + "net": "-8.05", + "source_id": 777128868, + "source_type": "Payments::Refund", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 758509248, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-1.50", + "fee": "-0.25", + "net": "-1.75", + "source_id": 764194150, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 746296004, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "10.00", + "fee": "2.00", + "net": "8.00", + "source_id": 746296004, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 515523000, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.65", + "net": "10.85", + "source_id": 1006917261, + "source_type": "Payments::Refund", + "source_order_id": 217130470, + "source_order_transaction_id": 1006917261, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 482793472, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "0.45", + "fee": "0.00", + "net": "0.45", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 382557793, + "type": "adjustment", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "0.20", + "fee": "0.00", + "net": "0.20", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 201521674, + "type": "refund", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-2.00", + "fee": "0.00", + "net": "-2.00", + "source_id": 971443537, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 461790020, + "processed_at": "2020-11-04T19:52:08-05:00" + }, + { + "id": 620327031, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.63", + "net": "10.87", + "source_id": 620327031, + "source_type": "charge", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 726130462, + "type": "dispute", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-11.50", + "fee": "15.00", + "net": "-26.50", + "source_id": 598735659, + "source_type": "Payments::Dispute", + "source_order_id": 625362839, + "source_order_transaction_id": 897736458, + "processed_at": "2020-10-25T20:52:08-04:00" + }, + { + "id": 996672915, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-100.00", + "fee": "0.00", + "net": "-100.00", + "source_id": 996672915, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 843310825, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.63", + "net": "10.87", + "source_id": 843310825, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 897736458, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 841651232, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-100.00", + "fee": "0.00", + "net": "-100.00", + "source_id": 841651232, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 717600021, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "100.00", + "fee": "0.00", + "net": "100.00", + "source_id": 717600021, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 427940661, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "150.00", + "fee": "0.00", + "net": "150.00", + "source_id": 427940661, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 400852343, + "type": "reserve", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-42.00", + "fee": "0.00", + "net": "-42.00", + "source_id": null, + "source_type": null, + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 381560291, + "type": "debit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-150.00", + "fee": "0.00", + "net": "-150.00", + "source_id": 381560291, + "source_type": "adjustment", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 357948134, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "10.00", + "fee": "0.46", + "net": "9.54", + "source_id": 971443537, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 461790020, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 250467535, + "type": "reserve", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "42.00", + "fee": "0.00", + "net": "42.00", + "source_id": null, + "source_type": null, + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 217609728, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.00", + "net": "11.50", + "source_id": 930299385, + "source_type": "charge", + "source_order_id": 625362839, + "source_order_transaction_id": 348327371, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 138130604, + "type": "credit", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "150.00", + "fee": "0.00", + "net": "150.00", + "source_id": 138130604, + "source_type": "Payments::Balance::AdjustmentReversal", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2016-08-04T18:07:57-04:00" + }, + { + "id": 567994517, + "type": "charge", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "11.50", + "fee": "0.65", + "net": "10.85", + "source_id": 204289877, + "source_type": "charge", + "source_order_id": 217130470, + "source_order_transaction_id": 567994517, + "processed_at": "2014-01-21T13:05:38-05:00" + }, + { + "id": 854848137, + "type": "payout", + "test": false, + "payout_id": 623721858, + "payout_status": "paid", + "currency": "USD", + "amount": "-41.90", + "fee": "0.00", + "net": "-41.90", + "source_id": 623721858, + "source_type": "payout", + "source_order_id": null, + "source_order_transaction_id": null, + "processed_at": "2012-11-11T19:00:00-05:00" + } + ] +} \ No newline at end of file diff --git a/test/payouts_test.py b/test/payouts_test.py new file mode 100644 index 00000000..a2478889 --- /dev/null +++ b/test/payouts_test.py @@ -0,0 +1,18 @@ +import shopify +from test.test_helper import TestCase + + +class PayoutsTest(TestCase): + prefix = '/admin/api/unstable/shopify_payments' + + def test_get_payouts(self): + 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')) + payouts = shopify.Payouts.find(623721858) + self.assertEqual('paid', payouts.status) + self.assertEqual('41.90', payouts.amount) diff --git a/test/transactions_test.py b/test/transactions_test.py new file mode 100644 index 00000000..62044238 --- /dev/null +++ b/test/transactions_test.py @@ -0,0 +1,12 @@ +import shopify +from test.test_helper import TestCase + + +class TransactionsTest(TestCase): + 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')) + transactions = shopify.Transactions.find() + self.assertGreater(len(transactions), 0) From 3aee06c42301bc6de3a7e225969208096382ba78 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 10:52:02 -0500 Subject: [PATCH 075/259] Add workflow file --- .github/workflows/build.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..a1cdf3c6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + + +jobs: + build: + runs-on: ubuntu-latest + name: Python ${{ matrix.version }} + strategy: + matrix: + version: [2.7, 3.4, 3.5, 3.6] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.version }} + - name: Install Dependencies + run: python setup.py install + - name: Run Tests + run: python -m pytest -v + From 6d17867735a96d6df9b9166dc3af1d0431f7da04 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 10:58:22 -0500 Subject: [PATCH 076/259] Install dependencies --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1cdf3c6..232546ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,10 @@ jobs: with: python-version: ${{ matrix.version }} - name: Install Dependencies - run: python setup.py install + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + python setup.py install - name: Run Tests run: python -m pytest -v From 5e534b3ddddd4c7b92c521a2bb4a1892399fb40f Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 11:40:19 -0500 Subject: [PATCH 077/259] Add mock install --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 232546ef..9f801860 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,7 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest + pip install mock python setup.py install - name: Run Tests run: python -m pytest -v From d6c08f1aa1f22b4440bb1b75390d1f11096f706a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 12:25:01 -0500 Subject: [PATCH 078/259] Update from comments --- .github/workflows/build.yml | 5 ++--- .travis.yml | 27 --------------------------- README.md | 2 +- 3 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f801860..ac67cdda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [2.7, 3.4, 3.5, 3.6] + version: [2.7, 3.6, 3.9] steps: - uses: actions/checkout@v2 @@ -21,8 +21,7 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - pip install mock + pip install pytest mock python setup.py install - name: Run Tests run: python -m pytest -v diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2ddd682c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python - -# command to install dependencies -install: - - "python setup.py install" - -# command to run tests -script: - - python -m pytest -v - -stages: - - name: test - -jobs: - fast_finish: true - include: - - stage: test - python: "2.7" - - - stage: test - python: "3.4" - - - stage: test - python: "3.5" - - - stage: test - python: "3.6" diff --git a/README.md b/README.md index 5f31a693..0d0ce02c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Shopify API -[![Build Status](https://travis-ci.org/Shopify/shopify_python_api.svg?branch=master)](https://travis-ci.org/Shopify/shopify_python_api) +[![Build Status](https://github.com/Shopify/shopify_python_api/actions?query=workflow%3ACI+branch%3A+master](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From 73fb18b0b7a75ed93a7607411a0495743831640f Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 12:45:51 -0500 Subject: [PATCH 079/259] Library incompatible with python 3.9 --- .github/workflows/build.yml | 2 +- README.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac67cdda..04c10fb7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [2.7, 3.6, 3.9] + version: [2.7, 3.6, 3.8] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 0d0ce02c..c3f2b930 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library ### 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. + ### Installation To easily install or upgrade to the latest release, use [pip](http://www.pip-installer.org/). From b71bd95ead166f7c844eb195db958f6126d4af07 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 12:53:17 -0500 Subject: [PATCH 080/259] Typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3f2b930..7038df63 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Shopify API -[![Build Status](https://github.com/Shopify/shopify_python_api/actions?query=workflow%3ACI+branch%3A+master](https://github.com/Shopify/shopify_python_api/actions) +[![Build Status](https://github.com/Shopify/shopify_python_api/actions?query=workflow%3ACI+branch%3A+master)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From fd3ca95de60df0d7afbc25c4518e1971c50e8bc3 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 12:55:55 -0500 Subject: [PATCH 081/259] Use correct link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7038df63..2b02e904 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Shopify API -[![Build Status](https://github.com/Shopify/shopify_python_api/actions?query=workflow%3ACI+branch%3A+master)](https://github.com/Shopify/shopify_python_api/actions) +[![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From 33def199f42af96bf780565e95a597c977313da5 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 18 Nov 2020 12:56:46 -0500 Subject: [PATCH 082/259] Correct badge finally --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b02e904..7ce672dc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Shopify API -[![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg](https://github.com/Shopify/shopify_python_api/actions) +[![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From 6e264aee4c3f00f96be19b73a03848f5a833e3fb Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 19 Nov 2020 17:18:51 -0500 Subject: [PATCH 083/259] Mention Six in Contributing docs --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 490f988b..a08624f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,4 +5,6 @@ Please open an issue here if you encounter a specific bug with this API client l General questions about the Shopify API and usage of this package (not necessarily a bug) should be posted on the [Shopify forums](https://ecommerce.shopify.com/c/shopify-apis-and-technology). +For compatibility across Python 2 and Python 3, look into [Six](https://six.readthedocs.io/). + When in doubt, post on the forum first. You'll likely have your questions answered more quickly if you post there; more people monitor the forum than Github. From e614bfa572339d1b1b92ab8a931a2c5493820676 Mon Sep 17 00:00:00 2001 From: smubbs Date: Wed, 25 Nov 2020 16:07:31 +0800 Subject: [PATCH 084/259] Update api_version.py --- shopify/api_version.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 39ae8fb8..16c35b68 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -24,11 +24,15 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): - cls.define_version(Unstable()) - cls.define_version(Release('2020-01')) - cls.define_version(Release('2020-04')) - cls.define_version(Release('2020-07')) - cls.define_version(Release('2020-10')) + req = request.urlopen("https://app.shopify.com/services/apis.json") + data = json.loads(req.read().decode("utf-8")) + for api in j['apis']: + if api['handle'] == 'admin': + for release in api['versions']: + if release == 'unstable': + cls.define_version(Unstable()) + else: + cls.define_version(Release(release) @classmethod def clear_defined_versions(cls): From 44848c0d663093cf6bf96f4c9e1542ab54eb5ff3 Mon Sep 17 00:00:00 2001 From: smubbs Date: Wed, 25 Nov 2020 16:17:35 +0800 Subject: [PATCH 085/259] Update api_version.py --- shopify/api_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index 16c35b68..4e9e4a2c 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -1,4 +1,5 @@ import re +from six.moves.urllib import request class InvalidVersionError(Exception): From c84bb4472271348f2ead6a28c92527ac42348422 Mon Sep 17 00:00:00 2001 From: smubbs Date: Wed, 25 Nov 2020 16:17:59 +0800 Subject: [PATCH 086/259] Update api_version.py --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 4e9e4a2c..5ac7e469 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -33,7 +33,7 @@ def define_known_versions(cls): if release == 'unstable': cls.define_version(Unstable()) else: - cls.define_version(Release(release) + cls.define_version(Release(release)) @classmethod def clear_defined_versions(cls): From b27be9538974ba59bc5ba6abbe88a7238fb5f5b2 Mon Sep 17 00:00:00 2001 From: smubbs Date: Wed, 25 Nov 2020 16:22:39 +0800 Subject: [PATCH 087/259] Release handle will now used --- shopify/api_version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 5ac7e469..66901852 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -30,10 +30,10 @@ def define_known_versions(cls): for api in j['apis']: if api['handle'] == 'admin': for release in api['versions']: - if release == 'unstable': + if release['handle'] == 'unstable': cls.define_version(Unstable()) else: - cls.define_version(Release(release)) + cls.define_version(Release(release['handle'])) @classmethod def clear_defined_versions(cls): From ea84f13e8576c678bf84fb317c0f85fed1fad637 Mon Sep 17 00:00:00 2001 From: smubbs Date: Wed, 25 Nov 2020 17:00:51 +0800 Subject: [PATCH 088/259] json import --- shopify/api_version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 66901852..18789b9a 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -1,4 +1,5 @@ import re +import json from six.moves.urllib import request @@ -27,7 +28,7 @@ def define_version(cls, version): 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 j['apis']: + for api in data['apis']: if api['handle'] == 'admin': for release in api['versions']: if release['handle'] == 'unstable': From 0db4573dbc13e5de626f42928102ce10f228c3ac Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Tue, 1 Dec 2020 11:29:08 -0500 Subject: [PATCH 089/259] Release v8.1.0 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0918bdc2..1086b7e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index f68dd5f5..80ef83b6 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.0.4' +VERSION = '8.1.0' From e08138a728287b85beb2e11463f517d35b6d5e0e Mon Sep 17 00:00:00 2001 From: Ben Smith-Stubbs Date: Wed, 2 Dec 2020 15:46:32 +0800 Subject: [PATCH 090/259] updated test_helper.py to add a fake for the dynamic API version request Updated fake() to reflect the use of a url --- shopify/api_version.py | 3 +-- test/fixtures/api_version.json | 1 + test/test_helper.py | 19 ++++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/api_version.json diff --git a/shopify/api_version.py b/shopify/api_version.py index 18789b9a..ce3d609a 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -2,7 +2,6 @@ import json from six.moves.urllib import request - class InvalidVersionError(Exception): pass @@ -28,7 +27,7 @@ def define_version(cls, version): 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']: + for api in data['apis']: if api['handle'] == 'admin': for release in api['versions']: if release['handle'] == 'unstable': diff --git a/test/fixtures/api_version.json b/test/fixtures/api_version.json new file mode 100644 index 00000000..43d1354d --- /dev/null +++ b/test/fixtures/api_version.json @@ -0,0 +1 @@ +{"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/test_helper.py b/test/test_helper.py index dfb80968..7dc8644f 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -20,6 +20,14 @@ def setUp(self): 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 + ) def load_fixture(self, name, format='json'): with open(os.path.dirname(__file__)+'/fixtures/%s.%s' % (name, format), 'rb') as f: @@ -35,13 +43,10 @@ def fake(self, endpoint, **kwargs): extension = "" else: extension = ".%s" % (kwargs.pop('extension', 'json')) - - url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension) - try: - url = kwargs['url'] - except KeyError: - pass - + if kwargs.get('url'): + url = kwargs.get('url') + else: + url = "https://this-is-my-test-show.myshopify.com%s/%s%s" % (prefix, endpoint, extension) headers = {} if kwargs.pop('has_user_agent', True): userAgent = 'ShopifyPythonAPI/%s Python/%s' % (shopify.VERSION, sys.version.split(' ', 1)[0]) From bd48f945e2e2874ff453521bd9a3c043828112ff Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Wed, 2 Dec 2020 10:08:23 -0500 Subject: [PATCH 091/259] Update links to Partners Dashboard The old link for adding an app was broken Also fix spelling of retrieve --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7ce672dc..b2161a0e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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. +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. #### Python version This library requires Python 3.8 or lower. @@ -24,7 +24,7 @@ pip install --upgrade ShopifyAPI ### 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 From 94aed5fd052d6d29bfc50035029684741cd049b3 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 4 Jan 2021 11:38:04 -0500 Subject: [PATCH 092/259] Mention activation charge changes in 2021-01 version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ce672dc..dc531abf 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ _Note: Your application must be public to test the billing process. To test on a charge = shopify.ApplicationCharge.find(charge_id) shopify.ApplicationCharge.activate(charge) ``` -1. Check that `activated_charge` status is `active` +1. Check that `activated_charge` status is `active` (This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)) ```python activated_charge = shopify.ApplicationCharge.find(charge_id) has_been_billed = activated_charge.status == 'active' From f7f9061fdf3d05c94269033df383cb5a9d51e0ed Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 4 Jan 2021 12:53:22 -0500 Subject: [PATCH 093/259] Update README.md Co-authored-by: Kevin O'Sullivan <56687600+mkevinosullivan@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc531abf..5d7569dc 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ _Note: Your application must be public to test the billing process. To test on a charge = shopify.ApplicationCharge.find(charge_id) shopify.ApplicationCharge.activate(charge) ``` -1. Check that `activated_charge` status is `active` (This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)) +1. Check that `activated_charge` status is `active` (_Note: This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)_) ```python activated_charge = shopify.ApplicationCharge.find(charge_id) has_been_billed = activated_charge.status == 'active' From 758eda39896c5e4c13a8504c0cc4996e848e0d93 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 4 Jan 2021 12:58:37 -0500 Subject: [PATCH 094/259] Move note to right step --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5d7569dc..435cdb6a 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,12 @@ _Note: Your application must be public to test the billing process. To test on a }) # Redirect user to application_charge.confirmation_url so they can approve the charge ``` -1. After approving the charge, the user is redirected to `return_url` with `charge_id` parameter +1. After approving the charge, the user is redirected to `return_url` with `charge_id` parameter (_Note: This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)_) ```python charge = shopify.ApplicationCharge.find(charge_id) shopify.ApplicationCharge.activate(charge) ``` -1. Check that `activated_charge` status is `active` (_Note: This action is no longer necessary if the charge is created with [API version 2021-01 or later](https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions)_) +1. Check that `activated_charge` status is `active` ```python activated_charge = shopify.ApplicationCharge.find(charge_id) has_been_billed = activated_charge.status == 'active' From bc8cb4c4e664a58158cdd288e072a886bc9ec9c2 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Jan 2021 10:36:17 -0500 Subject: [PATCH 095/259] Release v8.2.0 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 1086b7e4..4458dd8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Version 8.2.0 +- [Feature] Add support for Dynamic API Versioning (#441) + == Version 8.1.0 - [Feature] Add support for Shopify Payments resources (#428) diff --git a/shopify/version.py b/shopify/version.py index 80ef83b6..1010d782 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.1.0' +VERSION = '8.2.0' From fd2e44e126fed0fa71adb93d9c172c4d7b5b817b Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Jan 2021 10:44:38 -0500 Subject: [PATCH 096/259] Update link --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4458dd8d..175b6371 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ == Version 8.2.0 -- [Feature] Add support for Dynamic API Versioning (#441) +- [Feature] Add support for Dynamic API Versioning ([#441](https://github.com/Shopify/shopify_python_api/pull/441)) == Version 8.1.0 - [Feature] Add support for Shopify Payments resources (#428) From 849c9931bb6f1f4762afb62b92d42612497ef098 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Jan 2021 10:59:35 -0500 Subject: [PATCH 097/259] Expand entry --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 175b6371..254379c3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ == Version 8.2.0 -- [Feature] Add support for Dynamic API Versioning ([#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) From 69321b29cfd4bca84be037400a77caa226b95f06 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 11 Nov 2020 15:37:59 -0500 Subject: [PATCH 098/259] Add update_tracking --- shopify/resources/fulfillment.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index c0b5e7e7..82d5c66e 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -1,4 +1,5 @@ from ..base import ShopifyResource +import json class Fulfillment(ShopifyResource): @@ -13,6 +14,24 @@ def complete(self): def open(self): self._load_attributes_from_response(self.post("open")) + def update_tracking(self, tracking_info, notify_customer): + fulfill = FulfillmentV2() + fulfill.id = self.id + fulfill.update_tracking(tracking_info, notify_customer) + class FulfillmentOrders(ShopifyResource): _prefix_source = "/orders/$order_id/" + +class FulfillmentV2(ShopifyResource): + _singular = 'fulfillment' + _plural = 'fulfillments' + + def update_tracking(self, tracking_info, notify_customer): + body = { + "fulfillment": { + "tracking_info": tracking_info, + "notify_customer": notify_customer + } + } + self._load_attributes_from_response(self.post("update_tracking", json.dumps(body).encode())) From 58462b43b3ae5c8e8ba76c6f91a82d8ce802baf9 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 11 Nov 2020 16:42:16 -0500 Subject: [PATCH 099/259] Add tests for update_tracking --- shopify/resources/fulfillment.py | 4 ++-- test/fixtures/fulfillment.json | 4 ++-- test/fulfillment_test.py | 23 ++++++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index 82d5c66e..39277acc 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -17,7 +17,7 @@ def open(self): def update_tracking(self, tracking_info, notify_customer): fulfill = FulfillmentV2() fulfill.id = self.id - fulfill.update_tracking(tracking_info, notify_customer) + self._load_attributes_from_response(fulfill.update_tracking(tracking_info, notify_customer)) class FulfillmentOrders(ShopifyResource): @@ -34,4 +34,4 @@ def update_tracking(self, tracking_info, notify_customer): "notify_customer": notify_customer } } - self._load_attributes_from_response(self.post("update_tracking", json.dumps(body).encode())) + return self.post("update_tracking", json.dumps(body).encode()) diff --git a/test/fixtures/fulfillment.json b/test/fixtures/fulfillment.json index 9d7e04d3..6d8bd9e8 100644 --- a/test/fixtures/fulfillment.json +++ b/test/fixtures/fulfillment.json @@ -5,7 +5,7 @@ "order_id": 450789469, "service": "manual", "status": "pending", - "tracking_company": null, + "tracking_company": "null-company", "updated_at": "2013-11-01T16:06:08-04:00", "tracking_number": "1Z2345", "tracking_numbers": [ @@ -46,4 +46,4 @@ } ] } -} \ No newline at end of file +} diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py index 20034fb1..7b88623f 100644 --- a/test/fulfillment_test.py +++ b/test/fulfillment_test.py @@ -3,7 +3,7 @@ from pyactiveresource.activeresource import ActiveResource class FulFillmentTest(TestCase): - + def setUp(self): super(FulFillmentTest, self).setUp() self.fake("orders/450789469/fulfillments/255858046", method='GET', body=self.load_fixture('fulfillment')) @@ -40,3 +40,24 @@ def test_able_to_cancel_fulfillment(self): self.assertEqual('pending', fulfillment.status) fulfillment.cancel() 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"} + 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') + + 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) From d837bfd19ac4e306a7d377e0573a9b922aac1c0d Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 11 Nov 2020 16:56:12 -0500 Subject: [PATCH 100/259] Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 254379c3..9a958f31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +- Added support for Fulfillment.update_tracking ([#432](https://github.com/Shopify/shopify_python_api/pull/432)) + == 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)) From 6a246f058b657fe28dc1693a76c1e95c8e52e2bb Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 25 Jan 2021 16:38:23 -0500 Subject: [PATCH 101/259] Add fulfillment event class --- shopify/resources/__init__.py | 1 + shopify/resources/fulfillment_event.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 shopify/resources/fulfillment_event.py diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 8f978535..803c5351 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -38,6 +38,7 @@ from .country import Country from .refund import Refund from .fulfillment import Fulfillment, FulfillmentOrders +from .fulfillment_event import FulfillmentEvent from .fulfillment_service import FulfillmentService from .carrier_service import CarrierService from .transaction import Transaction diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py new file mode 100644 index 00000000..a7175c3f --- /dev/null +++ b/shopify/resources/fulfillment_event.py @@ -0,0 +1,22 @@ +from ..base import ShopifyResource + +class FulfillmentEvent(ShopifyResource): + _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id" + _singular = "event" + _plural = "events" + + @classmethod + def _prefix(cls, options={}): + order_id = options.get("order_id") + fulfillment_id = options.get('fulfillment_id') + if order_id: + return "%s/orders/%s/fulfillments/%s" % ( + cls.site, order_id, fulfillment_id) + else: + return cls.site + + 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() From 2ec5c7fe2be4d40abc9fcdffdef6f39490f76f2a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 27 Jan 2021 15:57:31 -0500 Subject: [PATCH 102/259] Add tests --- shopify/resources/fulfillment_event.py | 15 +++++++-------- test/fixtures/fulfillment_event.json | 22 ++++++++++++++++++++++ test/fulfillment_event_test.py | 22 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/fulfillment_event.json create mode 100644 test/fulfillment_event_test.py diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py index a7175c3f..886e4598 100644 --- a/shopify/resources/fulfillment_event.py +++ b/shopify/resources/fulfillment_event.py @@ -1,19 +1,18 @@ from ..base import ShopifyResource class FulfillmentEvent(ShopifyResource): - _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id" - _singular = "event" - _plural = "events" + _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id/" + _singular = 'event' + _plural = 'events' @classmethod def _prefix(cls, options={}): order_id = options.get("order_id") fulfillment_id = options.get('fulfillment_id') - if order_id: - return "%s/orders/%s/fulfillments/%s" % ( - cls.site, order_id, fulfillment_id) - else: - return cls.site + event_id = options.get("event_id") + + return "%s/orders/%s/fulfillments/%s" % ( + cls.site, order_id, fulfillment_id) def save(self): status = self.attributes['status'] diff --git a/test/fixtures/fulfillment_event.json b/test/fixtures/fulfillment_event.json new file mode 100644 index 00000000..7115a158 --- /dev/null +++ b/test/fixtures/fulfillment_event.json @@ -0,0 +1,22 @@ +{ + "fulfillment_event": { + "id": 12584341209251, + "fulfillment_id": 2608403447971, + "status": "label_printed", + "message": null, + "happened_at": "2021-01-25T16:32:23-05:00", + "city": null, + "province": null, + "country": null, + "zip": null, + "address1": null, + "latitude": null, + "longitude": null, + "shop_id": 49144037539, + "created_at": "2021-01-25T16:32:23-05:00", + "updated_at": "2021-01-25T16:32:23-05:00", + "estimated_delivery_at": null, + "order_id": 2776493818019, + "admin_graphql_api_id": "gid://shopify/FulfillmentEvent/12584341209251" + } +} diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py new file mode 100644 index 00000000..8cb16180 --- /dev/null +++ b/test/fulfillment_event_test.py @@ -0,0 +1,22 @@ +import shopify +from test.test_helper import TestCase +from pyactiveresource.activeresource import ActiveResource + +class FulFillmentEventTest(TestCase): + def setUp(self): + super(FulFillmentEventTest, self).setUp() + self.fake("orders/450789469/fulfillments/255858046/events", method='GET', body=self.load_fixture('fulfillment_event')) + + def test_get_fulfillment_event(self): + fulfillment_event = shopify.FulfillmentEvent.find(order_id='450789469', fulfillment_id='255858046') + self.assertEqual(1, len(fulfillment_event)) + + # def test_create_fulfillment_event(self): + + + def test_error_on_incorrect_status(self): + with self.assertRaises(AttributeError): + incorrect_status = 'asdf' + fulfillment_event = shopify.FulfillmentEvent.find(order_id='450789469', fulfillment_id='255858046') + fulfillment_event.status = incorrect_status + fulfillment_event.save() From a362f363010cc1b6cd11225a3cbdb90f47c03702 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 28 Jan 2021 16:54:07 -0500 Subject: [PATCH 103/259] Clean up fulfillment_event_test.py --- test/fulfillment_event_test.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index 8cb16180..18e4eefc 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -3,20 +3,18 @@ from pyactiveresource.activeresource import ActiveResource class FulFillmentEventTest(TestCase): - def setUp(self): - super(FulFillmentEventTest, self).setUp() - self.fake("orders/450789469/fulfillments/255858046/events", method='GET', body=self.load_fixture('fulfillment_event')) + def setUp(self): + super(FulFillmentEventTest, self).setUp() + self.fake("orders/2776493818019/fulfillments/2608403447971/events", method='GET', body=self.load_fixture('fulfillment_event')) + self.fulfillment_event = shopify.FulfillmentEvent.find(order_id=2776493818019, fulfillment_id=2608403447971) - def test_get_fulfillment_event(self): - fulfillment_event = shopify.FulfillmentEvent.find(order_id='450789469', fulfillment_id='255858046') - self.assertEqual(1, len(fulfillment_event)) + def test_get_fulfillment_event(self): + self.assertEqual(1, len(self.fulfillment_event)) - # def test_create_fulfillment_event(self): + def test_create_fulfillment_event(self): + pass - - def test_error_on_incorrect_status(self): - with self.assertRaises(AttributeError): - incorrect_status = 'asdf' - fulfillment_event = shopify.FulfillmentEvent.find(order_id='450789469', fulfillment_id='255858046') - fulfillment_event.status = incorrect_status - fulfillment_event.save() + def test_error_on_incorrect_status(self): + with self.assertRaises(AttributeError): + self.fake("orders/2776493818019/fulfillments/2608403447971/events", method='PUT', body=self.load_fixture('article'), headers={'Content-type': 'application/json'}) + self.fulfillment_event.save() From 8b986e97bc894987d7af2aeb9b5d585fbb41b81b Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Fri, 29 Jan 2021 13:15:12 -0500 Subject: [PATCH 104/259] Wrap up testing --- test/fulfillment_event_test.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index 18e4eefc..1ba9a73e 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -3,18 +3,22 @@ from pyactiveresource.activeresource import ActiveResource class FulFillmentEventTest(TestCase): - def setUp(self): - super(FulFillmentEventTest, self).setUp() - self.fake("orders/2776493818019/fulfillments/2608403447971/events", method='GET', body=self.load_fixture('fulfillment_event')) - self.fulfillment_event = shopify.FulfillmentEvent.find(order_id=2776493818019, fulfillment_id=2608403447971) - def test_get_fulfillment_event(self): - self.assertEqual(1, len(self.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): - pass + 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", method='PUT', body=self.load_fixture('article'), headers={'Content-type': 'application/json'}) - self.fulfillment_event.save() + 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() + From 1e855a78fe39b28e1a9dade3fe38d7805d12e632 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Fri, 29 Jan 2021 15:25:00 -0500 Subject: [PATCH 105/259] Update changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9a958f31..c5ce222d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ - 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)) == Version 8.2.0 - [Feature] Add support for Dynamic API Versioning. When the library is initialized, it will now make a request to From 7a2619767762fa715d82964a46c3e7f76a176b0e Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Fri, 5 Feb 2021 10:39:38 -0500 Subject: [PATCH 106/259] Add python 3.9 support --- .github/workflows/build.yml | 2 +- README.md | 3 --- setup.py | 3 ++- tox.ini | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04c10fb7..ea45e594 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [2.7, 3.6, 3.8] + version: [2.7, 3.6, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 435cdb6a..74fb899f 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,6 @@ The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library ### 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. - ### Installation To easily install or upgrade to the latest release, use [pip](http://www.pip-installer.org/). diff --git a/setup.py b/setup.py index 47334b33..2f57afb1 100755 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ scripts=['scripts/shopify_api.py'], license='MIT License', install_requires=[ - 'pyactiveresource>=2.2.0', + 'pyactiveresource>=2.2.2', 'PyYAML', 'six', ], @@ -41,6 +41,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules'] diff --git a/tox.ini b/tox.ini index 47520dac..720c4805 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35, py36 +envlist = py27, py34, py35, py36, py39 skip_missing_interpreters = true [testenv] From 03a1887c0bf9474e66c29fca579e96889ed8b68d Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 10:58:16 -0500 Subject: [PATCH 107/259] Fix 'object of type "filter" has no len()' issue --- scripts/shopify_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 60f48065..e73f8fc9 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -51,7 +51,7 @@ def run_task(cls, task=None, *args): # Allow unambigious abbreviations of tasks if task not in cls._tasks: - matches = filter(lambda item: item.startswith(task), cls._tasks) + matches = list(filter(lambda item: item.startswith(task), cls._tasks)) if len(matches) == 1: task = matches[0] else: From c28ca0538614fe08323708327481e4c0285ec60b Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 11:59:37 -0500 Subject: [PATCH 108/259] Try to fix python 3.6 issue --- scripts/shopify_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index e73f8fc9..5a448743 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -51,9 +51,9 @@ def run_task(cls, task=None, *args): # Allow unambigious abbreviations of tasks if task not in cls._tasks: - matches = list(filter(lambda item: item.startswith(task), cls._tasks)) - if len(matches) == 1: - task = matches[0] + matches = filter(lambda item: item.startswith(task), cls._tasks) + if len(list(matches)) == 1: + task = list(matches)[0] else: sys.stderr.write('Could not find task "%s".\n' % (task)) From 4fad266cc0951034e4998c0e08ca904a693ea819 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 12:06:39 -0500 Subject: [PATCH 109/259] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index c5ce222d..795baab5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ - 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)) == Version 8.2.0 - [Feature] Add support for Dynamic API Versioning. When the library is initialized, it will now make a request to From f2202c72e0016dab8495a56be5569944f1011059 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 12:09:57 -0500 Subject: [PATCH 110/259] Avoid duplicate type change to list --- scripts/shopify_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 5a448743..75c8acc7 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -52,8 +52,9 @@ def run_task(cls, task=None, *args): # Allow unambigious abbreviations of tasks if task not in cls._tasks: matches = filter(lambda item: item.startswith(task), cls._tasks) - if len(list(matches)) == 1: - task = list(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)) From 684947bba780a05a82a3ce07ced936e620d23e81 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 16:51:20 -0500 Subject: [PATCH 111/259] Initial commit of application_credit.py and tests --- shopify/resources/__init__.py | 1 + shopify/resources/application_credit.py | 12 ++++++++++++ test/application_credit_test.py | 15 +++++++++++++++ test/fixtures/application_credit.json | 8 ++++++++ test/fixtures/application_credits.json | 10 ++++++++++ 5 files changed, 46 insertions(+) create mode 100644 shopify/resources/application_credit.py create mode 100644 test/application_credit_test.py create mode 100644 test/fixtures/application_credit.json create mode 100644 test/fixtures/application_credits.json diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 803c5351..16220739 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 diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py new file mode 100644 index 00000000..042a5d5a --- /dev/null +++ b/shopify/resources/application_credit.py @@ -0,0 +1,12 @@ +from ..base import ShopifyResource + +class ApplicationCredit(ShopifyResource): + _prefix_source = "application_credits/$application_credit_id/" + + @classmethod + def _prefix(cls, options={}): + application_credit_id = options.get("application_credit_id") + if application_credit_id: + return "%s/application_credits/%s" % (cls.site, application_credit_id) + else: + return "%s" % (cls.site) diff --git a/test/application_credit_test.py b/test/application_credit_test.py new file mode 100644 index 00000000..0c97dccd --- /dev/null +++ b/test/application_credit_test.py @@ -0,0 +1,15 @@ +import shopify +from test.test_helper import TestCase +from pyactiveresource.activeresource import ActiveResource + +class ApplicationCreditTest(TestCase): + def test_get_application_credit(self): + self.fake("application_credits/445365009", method='GET', body=self.load_fixture('application_credit')) + 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')) + application_credits = shopify.ApplicationCredit.find() + self.assertEqual(1, len(application_credits)) + self.assertEqual(445365009, application_credits[0].id) 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 + } + ] +} From f2afa6ce2acbbcfbff3f4172ac84389aefaea1f8 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 17:32:01 -0500 Subject: [PATCH 112/259] Add test_create_application_credit() function --- test/application_credit_test.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/application_credit_test.py b/test/application_credit_test.py index 0c97dccd..cfa84fad 100644 --- a/test/application_credit_test.py +++ b/test/application_credit_test.py @@ -1,15 +1,39 @@ import shopify +import json from test.test_helper import TestCase from pyactiveresource.activeresource import ActiveResource class ApplicationCreditTest(TestCase): def test_get_application_credit(self): - self.fake("application_credits/445365009", method='GET', body=self.load_fixture('application_credit')) + 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')) + 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"))) From 1c312b489a8dfcf2648a287abd5b1ff65773ff34 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 17:35:13 -0500 Subject: [PATCH 113/259] Remove unused pyactiveresource imports --- test/application_credit_test.py | 1 - test/fulfillment_event_test.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/test/application_credit_test.py b/test/application_credit_test.py index cfa84fad..630d023d 100644 --- a/test/application_credit_test.py +++ b/test/application_credit_test.py @@ -1,7 +1,6 @@ import shopify import json from test.test_helper import TestCase -from pyactiveresource.activeresource import ActiveResource class ApplicationCreditTest(TestCase): def test_get_application_credit(self): diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index 1ba9a73e..c58cc6eb 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -1,6 +1,5 @@ import shopify from test.test_helper import TestCase -from pyactiveresource.activeresource import ActiveResource class FulFillmentEventTest(TestCase): def test_get_fulfillment_event(self): @@ -21,4 +20,3 @@ def test_error_on_incorrect_status(self): fulfillment_event = shopify.FulfillmentEvent.find(12584341209251, order_id='2776493818019', fulfillment_id='2608403447971') fulfillment_event.status = incorrect_status fulfillment_event.save() - From 398e0ec815938280895590f9d9a3a3e73a1c427f Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 8 Feb 2021 17:38:11 -0500 Subject: [PATCH 114/259] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 795baab5..246a91b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ - 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)) == Version 8.2.0 - [Feature] Add support for Dynamic API Versioning. When the library is initialized, it will now make a request to From 7fb299689f838e07e55e3a8cb866d15e52d2fedb Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Tue, 9 Feb 2021 17:24:10 -0500 Subject: [PATCH 115/259] Replace logic in application_credit.py with a pass --- shopify/resources/application_credit.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py index 042a5d5a..83b8de1f 100644 --- a/shopify/resources/application_credit.py +++ b/shopify/resources/application_credit.py @@ -1,12 +1,4 @@ from ..base import ShopifyResource class ApplicationCredit(ShopifyResource): - _prefix_source = "application_credits/$application_credit_id/" - - @classmethod - def _prefix(cls, options={}): - application_credit_id = options.get("application_credit_id") - if application_credit_id: - return "%s/application_credits/%s" % (cls.site, application_credit_id) - else: - return "%s" % (cls.site) + pass From 388b59067e06a9d68b14fc1b4397336974e14d15 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 10 Feb 2021 14:39:06 -0500 Subject: [PATCH 116/259] Initial commit of build.yml changes for codecov --- .github/workflows/build.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04c10fb7..bdeb7157 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,8 @@ name: CI +env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + on: push: @@ -21,8 +24,11 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install pytest mock + pip install pytest mock coverage python setup.py install - name: Run Tests - run: python -m pytest -v - + run: | + python -m pytest -v + coverage run setup.py test + - name: Upload to codecov.io + run: bash <(curl -s https://codecov.io/bash) From fa9c2756c61025053387ffda77b8e3329d2a35cb Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 10 Feb 2021 14:47:51 -0500 Subject: [PATCH 117/259] Testing something --- .github/workflows/build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bdeb7157..21237bda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,11 +24,10 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install pytest mock coverage + pip install pytest mock codecov python setup.py install - name: Run Tests run: | - python -m pytest -v - coverage run setup.py test + coverage run -m pytest -v - name: Upload to codecov.io run: bash <(curl -s https://codecov.io/bash) From 5095cad8c36ffbe2188a1d55f36e85556bde744a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 10 Feb 2021 15:48:19 -0500 Subject: [PATCH 118/259] Add in 3.8 --- setup.py | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f57afb1..579591a1 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries', diff --git a/tox.ini b/tox.ini index 720c4805..1523475c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35, py36, py39 +envlist = py27, py34, py35, py36, py38, py39 skip_missing_interpreters = true [testenv] From 092e6bbcce6ee1e9f885dec24ee40021463b911e Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 10 Feb 2021 16:08:25 -0500 Subject: [PATCH 119/259] Implement codecov github action --- .github/workflows/build.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21237bda..6287f30b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,12 +1,5 @@ name: CI - -env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - -on: - push: - - +on: [push] jobs: build: runs-on: ubuntu-latest @@ -16,7 +9,8 @@ jobs: version: [2.7, 3.6, 3.8] steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -24,10 +18,16 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install pytest mock codecov - python setup.py install + pip install pytest mock pytest-cov + pytest --cov=./ --cov-report=xml + python setup.py install - name: Run Tests run: | - coverage run -m pytest -v - - name: Upload to codecov.io - run: bash <(curl -s https://codecov.io/bash) + python -m pytest -v + - name: upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + name: codecov-umbrella + fail_ci_if_error: true + verbose: true From 1c87c2c01057fc272fe44a8a779a8f56486d8937 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 10 Feb 2021 16:11:22 -0500 Subject: [PATCH 120/259] Comment out fail_ci_if_error for now --- .github/workflows/build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6287f30b..b4d13050 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,12 +22,11 @@ jobs: pytest --cov=./ --cov-report=xml python setup.py install - name: Run Tests - run: | - python -m pytest -v + run: python -m pytest -v - name: upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} name: codecov-umbrella - fail_ci_if_error: true + # fail_ci_if_error: true verbose: true From b5b582bf560b2681ecd696944f90ff453703e386 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 10 Feb 2021 16:12:23 -0500 Subject: [PATCH 121/259] Remove fail_ci_if_error --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4d13050..ad9e130c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,5 +28,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} name: codecov-umbrella - # fail_ci_if_error: true verbose: true From 7b0664e0af9771067b6d81eae8d7c1c584a169a8 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 11 Feb 2021 13:20:29 -0500 Subject: [PATCH 122/259] Add python setup.py install before pytest --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ad9e130c..96f58746 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,8 +19,8 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest mock pytest-cov - pytest --cov=./ --cov-report=xml python setup.py install + pytest --cov=./ --cov-report=xml - name: Run Tests run: python -m pytest -v - name: upload coverage to Codecov From a1791d28799ce4c01062e491e92e5294353a121e Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 11 Feb 2021 13:22:13 -0500 Subject: [PATCH 123/259] Remove verbose: true flag to reduce how much we need to scroll --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96f58746..ff7f64dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,4 +28,3 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} name: codecov-umbrella - verbose: true From 38607936b95727d4c14a0c779a372f8daeee5220 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 11 Feb 2021 13:23:36 -0500 Subject: [PATCH 124/259] Add fail_ci_if_error flag --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff7f64dc..3635102d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,3 +28,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} name: codecov-umbrella + fail_ci_if_error: true From 232aefeaf975ef1dc826d621ad4d3ae652331729 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 11 Feb 2021 13:29:21 -0500 Subject: [PATCH 125/259] Add badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5f65a61f..9e063414 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) +[![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/master/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From 5fc8fd86ffadfede33fc3233eb62e0b2a7387f54 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Thu, 11 Feb 2021 13:50:11 -0500 Subject: [PATCH 126/259] Add license badge to repo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9e063414..f661c726 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) [![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/master/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/master/LICENSE) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From a85be73e81692b53997ed7bebaabad1f82fc90ed Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 12 Feb 2021 09:08:18 -0500 Subject: [PATCH 127/259] Initial commit of .pre-commit-config.yaml --- .pre-commit-config.yaml | 7 +++++++ README.md | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..5eb1904f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +# 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: v3.4.0 + hooks: + - id: end-of-file-fixer diff --git a/README.md b/README.md index 310f2b2f..5c108cad 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,13 @@ next_url = page1.next_page_url page2 = shopify.Product.find(from_=next_url) ``` +## Development on this repo +Install requirements and initialize [pre-commit](https://pre-commit.com/) +```shell +pip install -r requirements.txt +pre-commit install +``` + ## Limitations Currently there is no support for: From 841a920ba592b00cd0def794fffffdbacca3a989 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 12 Feb 2021 09:19:01 -0500 Subject: [PATCH 128/259] Fix the ends of all files in project --- test/api_version_test.py | 1 - test/currency_test.py | 1 - test/discount_code_test.py | 3 --- test/event_test.py | 1 - test/fixtures/access_scopes.json | 2 +- test/fixtures/api_version.json | 2 +- test/fixtures/article.json | 2 +- test/fixtures/articles.json | 2 +- test/fixtures/balance.json | 2 +- test/fixtures/blog.json | 2 +- test/fixtures/blogs.json | 2 +- test/fixtures/carts.json | 2 +- test/fixtures/discount_code.json | 2 +- test/fixtures/discount_codes.json | 2 +- test/fixtures/dispute.json | 2 +- test/fixtures/disputes.json | 2 +- test/fixtures/graphql.json | 1 - test/fixtures/payout.json | 2 +- test/fixtures/payouts.json | 2 +- test/fixtures/payouts_transactions.json | 2 +- test/fixtures/price_rule.json | 2 +- test/fixtures/price_rules.json | 2 +- test/fixtures/recurring_application_charges_no_active.json | 2 +- test/fixtures/shipping_zones.json | 2 +- test/fixtures/transaction.json | 2 +- test/gift_card_test.py | 2 +- test/graphql_test.py | 2 +- test/location_test.py | 2 +- 28 files changed, 23 insertions(+), 30 deletions(-) diff --git a/test/api_version_test.py b/test/api_version_test.py index 9e514f85..18b46ec8 100644 --- a/test/api_version_test.py +++ b/test/api_version_test.py @@ -42,4 +42,3 @@ def test_two_release_versions_with_same_number_are_equal(self): version1 = shopify.Release('2019-01') version2 = shopify.Release('2019-01') self.assertEqual(version1, version2) - diff --git a/test/currency_test.py b/test/currency_test.py index 24782f79..a47ea24d 100644 --- a/test/currency_test.py +++ b/test/currency_test.py @@ -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/discount_code_test.py b/test/discount_code_test.py index 92469928..ff30957f 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -28,6 +28,3 @@ 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/event_test.py b/test/event_test.py index 67e80335..1e022098 100644 --- a/test/event_test.py +++ b/test/event_test.py @@ -9,4 +9,3 @@ def test_prefix_uses_resource(self): 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 index 43d1354d..14150e29 100644 --- a/test/fixtures/api_version.json +++ b/test/fixtures/api_version.json @@ -1 +1 @@ -{"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 +{"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}]}]} 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..908323af 100644 --- a/test/fixtures/carts.json +++ b/test/fixtures/carts.json @@ -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/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..eb892834 100644 --- a/test/fixtures/price_rules.json +++ b/test/fixtures/price_rules.json @@ -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/gift_card_test.py b/test/gift_card_test.py index ca209090..ca4cb5ff 100644 --- a/test/gift_card_test.py +++ b/test/gift_card_test.py @@ -38,4 +38,4 @@ def test_search(self): 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 + self.assertEqual(results[0].balance, "10.00") diff --git a/test/graphql_test.py b/test/graphql_test.py index e6115f46..7b768cc5 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -31,4 +31,4 @@ def setUp(self): def test_fetch_shop_with_graphql(self): - self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers') \ No newline at end of file + self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers') diff --git a/test/location_test.py b/test/location_test.py index 4d22ae6e..f4c40c4c 100644 --- a/test/location_test.py +++ b/test/location_test.py @@ -27,4 +27,4 @@ def test_inventory_levels_returns_all_inventory_levels(self): 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) From c22b8b60c37e3512dc163e554ac9725313658ef1 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 12 Feb 2021 15:45:42 -0500 Subject: [PATCH 129/259] Set up pre-commit GitHub action and make README clearer --- .github/workflows/pre-commit.yml | 14 ++++++++++++++ README.md | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pre-commit.yml diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..72334791 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 diff --git a/README.md b/README.md index 5c108cad..32353aff 100644 --- a/README.md +++ b/README.md @@ -196,8 +196,8 @@ next_url = page1.next_page_url page2 = shopify.Product.find(from_=next_url) ``` -## Development on this repo -Install requirements and initialize [pre-commit](https://pre-commit.com/) +## 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 `master` 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 From a7fef67c60514cb0a33bd6031c159fc2e258e2a8 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 13:04:16 -0500 Subject: [PATCH 130/259] Update discount_code_test.py --- test/discount_code_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/discount_code_test.py b/test/discount_code_test.py index ff30957f..b19d904c 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -28,3 +28,7 @@ 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()) + + + + From 7d051479a8815002483f37653671ab9fd5966df6 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 14:31:51 -0500 Subject: [PATCH 131/259] Update pre-commit.yml with extra_args --- .github/workflows/pre-commit.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 72334791..3dfd6e59 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -12,3 +12,5 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.0 + with: + extra_args: end-of-file-fixer From 4492d35492836c7f641bbb3a4e4bf1fb3f33044b Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 14:33:58 -0500 Subject: [PATCH 132/259] Remove newlines at the end of discount_code_test.py --- test/discount_code_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/discount_code_test.py b/test/discount_code_test.py index b19d904c..ff30957f 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -28,7 +28,3 @@ 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()) - - - - From f9beec44ff5467d45836a558ac2922a242e35581 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 14:55:45 -0500 Subject: [PATCH 133/259] Add newlines to EOF to check pre-commit GitHub Action --- shopify/resources/price_rule.py | 3 +++ test/discount_code_test.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/shopify/resources/price_rule.py b/shopify/resources/price_rule.py index a80d592a..c39e00ba 100644 --- a/shopify/resources/price_rule.py +++ b/shopify/resources/price_rule.py @@ -20,3 +20,6 @@ def create_batch(self, codes = []): 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)) + + + diff --git a/test/discount_code_test.py b/test/discount_code_test.py index ff30957f..db984e8b 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -28,3 +28,5 @@ 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()) + + From 320477639956045b1e5d56a3301fa127541c9db0 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 14:58:58 -0500 Subject: [PATCH 134/259] Modify pre-commit.yml to run end-of-file-fixer on all files --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 3dfd6e59..053e767b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.0 with: - extra_args: end-of-file-fixer + extra_args: end-of-file-fixer --all-files From 429cc792191491e50c9dd015dc28db15f3a39987 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 15:04:33 -0500 Subject: [PATCH 135/259] Run end-of-file-fixer to fix tests --- test/discount_code_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/discount_code_test.py b/test/discount_code_test.py index db984e8b..ff30957f 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -28,5 +28,3 @@ 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()) - - From e358d3f3206efa5b5f9ae10e3edb9dff3d8820c2 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Feb 2021 15:06:49 -0500 Subject: [PATCH 136/259] Run end-of-file-fixer to fix tests --- shopify/resources/price_rule.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/shopify/resources/price_rule.py b/shopify/resources/price_rule.py index c39e00ba..a80d592a 100644 --- a/shopify/resources/price_rule.py +++ b/shopify/resources/price_rule.py @@ -20,6 +20,3 @@ def create_batch(self, codes = []): 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)) - - - From 03c12382461e039924692cc442f495db92579832 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 19 Feb 2021 11:11:21 -0500 Subject: [PATCH 137/259] Add trailing-whitespace hook and run on all files --- .github/workflows/build.yml | 6 +++--- .pre-commit-config.yaml | 1 + CHANGELOG | 2 +- README.md | 2 +- shopify/resources/fulfillment.py | 2 +- test/blog_test.py | 2 +- test/cart_test.py | 2 +- test/checkout_test.py | 2 +- test/customer_saved_search_test.py | 2 +- test/fixtures/carts.json | 2 +- test/fixtures/price_rules.json | 2 +- test/fulfillment_test.py | 2 +- test/graphql_test.py | 8 ++++---- test/shipping_zone_test.py | 3 +-- test/variant_test.py | 2 +- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c751bd38..77870e16 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,11 +4,11 @@ jobs: build: runs-on: ubuntu-latest name: Python ${{ matrix.version }} - strategy: + strategy: matrix: version: [2.7, 3.6, 3.8, 3.9] - steps: + steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -16,7 +16,7 @@ jobs: with: python-version: ${{ matrix.version }} - name: Install Dependencies - run: | + run: | python -m pip install --upgrade pip pip install pytest mock pytest-cov python setup.py install diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5eb1904f..249002ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,3 +5,4 @@ repos: rev: v3.4.0 hooks: - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/CHANGELOG b/CHANGELOG index 246a91b4..5a7a80ab 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ - Add ApplicationCredit resource ([#457](https://github.com/Shopify/shopify_python_api/pull/457)) == Version 8.2.0 -- [Feature] Add support for Dynamic API Versioning. When the library is initialized, it will now make a request to +- [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 diff --git a/README.md b/README.md index 32353aff..5ef0e121 100644 --- a/README.md +++ b/README.md @@ -214,4 +214,4 @@ 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) diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index 39277acc..ed516c35 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -26,7 +26,7 @@ class FulfillmentOrders(ShopifyResource): class FulfillmentV2(ShopifyResource): _singular = 'fulfillment' _plural = 'fulfillments' - + def update_tracking(self, tracking_info, notify_customer): body = { "fulfillment": { diff --git a/test/blog_test.py b/test/blog_test.py index 728f2e18..7f1d16da 100644 --- a/test/blog_test.py +++ b/test/blog_test.py @@ -2,7 +2,7 @@ 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"}) diff --git a/test/cart_test.py b/test/cart_test.py index 3391dc9a..82c5e7d8 100644 --- a/test/cart_test.py +++ b/test/cart_test.py @@ -2,7 +2,7 @@ from test.test_helper import TestCase class CartTest(TestCase): - + def test_all_should_return_all_carts(self): self.fake('carts') carts = shopify.Cart.find() diff --git a/test/checkout_test.py b/test/checkout_test.py index 508a3d9f..76d8e234 100644 --- a/test/checkout_test.py +++ b/test/checkout_test.py @@ -2,7 +2,7 @@ from test.test_helper import TestCase class CheckoutTest(TestCase): - + def test_all_should_return_all_checkouts(self): self.fake('checkouts') checkouts = shopify.Checkout.find() diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py index 5c775af7..ce911197 100644 --- a/test/customer_saved_search_test.py +++ b/test/customer_saved_search_test.py @@ -2,7 +2,7 @@ from test.test_helper import TestCase class CustomerSavedSearchTest(TestCase): - + def setUp(self): super(CustomerSavedSearchTest, self).setUp() self.load_customer_saved_search() diff --git a/test/fixtures/carts.json b/test/fixtures/carts.json index 908323af..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, diff --git a/test/fixtures/price_rules.json b/test/fixtures/price_rules.json index eb892834..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", diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py index 7b88623f..9ed73b03 100644 --- a/test/fulfillment_test.py +++ b/test/fulfillment_test.py @@ -29,7 +29,7 @@ def test_able_to_complete_fulfillment(self): self.assertEqual('pending', fulfillment.status) fulfillment.complete() self.assertEqual('success', fulfillment.status) - + def test_able_to_cancel_fulfillment(self): fulfillment = shopify.Fulfillment.find(255858046, order_id=450789469) diff --git a/test/graphql_test.py b/test/graphql_test.py index 7b768cc5..229dea9a 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -11,12 +11,12 @@ def setUp(self): shopify.ShopifyResource.activate_session(shopify_session) client = shopify.GraphQL() self.fake( - 'graphql', - method='POST', - code=201, + 'graphql', + method='POST', + code=201, headers={ 'X-Shopify-Access-Token': 'token', - 'Accept': 'application/json', + 'Accept': 'application/json', 'Content-Type': 'application/json' }) query = ''' diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py index e81cfe6a..9af1bca4 100644 --- a/test/shipping_zone_test.py +++ b/test/shipping_zone_test.py @@ -6,6 +6,5 @@ def test_get_shipping_zones(self): 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(shipping_zones[0].name,"Some zone") self.assertEqual(3,len(shipping_zones[0].countries)) - diff --git a/test/variant_test.py b/test/variant_test.py index ebfdd8c0..0562a565 100644 --- a/test/variant_test.py +++ b/test/variant_test.py @@ -28,7 +28,7 @@ def test_create_variant_then_add_parent_id(self): 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')) v = shopify.Variant.find(808950810) From 9af6925d16221f1f281dbbe49c434813d57dd273 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 19 Feb 2021 11:28:34 -0500 Subject: [PATCH 138/259] Remove extra args from pre-commit.yml --- .github/workflows/pre-commit.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 053e767b..54fc61d5 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,8 +9,9 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.0 - with: - extra_args: end-of-file-fixer --all-files + - name: Checkout + uses: actions/checkout@v2 + - name: Setup + uses: actions/setup-python@v2 + - name: Pre-commit + uses: pre-commit/action@v2.0.0 From 6469a9a98d1ba35a5f8b0ff8750dca1206913621 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 22 Feb 2021 16:32:05 -0500 Subject: [PATCH 139/259] Add autopep8 to pre-commit config and add config file --- .pre-commit-config.yaml | 4 ++++ pyproject.toml | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 249002ef..d424ed1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,3 +6,7 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v1.5.4 + hooks: + - id: autopep8 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..7ff18c21 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.autopep8] +max_line_length = 120 From 4875ddce63e0b28e802626b7635e813113aadb8a Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 22 Feb 2021 16:34:12 -0500 Subject: [PATCH 140/259] Run autopep8 on all files --- scripts/shopify_api.py | 7 +- setup.py | 8 +- shopify/api_version.py | 5 +- shopify/collection.py | 2 + shopify/mixins.py | 1 + shopify/resources/api_permission.py | 1 + shopify/resources/application_credit.py | 1 + shopify/resources/asset.py | 2 +- shopify/resources/collection_listing.py | 1 + shopify/resources/customer.py | 2 +- shopify/resources/discount_code.py | 1 + shopify/resources/discount_code_creation.py | 1 + shopify/resources/draft_order.py | 8 +- shopify/resources/event.py | 1 + shopify/resources/fulfillment.py | 1 + shopify/resources/fulfillment_event.py | 9 ++- shopify/resources/graphql.py | 1 + shopify/resources/image.py | 4 +- shopify/resources/inventory_level.py | 2 +- shopify/resources/line_item.py | 4 +- shopify/resources/location.py | 1 + shopify/resources/marketing_event.py | 3 +- shopify/resources/order_risk.py | 7 +- shopify/resources/policy.py | 3 +- shopify/resources/price_rule.py | 7 +- shopify/resources/product_listing.py | 1 + .../resources/recurring_application_charge.py | 3 +- shopify/resources/tender_transaction.py | 1 + shopify/resources/usage_charge.py | 1 + shopify/session.py | 7 +- test/access_scope_test.py | 13 ++-- test/api_permission_test.py | 1 + test/application_credit_test.py | 1 + test/article_test.py | 9 ++- test/asset_test.py | 27 ++++--- test/base_test.py | 3 +- test/blog_test.py | 4 +- test/carrier_service_test.py | 4 +- test/cart_test.py | 17 ++-- test/checkout_test.py | 15 ++-- test/collection_listing_test.py | 4 +- test/collection_publication_test.py | 3 +- test/currency_test.py | 1 + test/customer_saved_search_test.py | 9 ++- test/customer_test.py | 13 +++- test/discount_code_creation_test.py | 1 + test/discount_code_test.py | 9 ++- test/draft_order_test.py | 38 +++++---- test/event_test.py | 1 + test/fulfillment_event_test.py | 16 ++-- test/fulfillment_service_test.py | 4 +- test/fulfillment_test.py | 19 +++-- test/gift_card_test.py | 13 +++- test/graphql_test.py | 4 +- test/image_test.py | 19 +++-- test/inventory_item_test.py | 1 + test/inventory_level_test.py | 4 +- test/location_test.py | 9 ++- test/marketing_event_test.py | 16 ++-- test/order_risk_test.py | 77 ++++++++++--------- test/order_test.py | 1 + test/pagination_test.py | 12 +-- test/price_rules_test.py | 10 ++- test/product_listing_test.py | 4 +- test/product_publication_test.py | 3 +- test/product_test.py | 16 ++-- test/publication_test.py | 1 + test/recurring_charge_test.py | 12 ++- test/refund_test.py | 1 + test/report_test.py | 1 + test/resource_feedback_test.py | 16 ++-- test/session_test.py | 72 +++++++++-------- test/shipping_zone_test.py | 7 +- test/shop_test.py | 9 ++- test/storefront_access_token_test.py | 10 ++- test/tender_transaction_test.py | 1 + test/test_helper.py | 11 +-- test/transaction_test.py | 1 + test/usage_charge_test.py | 12 ++- test/user_test.py | 1 + test/variant_test.py | 18 +++-- 81 files changed, 416 insertions(+), 254 deletions(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 75c8acc7..a3de00b6 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -12,6 +12,7 @@ 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()) @@ -19,9 +20,11 @@ def start_interpreter(**variables): 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): @@ -29,6 +32,7 @@ def decorate(func): return func return decorate + class TasksMeta(type): _prog = os.path.basename(sys.argv[0]) @@ -37,6 +41,7 @@ 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) @@ -222,7 +227,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() @@ -253,6 +257,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 579591a1..f6bceabf 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ from setuptools import setup -NAME='ShopifyAPI' +NAME = 'ShopifyAPI' exec(open('shopify/version.py').read()) -DESCRIPTION='Shopify API for Python' -LONG_DESCRIPTION="""\ +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 @@ -27,7 +27,7 @@ ], test_suite='test', tests_require=[ - 'mock>=1.0.1', + 'mock>=1.0.1', ], platforms='Any', classifiers=['Development Status :: 5 - Production/Stable', diff --git a/shopify/api_version.py b/shopify/api_version.py index ce3d609a..be8bf887 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -2,12 +2,15 @@ import json from six.moves.urllib import request + class InvalidVersionError(Exception): pass + class VersionNotFoundError(Exception): pass + class ApiVersion(object): versions = {} @@ -76,7 +79,7 @@ class Unstable(ApiVersion): def __init__(self): 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/collection.py b/shopify/collection.py index b254218f..b233ace0 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -2,6 +2,7 @@ from six.moves.urllib.parse import urlparse, parse_qs import cgi + class PaginatedCollection(Collection): """ A subclass of Collection which allows cycling through pages of @@ -141,6 +142,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/mixins.py b/shopify/mixins.py index c7806a0c..38840554 100644 --- a/shopify/mixins.py +++ b/shopify/mixins.py @@ -1,5 +1,6 @@ import shopify.resources + class Countable(object): @classmethod diff --git a/shopify/resources/api_permission.py b/shopify/resources/api_permission.py index 0f4f9fc6..3b4fc016 100644 --- a/shopify/resources/api_permission.py +++ b/shopify/resources/api_permission.py @@ -1,5 +1,6 @@ from ..base import ShopifyResource + class ApiPermission(ShopifyResource): @classmethod diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py index 83b8de1f..ecc12fa0 100644 --- a/shopify/resources/application_credit.py +++ b/shopify/resources/application_credit.py @@ -1,4 +1,5 @@ from ..base import ShopifyResource + class ApplicationCredit(ShopifyResource): pass diff --git a/shopify/resources/asset.py b/shopify/resources/asset.py index 899e026f..643e58bc 100644 --- a/shopify/resources/asset.py +++ b/shopify/resources/asset.py @@ -18,7 +18,7 @@ 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, + return "%s%s.%s%s" % (cls._prefix(prefix_options) + '/', cls.plural, cls.format.extension, cls._query_string(query_options)) @classmethod diff --git a/shopify/resources/collection_listing.py b/shopify/resources/collection_listing.py index 7bd25107..497dc041 100644 --- a/shopify/resources/collection_listing.py +++ b/shopify/resources/collection_listing.py @@ -1,5 +1,6 @@ from ..base import ShopifyResource + class CollectionListing(ShopifyResource): _primary_key = "collection_id" diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py index ca4a51f2..80be7fc5 100644 --- a/shopify/resources/customer.py +++ b/shopify/resources/customer.py @@ -21,6 +21,6 @@ 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)) 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..99c919e9 100644 --- a/shopify/resources/discount_code_creation.py +++ b/shopify/resources/discount_code_creation.py @@ -1,6 +1,7 @@ from ..base import ShopifyResource from .discount_code import DiscountCode + class DiscountCodeCreation(ShopifyResource): _prefix_source = "/price_rules/$price_rule_id/" diff --git a/shopify/resources/draft_order.py b/shopify/resources/draft_order.py index b8492d36..a8aa7bc4 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 = {}): + def complete(self, params={}): if params.get('payment_pending', False): - self._load_attributes_from_response(self.put("complete", payment_pending='true')) + 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 ed516c35..81a79b74 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -23,6 +23,7 @@ def update_tracking(self, tracking_info, notify_customer): class FulfillmentOrders(ShopifyResource): _prefix_source = "/orders/$order_id/" + class FulfillmentV2(ShopifyResource): _singular = 'fulfillment' _plural = 'fulfillments' diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py index 886e4598..6341d2b9 100644 --- a/shopify/resources/fulfillment_event.py +++ b/shopify/resources/fulfillment_event.py @@ -1,5 +1,6 @@ from ..base import ShopifyResource + class FulfillmentEvent(ShopifyResource): _prefix_source = "/orders/$order_id/fulfillments/$fulfillment_id/" _singular = 'event' @@ -15,7 +16,7 @@ def _prefix(cls, options={}): 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/graphql.py b/shopify/resources/graphql.py index b518be31..3bd73f34 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -3,6 +3,7 @@ from six.moves import urllib import json + class GraphQL(): def __init__(self): diff --git a/shopify/resources/image.py b/shopify/resources/image.py index fc45ccc8..e5827e06 100644 --- a/shopify/resources/image.py +++ b/shopify/resources/image.py @@ -30,8 +30,8 @@ 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: diff --git a/shopify/resources/inventory_level.py b/shopify/resources/inventory_level.py index 8f24c723..45c21b80 100644 --- a/shopify/resources/inventory_level.py +++ b/shopify/resources/inventory_level.py @@ -13,7 +13,7 @@ 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, + return "%s%s.%s%s" % (cls._prefix(prefix_options) + '/', cls.plural, cls.format.extension, cls._query_string(query_options)) @classmethod 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..31e828cd 100644 --- a/shopify/resources/location.py +++ b/shopify/resources/location.py @@ -1,6 +1,7 @@ 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" % ( diff --git a/shopify/resources/marketing_event.py b/shopify/resources/marketing_event.py index 9e07deb8..d5c1b70b 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 }) + engagements_json = json.dumps({'engagements': engagements}) return self.post('engagements', engagements_json.encode()) 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..b75943cd 100644 --- a/shopify/resources/price_rule.py +++ b/shopify/resources/price_rule.py @@ -3,16 +3,17 @@ 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)) diff --git a/shopify/resources/product_listing.py b/shopify/resources/product_listing.py index cd11ac2e..119c23eb 100644 --- a/shopify/resources/product_listing.py +++ b/shopify/resources/product_listing.py @@ -1,5 +1,6 @@ from ..base import ShopifyResource + class ProductListing(ShopifyResource): _primary_key = "product_id" diff --git a/shopify/resources/recurring_application_charge.py b/shopify/resources/recurring_application_charge.py index d7b0d9ed..28cf7771 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: @@ -14,7 +15,7 @@ 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/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/session.py b/shopify/session.py index 0490e27c..e6a607b9 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -12,9 +12,11 @@ from shopify.api_version import ApiVersion, Release, Unstable import six + class ValidationException(Exception): pass + class Session(object): api_key = None secret = None @@ -49,7 +51,8 @@ def __init__(self, shop_url, version=None, token=None): 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 + 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): @@ -148,7 +151,7 @@ def encoded_pairs(params): continue if k.endswith('[]'): - #foo[]=1&foo[]=2 has to be transformed as foo=["1", "2"] note the whitespace after comma + # 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))) diff --git a/test/access_scope_test.py b/test/access_scope_test.py index fa231a78..9aff6cce 100644 --- a/test/access_scope_test.py +++ b/test/access_scope_test.py @@ -1,11 +1,12 @@ 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) + 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_permission_test.py b/test/api_permission_test.py index 541e060b..0104097f 100644 --- a/test/api_permission_test.py +++ b/test/api_permission_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class ApiPermissionTest(TestCase): def test_delete_api_permission(self): diff --git a/test/application_credit_test.py b/test/application_credit_test.py index 630d023d..81c2c18f 100644 --- a/test/application_credit_test.py +++ b/test/application_credit_test.py @@ -2,6 +2,7 @@ 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) diff --git a/test/article_test.py b/test/article_test.py index 58e29b02..a1a42b3b 100644 --- a/test/article_test.py +++ b/test/article_test.py @@ -1,11 +1,13 @@ import shopify from test.test_helper import 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) @@ -18,7 +20,8 @@ def test_update_article(self): 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): diff --git a/test/asset_test.py b/test/asset_test.py index c7c7b153..de2c14f6 100644 --- a/test/asset_test.py +++ b/test/asset_test.py @@ -3,6 +3,7 @@ import shopify from test.test_helper import TestCase + class AssetTest(TestCase): def test_get_assets(self): @@ -10,11 +11,13 @@ def test_get_assets(self): 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')) + 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')) + 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'}) @@ -22,28 +25,34 @@ def test_update_asset(self): 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) + 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')) + 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')) + 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')) + 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'}) + 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 diff --git a/test/base_test.py b/test/base_test.py index 88478570..039cfc5b 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -4,6 +4,7 @@ from mock import patch import threading + class BaseTest(TestCase): @classmethod @@ -69,7 +70,7 @@ def test_activate_session_with_one_session_then_clearing_and_activating_with_ano def test_delete_should_send_custom_headers_with_request(self): shopify.ShopifyResource.activate_session(self.session1) - org_headers=shopify.ShopifyResource.headers + org_headers = shopify.ShopifyResource.headers shopify.ShopifyResource.set_headers({'X-Custom': 'abc'}) with patch('shopify.ShopifyResource.connection.delete') as mock: diff --git a/test/blog_test.py b/test/blog_test.py index 7f1d16da..f88fcaf7 100644 --- a/test/blog_test.py +++ b/test/blog_test.py @@ -1,9 +1,11 @@ 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'}) + 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..a317269e 100644 --- a/test/carrier_service_test.py +++ b/test/carrier_service_test.py @@ -1,9 +1,11 @@ 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"}) self.assertEqual("Some Postal Service", carrier_service.name) diff --git a/test/cart_test.py b/test/cart_test.py index 82c5e7d8..8936f828 100644 --- a/test/cart_test.py +++ b/test/cart_test.py @@ -1,13 +1,14 @@ 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 76d8e234..55549b4f 100644 --- a/test/checkout_test.py +++ b/test/checkout_test.py @@ -1,12 +1,13 @@ 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..efb62708 100644 --- a/test/collection_listing_test.py +++ b/test/collection_listing_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class CollectionListingTest(TestCase): def test_get_collection_listings(self): @@ -30,7 +31,8 @@ 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..8cc61fcd 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') + body=self.load_fixture('collection_publications') ) collection_publications = shopify.CollectionPublication.find(publication_id=55650051) diff --git a/test/currency_test.py b/test/currency_test.py index a47ea24d..4982c06f 100644 --- a/test/currency_test.py +++ b/test/currency_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class CurrencyTest(TestCase): def test_get_currencies(self): diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py index ce911197..207da549 100644 --- a/test/customer_saved_search_test.py +++ b/test/customer_saved_search_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class CustomerSavedSearchTest(TestCase): def setUp(self): @@ -8,13 +9,15 @@ def setUp(self): 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) diff --git a/test/customer_test.py b/test/customer_test.py index ef237b84..5819f292 100644 --- a/test/customer_test.py +++ b/test/customer_test.py @@ -2,6 +2,7 @@ import json from test.test_helper import TestCase + class CustomerTest(TestCase): def setUp(self): super(CustomerTest, self).setUp() @@ -9,7 +10,8 @@ def setUp(self): 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' @@ -26,7 +28,8 @@ 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) @@ -34,7 +37,8 @@ def test_search(self): def test_send_invite_with_no_params(self): 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) @@ -43,7 +47,8 @@ def test_send_invite_with_no_params(self): def test_send_invite_with_params(self): 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(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) diff --git a/test/discount_code_creation_test.py b/test/discount_code_creation_test.py index be814cc9..6a8a078d 100644 --- a/test/discount_code_creation_test.py +++ b/test/discount_code_creation_test.py @@ -1,6 +1,7 @@ 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')) diff --git a/test/discount_code_test.py b/test/discount_code_test.py index ff30957f..ea93b38b 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -23,8 +23,9 @@ def test_update_a_specific_discount_code(self): 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.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/draft_order_test.py b/test/draft_order_test.py index ef155ac2..b77c0fe8 100644 --- a/test/draft_order_test.py +++ b/test/draft_order_test.py @@ -26,25 +26,30 @@ def test_get_count_draft_orders(self): 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.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']) def test_send_invoice_with_no_params(self): 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) @@ -53,8 +58,10 @@ def test_send_invoice_with_no_params(self): def test_send_invoice_with_params(self): 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) @@ -65,10 +72,13 @@ def test_delete_draft_order(self): 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.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) @@ -83,7 +93,7 @@ def test_get_metafields_for_draft_order(self): 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'} + 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) diff --git a/test/event_test.py b/test/event_test.py index 1e022098..7c36aade 100644 --- a/test/event_test.py +++ b/test/event_test.py @@ -1,6 +1,7 @@ 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}) diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index c58cc6eb..cf8d52f8 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -1,22 +1,28 @@ import shopify from test.test_helper import TestCase + 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'}) + 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')) + 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 = 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..5313be7a 100644 --- a/test/fulfillment_service_test.py +++ b/test/fulfillment_service_test.py @@ -1,9 +1,11 @@ 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"}) self.assertEqual("SomeService", fulfillment_service.name) diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py index 9ed73b03..312ee7d8 100644 --- a/test/fulfillment_test.py +++ b/test/fulfillment_test.py @@ -2,6 +2,7 @@ from test.test_helper import TestCase from pyactiveresource.activeresource import ActiveResource + class FulFillmentTest(TestCase): def setUp(self): @@ -12,8 +13,9 @@ 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 = 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) fulfillment.open() @@ -23,8 +25,9 @@ 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 = 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) fulfillment.complete() @@ -35,7 +38,8 @@ def test_able_to_cancel_fulfillment(self): 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.fake("orders/450789469/fulfillments/255858046/cancel", method='POST', + headers={'Content-length': '0', 'Content-type': 'application/json'}, body=cancelled) self.assertEqual('pending', fulfillment.status) fulfillment.cancel() @@ -44,7 +48,7 @@ def test_able_to_cancel_fulfillment(self): 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') @@ -52,7 +56,8 @@ def test_update_tracking(self): 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) diff --git a/test/gift_card_test.py b/test/gift_card_test.py index ca4cb5ff..4897b0c7 100644 --- a/test/gift_card_test.py +++ b/test/gift_card_test.py @@ -2,10 +2,12 @@ import shopify from test.test_helper import 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'}) + 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) @@ -17,7 +19,8 @@ def test_fetch_gift_cards(self): 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/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() @@ -25,7 +28,8 @@ def test_disable_gift_card(self): 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/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({ @@ -35,7 +39,8 @@ def test_adjust_gift_card(self): 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") diff --git a/test/graphql_test.py b/test/graphql_test.py index 229dea9a..5075bdfe 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -2,6 +2,7 @@ import json from test.test_helper import TestCase + class GraphQLTest(TestCase): def setUp(self): @@ -18,7 +19,7 @@ def setUp(self): 'X-Shopify-Access-Token': 'token', 'Accept': 'application/json', 'Content-Type': 'application/json' - }) + }) query = ''' { shop { @@ -29,6 +30,5 @@ def setUp(self): ''' self.result = client.execute(query) - def test_fetch_shop_with_graphql(self): self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers') diff --git a/test/image_test.py b/test/image_test.py index bde789ec..308dd6c1 100644 --- a/test/image_test.py +++ b/test/image_test.py @@ -2,11 +2,13 @@ from test.test_helper import TestCase import base64 + 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() @@ -15,10 +17,12 @@ def test_create_image(self): 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==") + 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) @@ -28,7 +32,8 @@ def test_attach_image(self): 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 @@ -52,7 +57,7 @@ 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')) - 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..d2d12ab7 100644 --- a/test/inventory_item_test.py +++ b/test/inventory_item_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class InventoryItemTest(TestCase): def test_fetch_inventory_item(self): diff --git a/test/inventory_level_test.py b/test/inventory_level_test.py index c969c1d0..51c69d95 100644 --- a/test/inventory_level_test.py +++ b/test/inventory_level_test.py @@ -3,6 +3,7 @@ from six.moves.urllib.parse import urlencode from test.test_helper import TestCase + class InventoryLevelTest(TestCase): def test_fetch_inventory_level(self): @@ -19,7 +20,8 @@ def test_fetch_inventory_level(self): 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): diff --git a/test/location_test.py b/test/location_test.py index f4c40c4c..f93dbb96 100644 --- a/test/location_test.py +++ b/test/location_test.py @@ -2,20 +2,21 @@ 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')) 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')) 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, diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py index a49afa9a..3ee079c9 100644 --- a/test/marketing_event_test.py +++ b/test/marketing_event_test.py @@ -18,7 +18,8 @@ def test_get_marketing_events(self): 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' @@ -41,7 +42,8 @@ def test_delete_marketing_event(self): 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='PUT', code=200, + body=self.load_fixture('marketing_event'), headers={'Content-type': 'application/json'}) marketing_event = shopify.MarketingEvent.find(1) marketing_event.currency = 'USD' @@ -56,11 +58,11 @@ def test_count_marketing_events(self): def test_add_engagements(self): 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) diff --git a/test/order_risk_test.py b/test/order_risk_test.py index fb8b09b8..83dc5afa 100644 --- a/test/order_risk_test.py +++ b/test/order_risk_test.py @@ -1,42 +1,45 @@ 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() + 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..46278a72 100644 --- a/test/order_test.py +++ b/test/order_test.py @@ -3,6 +3,7 @@ from pyactiveresource.activeresource import ActiveResource from pyactiveresource.util import xml_to_dict + class OrderTest(TestCase): def test_should_be_loaded_correctly_from_order_xml(self): diff --git a/test/pagination_test.py b/test/pagination_test.py index 6b00bb78..43681657 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -2,6 +2,7 @@ import json from test.test_helper import TestCase + class PaginationTest(TestCase): def setUp(self): @@ -17,15 +18,15 @@ def setUp(self): self.fake("products", url=prefix + "/products.json?limit=2", - body=json.dumps({ "products": fixture[: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] }), + 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] }), + body=json.dumps({"products": fixture[:2]}), response_headers=next_headers) def test_nonpaginates_collection(self): @@ -33,7 +34,8 @@ def test_nonpaginates_collection(self): 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) @@ -67,7 +69,7 @@ def test_pagination_previous(self): self.assertIsInstance(p, shopify.collection.PaginatedCollection, "previous_page() result is not PaginatedCollection") - self.assertEqual(len(p), 4, # cached + self.assertEqual(len(p), 4, # cached "previous_page() collection has incorrect length") self.assertIn("pagination", p.metadata) self.assertIn("next", p.metadata["pagination"], diff --git a/test/price_rules_test.py b/test/price_rules_test.py index 0c822cf4..aef98bef 100644 --- a/test/price_rules_test.py +++ b/test/price_rules_test.py @@ -26,11 +26,11 @@ def test_get_all_price_rules(self): 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']) - def test_delete_price_rule(self): self.fake('price_rules/1213131', method='DELETE', body='destroyed') self.price_rule.destroy() @@ -56,7 +56,8 @@ def test_price_rule_creation(self): 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)) @@ -67,7 +68,8 @@ def test_add_discount_code(self): 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'])) + 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) diff --git a/test/product_listing_test.py b/test/product_listing_test.py index e0726b9f..e67fb3fd 100644 --- a/test/product_listing_test.py +++ b/test/product_listing_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class ProductListingTest(TestCase): def test_get_product_listings(self): @@ -29,7 +30,8 @@ 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..2a76cd13 100644 --- a/test/product_publication_test.py +++ b/test/product_publication_test.py @@ -2,12 +2,13 @@ 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') + body=self.load_fixture('product_publications') ) product_publications = shopify.ProductPublication.find(publication_id=55650051) diff --git a/test/product_test.py b/test/product_test.py index dcc9ae72..b7ea1f1a 100644 --- a/test/product_test.py +++ b/test/product_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class ProductTest(TestCase): def setUp(self): @@ -10,9 +11,11 @@ def setUp(self): 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'}) + 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"})) + 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) @@ -43,7 +46,8 @@ def test_get_metafields_for_product_count(self): 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) @@ -56,7 +60,9 @@ def test_update_loaded_variant(self): 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..d352952d 100644 --- a/test/publication_test.py +++ b/test/publication_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class PublicationTest(TestCase): def test_find_all_publications(self): self.fake('publications') diff --git a/test/recurring_charge_test.py b/test/recurring_charge_test.py index 7d178c20..153a9ea6 100644 --- a/test/recurring_charge_test.py +++ b/test/recurring_charge_test.py @@ -1,10 +1,12 @@ 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=" ") + 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() @@ -26,7 +28,8 @@ 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,8 +38,9 @@ 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): diff --git a/test/refund_test.py b/test/refund_test.py index 5750a1b0..300d888f 100644 --- a/test/refund_test.py +++ b/test/refund_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class RefundTest(TestCase): def setUp(self): super(RefundTest, self).setUp() diff --git a/test/report_test.py b/test/report_test.py index 4de7fd23..6768a599 100644 --- a/test/report_test.py +++ b/test/report_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class CustomerSavedSearchTest(TestCase): def test_get_report(self): diff --git a/test/resource_feedback_test.py b/test/resource_feedback_test.py index 532bc5cb..49e040d9 100644 --- a/test/resource_feedback_test.py +++ b/test/resource_feedback_test.py @@ -2,9 +2,10 @@ 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' } ] }) + body = json.dumps({'resource_feedback': [{'resource_type': 'Shop'}]}) self.fake('resource_feedback', method='GET', body=body) feedback = shopify.ResourceFeedback.find() @@ -12,15 +13,15 @@ def test_get_resource_feedback(self): 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' } ] }) + 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) @@ -28,10 +29,11 @@ def test_get_resource_feedback_with_product_id(self): 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..f45933b5 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -6,6 +6,7 @@ from six.moves import urllib from six import u + class SessionTest(TestCase): @classmethod @@ -40,7 +41,7 @@ def test_append_the_myshopify_domain_if_not_given(self): 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'}) + 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") @@ -91,43 +92,49 @@ def test_create_permission_url_returns_correct_url_with_single_scope_and_redirec 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)) + 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): 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"] + 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)) + 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): shopify.Session.setup(api_key="My_test_key", secret="My test secret") 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)) + 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)) def test_create_permission_url_returns_correct_url_with_no_scope_and_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') 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)) + 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)) def test_create_permission_url_returns_correct_url_with_single_scope_and_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') 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)) + 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) + 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) @@ -137,45 +144,45 @@ def test_return_site_for_session(self): 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']) 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() 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[]': [ @@ -187,18 +194,19 @@ def test_param_validation_of_param_values_with_lists(self): self.assertEqual(True, shopify.Session.validate_hmac(params)) def test_return_token_if_hmac_is_valid(self): - shopify.Session.secret='secret' + 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) + 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') token = session.request_token(params) self.assertEqual("token", token) def test_raise_error_if_hmac_is_invalid(self): - shopify.Session.secret='secret' + shopify.Session.secret = 'secret' params = {'code': 'any-code', 'timestamp': time.time()} params['hmac'] = 'a94a110d86d2452e92a4a64275b128e9273be3037f2c339eb3e2af4cfb8a3828' @@ -207,7 +215,7 @@ def test_raise_error_if_hmac_is_invalid(self): session = session.request_token(params) def test_raise_error_if_hmac_does_not_match_expected(self): - shopify.Session.secret='secret' + shopify.Session.secret = 'secret' params = {'foo': 'hello', 'timestamp': time.time()} hmac = shopify.Session.calculate_hmac(params) params['hmac'] = hmac @@ -219,9 +227,9 @@ def test_raise_error_if_hmac_does_not_match_expected(self): 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 diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py index 9af1bca4..80781354 100644 --- a/test/shipping_zone_test.py +++ b/test/shipping_zone_test.py @@ -1,10 +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')) 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..140e90da 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,11 @@ 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'}) + 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"})) + 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..d8a2edad 100644 --- a/test/storefront_access_token_test.py +++ b/test/storefront_access_token_test.py @@ -1,15 +1,18 @@ 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' }) + 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') @@ -17,7 +20,8 @@ def test_get_and_delete_storefront_access_token(self): 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..e1b6851b 100644 --- a/test/tender_transaction_test.py +++ b/test/tender_transaction_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class TenderTransactionTest(TestCase): def setUp(self): super(TenderTransactionTest, self).setUp() diff --git a/test/test_helper.py b/test/test_helper.py index 7dc8644f..26213c1f 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -5,11 +5,12 @@ from pyactiveresource.testing import http_fake import shopify + 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" @@ -30,13 +31,13 @@ def setUp(self): ) def load_fixture(self, name, format='json'): - with open(os.path.dirname(__file__)+'/fixtures/%s.%s' % (name, format), 'rb') as f: + 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') + 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']): @@ -55,7 +56,7 @@ def fake(self, endpoint, **kwargs): try: headers.update(kwargs['headers']) except KeyError: - pass + pass code = kwargs.pop('code', 200) diff --git a/test/transaction_test.py b/test/transaction_test.py index 79335eee..6b44b53b 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class TransactionTest(TestCase): def setUp(self): super(TransactionTest, self).setUp() diff --git a/test/usage_charge_test.py b/test/usage_charge_test.py index ab341fb3..47cca7bb 100644 --- a/test/usage_charge_test.py +++ b/test/usage_charge_test.py @@ -1,16 +1,20 @@ 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) 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) + 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..35caac26 100644 --- a/test/user_test.py +++ b/test/user_test.py @@ -1,6 +1,7 @@ import shopify from test.test_helper import TestCase + class UserTest(TestCase): def test_get_all_users(self): diff --git a/test/variant_test.py b/test/variant_test.py index 0562a565..7474c2dd 100644 --- a/test/variant_test.py +++ b/test/variant_test.py @@ -1,30 +1,34 @@ import shopify from test.test_helper import 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) + 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) + 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) + 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() From 91a8fd8f69d446ebcfdd4a1cefd80e3f5c2979e8 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Tue, 23 Feb 2021 17:07:40 -0500 Subject: [PATCH 141/259] Add method for getting order using customer_id with tests --- shopify/resources/order.py | 9 + test/fixtures/orders.json | 935 +++++++++++++++++++++++++++++++++++++ test/order_test.py | 7 + 3 files changed, 951 insertions(+) create mode 100644 test/fixtures/orders.json 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/test/fixtures/orders.json b/test/fixtures/orders.json new file mode 100644 index 00000000..58ebd2d1 --- /dev/null +++ b/test/fixtures/orders.json @@ -0,0 +1,935 @@ +{ + "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, + "discount_applications": [ + { + "type": "discount_code", + "value": "10.0", + "value_type": "fixed_amount", + "allocation_method": "across", + "target_selection": "all", + "target_type": "line_item", + "code": "TENOFF" + } + ], + "discount_codes": [ + { + "code": "TENOFF", + "amount": "10.00", + "type": "fixed_amount" + } + ], + "note_attributes": [ + { + "name": "custom engraving", + "value": "Happy Birthday" + }, + { + "name": "colour", + "value": "green" + } + ], + "payment_details": { + "credit_card_bin": null, + "avs_result_code": null, + "cvv_result_code": null, + "credit_card_number": "•••• •••• •••• 4242", + "credit_card_company": "Visa", + "credit_card_name": null, + "credit_card_wallet": null, + "credit_card_expiration_month": null, + "credit_card_expiration_year": null + }, + "payment_gateway_names": [ + "bogus" + ], + "processing_method": "direct", + "checkout_id": 901414060, + "source_name": "web", + "fulfillment_status": null, + "tax_lines": [ + { + "price": "11.94", + "rate": 0.06, + "title": "State Tax", + "price_set": { + "shop_money": { + "amount": "11.94", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "11.94", + "currency_code": "USD" + } + } + } + ], + "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" + } + }, + "line_items": [ + { + "id": 466157049, + "variant_id": 39072856, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008GREEN", + "variant_title": "green", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - green", + "variant_inventory_management": "shopify", + "properties": [ + { + "name": "Custom Engraving Front", + "value": "Happy Birthday" + }, + { + "name": "Custom Engraving Back", + "value": "Merry Christmas" + } + ], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.34", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.34", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.34", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/466157049", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + }, + { + "id": 518995019, + "variant_id": 49148385, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008RED", + "variant_title": "red", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - red", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.33", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.33", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.33", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/518995019", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + }, + { + "id": 703073504, + "variant_id": 457924702, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008BLACK", + "variant_title": "black", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - black", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.33", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.33", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.33", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/703073504", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + } + ], + "fulfillments": [ + { + "id": 255858046, + "order_id": 450789469, + "status": "failure", + "created_at": "2021-02-12T13:51:00-05:00", + "service": "manual", + "updated_at": "2021-02-12T13:51:00-05:00", + "tracking_company": "USPS", + "shipment_status": null, + "location_id": 905684977, + "line_items": [ + { + "id": 466157049, + "variant_id": 39072856, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008GREEN", + "variant_title": "green", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - green", + "variant_inventory_management": "shopify", + "properties": [ + { + "name": "Custom Engraving Front", + "value": "Happy Birthday" + }, + { + "name": "Custom Engraving Back", + "value": "Merry Christmas" + } + ], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.34", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.34", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.34", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/466157049", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + } + ], + "tracking_number": "1Z2345", + "tracking_numbers": [ + "1Z2345" + ], + "tracking_url": "https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345", + "tracking_urls": [ + "https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345" + ], + "receipt": { + "testcase": true, + "authorization": "123456" + }, + "name": "#1001.0", + "admin_graphql_api_id": "gid://shopify/Fulfillment/255858046" + } + ], + "refunds": [ + { + "id": 509562969, + "order_id": 450789469, + "created_at": "2021-02-12T13:51:00-05:00", + "note": "it broke during shipping", + "user_id": 799407056, + "processed_at": "2021-02-12T13:51:00-05:00", + "restock": true, + "duties": [], + "total_duties_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "admin_graphql_api_id": "gid://shopify/Refund/509562969", + "refund_line_items": [ + { + "id": 104689539, + "quantity": 1, + "line_item_id": 703073504, + "location_id": 487838322, + "restock_type": "legacy_restock", + "subtotal": 195.67, + "total_tax": 3.98, + "subtotal_set": { + "shop_money": { + "amount": "195.67", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "195.67", + "currency_code": "USD" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + }, + "line_item": { + "id": 703073504, + "variant_id": 457924702, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008BLACK", + "variant_title": "black", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - black", + "variant_inventory_management": "shopify", + "properties": [], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.33", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.33", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.33", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/703073504", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + } + }, + { + "id": 709875399, + "quantity": 1, + "line_item_id": 466157049, + "location_id": 487838322, + "restock_type": "legacy_restock", + "subtotal": 195.66, + "total_tax": 3.98, + "subtotal_set": { + "shop_money": { + "amount": "195.66", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "195.66", + "currency_code": "USD" + } + }, + "total_tax_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + }, + "line_item": { + "id": 466157049, + "variant_id": 39072856, + "title": "IPod Nano - 8gb", + "quantity": 1, + "sku": "IPOD2008GREEN", + "variant_title": "green", + "vendor": null, + "fulfillment_service": "manual", + "product_id": 632910392, + "requires_shipping": true, + "taxable": true, + "gift_card": false, + "name": "IPod Nano - 8gb - green", + "variant_inventory_management": "shopify", + "properties": [ + { + "name": "Custom Engraving Front", + "value": "Happy Birthday" + }, + { + "name": "Custom Engraving Back", + "value": "Merry Christmas" + } + ], + "product_exists": true, + "fulfillable_quantity": 1, + "grams": 200, + "price": "199.00", + "total_discount": "0.00", + "fulfillment_status": null, + "price_set": { + "shop_money": { + "amount": "199.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "199.00", + "currency_code": "USD" + } + }, + "total_discount_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [ + { + "amount": "3.34", + "discount_application_index": 0, + "amount_set": { + "shop_money": { + "amount": "3.34", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.34", + "currency_code": "USD" + } + } + } + ], + "duties": [], + "admin_graphql_api_id": "gid://shopify/LineItem/466157049", + "tax_lines": [ + { + "title": "State Tax", + "price": "3.98", + "rate": 0.06, + "price_set": { + "shop_money": { + "amount": "3.98", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "3.98", + "currency_code": "USD" + } + } + } + ] + } + } + ], + "transactions": [ + { + "id": 179259969, + "order_id": 450789469, + "kind": "refund", + "gateway": "bogus", + "status": "success", + "message": null, + "created_at": "2005-08-05T12:59:12-04:00", + "test": false, + "authorization": "authorization-key", + "location_id": null, + "user_id": null, + "parent_id": 801038806, + "processed_at": "2005-08-05T12:59:12-04:00", + "device_id": null, + "error_code": null, + "source_name": "web", + "receipt": {}, + "amount": "209.00", + "currency": "USD", + "admin_graphql_api_id": "gid://shopify/OrderTransaction/179259969" + } + ], + "order_adjustments": [] + } + ], + "total_tip_received": "0.0", + "original_total_duties_set": null, + "current_total_duties_set": null, + "admin_graphql_api_id": "gid://shopify/Order/450789469", + "shipping_lines": [ + { + "id": 369256396, + "title": "Free Shipping", + "price": "0.00", + "code": "Free Shipping", + "source": "shopify", + "phone": null, + "requested_fulfillment_service_id": null, + "delivery_category": null, + "carrier_identifier": null, + "discounted_price": "0.00", + "price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discounted_price_set": { + "shop_money": { + "amount": "0.00", + "currency_code": "USD" + }, + "presentment_money": { + "amount": "0.00", + "currency_code": "USD" + } + }, + "discount_allocations": [], + "tax_lines": [] + } + ], + "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/order_test.py b/test/order_test.py index 46278a72..a915a1ab 100644 --- a/test/order_test.py +++ b/test/order_test.py @@ -48,3 +48,10 @@ def test_get_order_transaction(self): 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) From 5b67f82402b53d272b3617b3ee67a17974afbe24 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Tue, 23 Feb 2021 17:14:16 -0500 Subject: [PATCH 142/259] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 5a7a80ab..6d863d7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ - 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 From 31dfa868760eca88439cf8fe9f068431fe907221 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 24 Feb 2021 15:55:28 -0500 Subject: [PATCH 143/259] Remove unused data from orders.json --- test/fixtures/orders.json | 731 +------------------------------------- 1 file changed, 1 insertion(+), 730 deletions(-) diff --git a/test/fixtures/orders.json b/test/fixtures/orders.json index 58ebd2d1..925a4a5d 100644 --- a/test/fixtures/orders.json +++ b/test/fixtures/orders.json @@ -51,69 +51,13 @@ }, "landing_site_ref": "abc", "order_number": 1001, - "discount_applications": [ - { - "type": "discount_code", - "value": "10.0", - "value_type": "fixed_amount", - "allocation_method": "across", - "target_selection": "all", - "target_type": "line_item", - "code": "TENOFF" - } - ], - "discount_codes": [ - { - "code": "TENOFF", - "amount": "10.00", - "type": "fixed_amount" - } - ], - "note_attributes": [ - { - "name": "custom engraving", - "value": "Happy Birthday" - }, - { - "name": "colour", - "value": "green" - } - ], "payment_details": { - "credit_card_bin": null, - "avs_result_code": null, - "cvv_result_code": null, "credit_card_number": "•••• •••• •••• 4242", - "credit_card_company": "Visa", - "credit_card_name": null, - "credit_card_wallet": null, - "credit_card_expiration_month": null, - "credit_card_expiration_year": null + "credit_card_company": "Visa" }, "payment_gateway_names": [ "bogus" ], - "processing_method": "direct", - "checkout_id": 901414060, - "source_name": "web", - "fulfillment_status": null, - "tax_lines": [ - { - "price": "11.94", - "rate": 0.06, - "title": "State Tax", - "price_set": { - "shop_money": { - "amount": "11.94", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "11.94", - "currency_code": "USD" - } - } - } - ], "tags": "", "contact_email": "bob.norman@hostmail.com", "order_status_url": "https://apple.myshopify.com/690933842/orders/b1946ac92492d2347c6235b4d2611184/authenticate?key=ccde591a93123786bd8d257abd970200", @@ -178,680 +122,7 @@ "currency_code": "USD" } }, - "line_items": [ - { - "id": 466157049, - "variant_id": 39072856, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008GREEN", - "variant_title": "green", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - green", - "variant_inventory_management": "shopify", - "properties": [ - { - "name": "Custom Engraving Front", - "value": "Happy Birthday" - }, - { - "name": "Custom Engraving Back", - "value": "Merry Christmas" - } - ], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.34", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.34", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.34", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/466157049", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - }, - { - "id": 518995019, - "variant_id": 49148385, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008RED", - "variant_title": "red", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - red", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.33", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.33", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.33", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/518995019", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - }, - { - "id": 703073504, - "variant_id": 457924702, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008BLACK", - "variant_title": "black", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - black", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.33", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.33", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.33", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/703073504", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - } - ], - "fulfillments": [ - { - "id": 255858046, - "order_id": 450789469, - "status": "failure", - "created_at": "2021-02-12T13:51:00-05:00", - "service": "manual", - "updated_at": "2021-02-12T13:51:00-05:00", - "tracking_company": "USPS", - "shipment_status": null, - "location_id": 905684977, - "line_items": [ - { - "id": 466157049, - "variant_id": 39072856, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008GREEN", - "variant_title": "green", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - green", - "variant_inventory_management": "shopify", - "properties": [ - { - "name": "Custom Engraving Front", - "value": "Happy Birthday" - }, - { - "name": "Custom Engraving Back", - "value": "Merry Christmas" - } - ], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.34", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.34", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.34", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/466157049", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - } - ], - "tracking_number": "1Z2345", - "tracking_numbers": [ - "1Z2345" - ], - "tracking_url": "https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345", - "tracking_urls": [ - "https://tools.usps.com/go/TrackConfirmAction_input?qtc_tLabels1=1Z2345" - ], - "receipt": { - "testcase": true, - "authorization": "123456" - }, - "name": "#1001.0", - "admin_graphql_api_id": "gid://shopify/Fulfillment/255858046" - } - ], - "refunds": [ - { - "id": 509562969, - "order_id": 450789469, - "created_at": "2021-02-12T13:51:00-05:00", - "note": "it broke during shipping", - "user_id": 799407056, - "processed_at": "2021-02-12T13:51:00-05:00", - "restock": true, - "duties": [], - "total_duties_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "admin_graphql_api_id": "gid://shopify/Refund/509562969", - "refund_line_items": [ - { - "id": 104689539, - "quantity": 1, - "line_item_id": 703073504, - "location_id": 487838322, - "restock_type": "legacy_restock", - "subtotal": 195.67, - "total_tax": 3.98, - "subtotal_set": { - "shop_money": { - "amount": "195.67", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "195.67", - "currency_code": "USD" - } - }, - "total_tax_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - }, - "line_item": { - "id": 703073504, - "variant_id": 457924702, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008BLACK", - "variant_title": "black", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - black", - "variant_inventory_management": "shopify", - "properties": [], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.33", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.33", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.33", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/703073504", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - } - }, - { - "id": 709875399, - "quantity": 1, - "line_item_id": 466157049, - "location_id": 487838322, - "restock_type": "legacy_restock", - "subtotal": 195.66, - "total_tax": 3.98, - "subtotal_set": { - "shop_money": { - "amount": "195.66", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "195.66", - "currency_code": "USD" - } - }, - "total_tax_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - }, - "line_item": { - "id": 466157049, - "variant_id": 39072856, - "title": "IPod Nano - 8gb", - "quantity": 1, - "sku": "IPOD2008GREEN", - "variant_title": "green", - "vendor": null, - "fulfillment_service": "manual", - "product_id": 632910392, - "requires_shipping": true, - "taxable": true, - "gift_card": false, - "name": "IPod Nano - 8gb - green", - "variant_inventory_management": "shopify", - "properties": [ - { - "name": "Custom Engraving Front", - "value": "Happy Birthday" - }, - { - "name": "Custom Engraving Back", - "value": "Merry Christmas" - } - ], - "product_exists": true, - "fulfillable_quantity": 1, - "grams": 200, - "price": "199.00", - "total_discount": "0.00", - "fulfillment_status": null, - "price_set": { - "shop_money": { - "amount": "199.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "199.00", - "currency_code": "USD" - } - }, - "total_discount_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [ - { - "amount": "3.34", - "discount_application_index": 0, - "amount_set": { - "shop_money": { - "amount": "3.34", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.34", - "currency_code": "USD" - } - } - } - ], - "duties": [], - "admin_graphql_api_id": "gid://shopify/LineItem/466157049", - "tax_lines": [ - { - "title": "State Tax", - "price": "3.98", - "rate": 0.06, - "price_set": { - "shop_money": { - "amount": "3.98", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "3.98", - "currency_code": "USD" - } - } - } - ] - } - } - ], - "transactions": [ - { - "id": 179259969, - "order_id": 450789469, - "kind": "refund", - "gateway": "bogus", - "status": "success", - "message": null, - "created_at": "2005-08-05T12:59:12-04:00", - "test": false, - "authorization": "authorization-key", - "location_id": null, - "user_id": null, - "parent_id": 801038806, - "processed_at": "2005-08-05T12:59:12-04:00", - "device_id": null, - "error_code": null, - "source_name": "web", - "receipt": {}, - "amount": "209.00", - "currency": "USD", - "admin_graphql_api_id": "gid://shopify/OrderTransaction/179259969" - } - ], - "order_adjustments": [] - } - ], - "total_tip_received": "0.0", - "original_total_duties_set": null, - "current_total_duties_set": null, "admin_graphql_api_id": "gid://shopify/Order/450789469", - "shipping_lines": [ - { - "id": 369256396, - "title": "Free Shipping", - "price": "0.00", - "code": "Free Shipping", - "source": "shopify", - "phone": null, - "requested_fulfillment_service_id": null, - "delivery_category": null, - "carrier_identifier": null, - "discounted_price": "0.00", - "price_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discounted_price_set": { - "shop_money": { - "amount": "0.00", - "currency_code": "USD" - }, - "presentment_money": { - "amount": "0.00", - "currency_code": "USD" - } - }, - "discount_allocations": [], - "tax_lines": [] - } - ], "billing_address": { "first_name": "Bob", "address1": "Chestnut Street 92", From 1642f2bb71b7819837113b460ab8952a28bf11c2 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 24 Feb 2021 18:32:04 -0500 Subject: [PATCH 144/259] Add black hook and run on all files --- .pre-commit-config.yaml | 5 ++ pyproject.toml | 4 + scripts/shopify_api.py | 12 ++- setup.py | 77 ++++++++-------- shopify/base.py | 38 +++----- shopify/collection.py | 6 +- shopify/limits.py | 1 + shopify/mixins.py | 3 - shopify/resources/api_permission.py | 1 - shopify/resources/application_charge.py | 1 - shopify/resources/asset.py | 8 +- shopify/resources/blog.py | 1 - shopify/resources/comment.py | 1 - shopify/resources/custom_collection.py | 1 - shopify/resources/customer.py | 1 - shopify/resources/customer_saved_search.py | 1 - shopify/resources/discount_code_creation.py | 14 +-- shopify/resources/fulfillment.py | 7 +- shopify/resources/fulfillment_event.py | 16 +++- shopify/resources/gift_card.py | 1 - shopify/resources/graphql.py | 8 +- shopify/resources/image.py | 4 +- shopify/resources/inventory_level.py | 11 ++- shopify/resources/location.py | 5 +- shopify/resources/order.py | 1 - shopify/resources/price_rule.py | 5 +- shopify/resources/product.py | 4 +- .../resources/recurring_application_charge.py | 1 - shopify/resources/refund.py | 9 +- shopify/resources/shop.py | 1 - shopify/resources/smart_collection.py | 1 - shopify/resources/user.py | 1 - shopify/resources/variant.py | 3 +- shopify/resources/webhook.py | 1 - shopify/session.py | 3 + shopify/yamlobjects.py | 2 + test/access_scope_test.py | 4 +- test/api_permission_test.py | 8 +- test/api_version_test.py | 13 +-- test/application_credit_test.py | 16 ++-- test/article_test.py | 17 ++-- test/asset_test.py | 62 +++++++++---- test/base_test.py | 7 +- test/blog_test.py | 10 ++- test/carrier_service_test.py | 8 +- test/cart_test.py | 1 - test/checkout_test.py | 1 - test/collection_listing_test.py | 9 +- test/collection_publication_test.py | 29 +++--- test/currency_test.py | 1 - test/customer_saved_search_test.py | 13 +-- test/customer_test.py | 28 ++++-- test/discount_code_test.py | 16 ++-- test/disputes_test.py | 3 +- test/draft_order_test.py | 76 ++++++++++++---- test/fulfillment_event_test.py | 28 ++++-- test/fulfillment_service_test.py | 8 +- test/fulfillment_test.py | 33 +++++-- test/gift_card_test.py | 43 ++++++--- test/graphql_test.py | 6 +- test/image_test.py | 40 ++++++--- test/inventory_item_test.py | 9 +- test/inventory_level_test.py | 23 +++-- test/limits_test.py | 31 +++---- test/location_test.py | 2 +- test/marketing_event_test.py | 45 ++++++---- test/order_risk_test.py | 17 ++-- test/order_test.py | 1 - test/pagination_test.py | 55 ++++++------ test/payouts_test.py | 3 +- test/price_rules_test.py | 89 +++++++++++-------- test/product_listing_test.py | 9 +- test/product_publication_test.py | 31 +++---- test/product_test.py | 43 ++++++--- test/recurring_charge_test.py | 24 +++-- test/refund_test.py | 3 +- test/report_test.py | 43 ++++----- test/resource_feedback_test.py | 5 +- test/session_test.py | 50 +++++++---- test/shop_test.py | 18 ++-- test/storefront_access_token_test.py | 18 ++-- test/test_helper.py | 24 ++--- test/transactions_test.py | 3 +- test/usage_charge_test.py | 20 +++-- test/user_test.py | 1 - test/variant_test.py | 25 ++++-- 86 files changed, 789 insertions(+), 542 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d424ed1e..5f8a792e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,3 +10,8 @@ repos: rev: v1.5.4 hooks: - id: autopep8 +- repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + args: [--line-length=120] diff --git a/pyproject.toml b/pyproject.toml index 7ff18c21..661cc7d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,6 @@ [tool.autopep8] max_line_length = 120 + +[tool.black] +line-length = 120 +skip-string-normalization = true diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index a3de00b6..0f6f0665 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -18,6 +18,7 @@ def start_interpreter(**variables): sys.path.append(os.getcwd()) console = type('shopify ' + shopify.version.VERSION, (code.InteractiveConsole, object), {}) import readline + console(variables).interact() @@ -27,9 +28,11 @@ class ConfigFileError(Exception): def usage(usage_string): """Decorator to add a usage string to a function""" + def decorate(func): func.usage = usage_string return func + return decorate @@ -44,6 +47,7 @@ def __new__(mcs, name, bases, new_attrs): def filter_func(item): return not item.startswith("_") and hasattr(getattr(cls, item), "__call__") + tasks = filter(filter_func, tasks) cls._tasks = sorted(tasks) @@ -84,7 +88,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) @@ -217,8 +221,10 @@ def version(cls, connection=None): @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): diff --git a/setup.py b/setup.py index f6bceabf..e1b7d816 100755 --- a/setup.py +++ b/setup.py @@ -10,40 +10,43 @@ 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.2', - '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', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - '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'], + scripts=['scripts/shopify_api.py'], + license='MIT License', + install_requires=[ + 'pyactiveresource>=2.2.2', + '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', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Topic :: Software Development', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) diff --git a/shopify/base.py b/shopify/base.py index f2d9eb06..328d2f1b 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -17,8 +17,7 @@ class ShopifyConnection(pyactiveresource.connection.Connection): response = None - def __init__(self, site, user=None, password=None, timeout=None, - format=formats.JSONFormat): + 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): @@ -30,11 +29,11 @@ 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""" @@ -51,8 +50,7 @@ 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): @@ -62,8 +60,7 @@ 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) @@ -72,8 +69,7 @@ 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) @@ -93,8 +89,7 @@ def set_site(cls, value): 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) @@ -103,8 +98,7 @@ 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'): @@ -114,8 +108,7 @@ def get_headers(cls): 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) @@ -124,8 +117,7 @@ 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.""" @@ -141,8 +133,7 @@ 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: @@ -153,8 +144,7 @@ def get_version(cls): 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) @@ -162,16 +152,14 @@ def get_url(cls): 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 diff --git a/shopify/collection.py b/shopify/collection.py index b233ace0..ee8ef1fc 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -56,13 +56,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): diff --git a/shopify/limits.py b/shopify/limits.py index 711e498d..79a6c2a4 100644 --- a/shopify/limits.py +++ b/shopify/limits.py @@ -8,6 +8,7 @@ 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' diff --git a/shopify/mixins.py b/shopify/mixins.py index 38840554..54496dbf 100644 --- a/shopify/mixins.py +++ b/shopify/mixins.py @@ -2,7 +2,6 @@ class Countable(object): - @classmethod def count(cls, _options=None, **kwargs): if _options is None: @@ -11,7 +10,6 @@ def count(cls, _options=None, **kwargs): class Metafields(object): - def metafields(self, _options=None, **kwargs): if _options is None: _options = kwargs @@ -32,6 +30,5 @@ def add_metafield(self, 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/api_permission.py b/shopify/resources/api_permission.py index 3b4fc016..08780fa7 100644 --- a/shopify/resources/api_permission.py +++ b/shopify/resources/api_permission.py @@ -2,7 +2,6 @@ class ApiPermission(ShopifyResource): - @classmethod def delete(cls): cls.connection.delete(cls.site + '/api_permissions/current.' + cls.format.extension, cls.headers) 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/asset.py b/shopify/resources/asset.py index 643e58bc..ef1840eb 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/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/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..a058037d 100644 --- a/shopify/resources/custom_collection.py +++ b/shopify/resources/custom_collection.py @@ -4,7 +4,6 @@ class CustomCollection(ShopifyResource, mixins.Metafields, mixins.Events): - def products(self): return shopify.Product.find(collection_id=self.id) diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py index 80be7fc5..772da94c 100644 --- a/shopify/resources/customer.py +++ b/shopify/resources/customer.py @@ -4,7 +4,6 @@ class Customer(ShopifyResource, mixins.Metafields): - @classmethod def search(cls, **kwargs): """ 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_creation.py b/shopify/resources/discount_code_creation.py index 99c919e9..43def613 100644 --- a/shopify/resources/discount_code_creation.py +++ b/shopify/resources/discount_code_creation.py @@ -6,8 +6,12 @@ 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/fulfillment.py b/shopify/resources/fulfillment.py index 81a79b74..8bdf8c83 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -29,10 +29,5 @@ class FulfillmentV2(ShopifyResource): _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 6341d2b9..5982a383 100644 --- a/shopify/resources/fulfillment_event.py +++ b/shopify/resources/fulfillment_event.py @@ -12,11 +12,21 @@ def _prefix(cls, options={}): 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']: + 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/graphql.py b/shopify/resources/graphql.py index 3bd73f34..a22edc5c 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -4,10 +4,9 @@ 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): @@ -20,8 +19,7 @@ def execute(self, query, variables=None): endpoint = self.endpoint default_headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} headers = self.merge_headers(default_headers, self.headers) - data = {'query': query, - 'variables': variables} + data = {'query': query, 'variables': variables} req = urllib.request.Request(self.endpoint, json.dumps(data).encode('utf-8'), headers) diff --git a/shopify/resources/image.py b/shopify/resources/image.py index e5827e06..894f2fe1 100644 --- a/shopify/resources/image.py +++ b/shopify/resources/image.py @@ -31,7 +31,9 @@ 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))) + 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: diff --git a/shopify/resources/inventory_level.py b/shopify/resources/inventory_level.py index 45c21b80..ff84859a 100644 --- a/shopify/resources/inventory_level.py +++ b/shopify/resources/inventory_level.py @@ -4,7 +4,6 @@ class InventoryLevel(ShopifyResource): - def __repr__(self): return '%s(inventory_item_id=%s, location_id=%s)' % (self._singular, self.inventory_item_id, self.location_id) @@ -13,15 +12,19 @@ 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 + 'available_adjustment': available_adjustment, } resource = cls.post('adjust', body=json.dumps(body).encode()) return InventoryLevel(InventoryLevel.format.decode(resource.body)) diff --git a/shopify/resources/location.py b/shopify/resources/location.py index 31e828cd..51e7ecdd 100644 --- a/shopify/resources/location.py +++ b/shopify/resources/location.py @@ -4,5 +4,6 @@ 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/order.py b/shopify/resources/order.py index 4e780b0a..73ee506a 100644 --- a/shopify/resources/order.py +++ b/shopify/resources/order.py @@ -4,7 +4,6 @@ class Order(ShopifyResource, mixins.Metafields, mixins.Events): - def close(self): self._load_attributes_from_response(self.post("close")) diff --git a/shopify/resources/price_rule.py b/shopify/resources/price_rule.py index b75943cd..f6090c13 100644 --- a/shopify/resources/price_rule.py +++ b/shopify/resources/price_rule.py @@ -19,5 +19,6 @@ def create_batch(self, codes=[]): 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..a9b4b5a1 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" @@ -34,8 +33,7 @@ def add_variant(self, variant): def save(self): start_api_version = '201910' api_version = ShopifyResource.version - if api_version and ( - api_version.strip('-') >= start_api_version) and api_version != 'unstable': + if 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: diff --git a/shopify/resources/recurring_application_charge.py b/shopify/resources/recurring_application_charge.py index 28cf7771..c94cac2d 100644 --- a/shopify/resources/recurring_application_charge.py +++ b/shopify/resources/recurring_application_charge.py @@ -10,7 +10,6 @@ def _get_first_by_status(resources, status): class RecurringApplicationCharge(ShopifyResource): - def usage_charges(self): return UsageCharge.find(recurring_application_charge_id=self.id) diff --git a/shopify/resources/refund.py b/shopify/resources/refund.py index 6a62b513..e8c439d2 100644 --- a/shopify/resources/refund.py +++ b/shopify/resources/refund.py @@ -25,10 +25,5 @@ def calculate(cls, order_id, shipping=None, refund_line_items=None): 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} - ) + 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/user.py b/shopify/resources/user.py index 1b2a21f6..f9b14b54 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')) diff --git a/shopify/resources/variant.py b/shopify/resources/variant.py index c0a38e6c..ba6e8089 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -19,8 +19,7 @@ def save(self): start_api_version = '201910' api_version = ShopifyResource.version - if api_version and ( - api_version.strip('-') >= start_api_version) and api_version != 'unstable': + if 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: 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 e6a607b9..4a6a2d85 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: @@ -33,6 +34,7 @@ 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_version = shopify.ShopifyResource.get_version() or version @@ -145,6 +147,7 @@ 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': diff --git a/shopify/yamlobjects.py b/shopify/yamlobjects.py index d4e7cd3a..b3aedac9 100644 --- a/shopify/yamlobjects.py +++ b/shopify/yamlobjects.py @@ -14,5 +14,7 @@ class YAMLHashWithIndifferentAccess(yaml.YAMLObject): @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 9aff6cce..e8d67491 100644 --- a/test/access_scope_test.py +++ b/test/access_scope_test.py @@ -3,10 +3,8 @@ 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') + 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_permission_test.py b/test/api_permission_test.py index 0104097f..d63eec25 100644 --- a/test/api_permission_test.py +++ b/test/api_permission_test.py @@ -3,13 +3,7 @@ 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 18b46ec8..59cbe7b4 100644 --- a/test/api_version_test.py +++ b/test/api_version_test.py @@ -12,8 +12,10 @@ 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() @@ -29,14 +31,15 @@ def test_coerce_to_version_raises_with_string_that_does_not_match_known_version( class ReleaseTest(TestCase): - def test_raises_if_format_invalid(self): with self.assertRaises(shopify.InvalidVersionError): 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') diff --git a/test/application_credit_test.py b/test/application_credit_test.py index 81c2c18f..567edbdc 100644 --- a/test/application_credit_test.py +++ b/test/application_credit_test.py @@ -21,19 +21,13 @@ def test_create_application_credit(self): method='POST', body=self.load_fixture('application_credit'), headers={'Content-type': 'application/json'}, - code=201 + code=201, ) - application_credit = shopify.ApplicationCredit.create({ - 'description': 'application credit for refund', - 'amount': 5.0 - }) + 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 - } - } + 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 a1a42b3b..9a3e9f11 100644 --- a/test/article_test.py +++ b/test/article_test.py @@ -3,10 +3,13 @@ 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'}) + 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) @@ -20,8 +23,12 @@ def test_update_article(self): 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): diff --git a/test/asset_test.py b/test/asset_test.py index de2c14f6..3435b317 100644 --- a/test/asset_test.py +++ b/test/asset_test.py @@ -5,19 +5,26 @@ class AssetTest(TestCase): - def test_get_assets(self): 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')) + 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')) + 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'}) @@ -28,31 +35,52 @@ def test_get_assets_namespaced(self): 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')) + 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')) + 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')) + 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'}) + 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 diff --git a/test/base_test.py b/test/base_test.py index 039cfc5b..ee382741 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -6,7 +6,6 @@ class BaseTest(TestCase): - @classmethod def setUpClass(self): shopify.ApiVersion.define_known_versions() @@ -54,7 +53,9 @@ def test_clear_session_should_clear_site_and_headers_from_Base(self): 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) @@ -106,7 +107,7 @@ def test_setting_with_user_and_pass_strips_them(self): url='https://this-is-my-test-show.myshopify.com/admin/shop.json', method='GET', body=self.load_fixture('shop'), - headers={'Authorization': u'Basic dXNlcjpwYXNz'} + headers={'Authorization': u'Basic dXNlcjpwYXNz'}, ) API_KEY = 'user' PASSWORD = 'pass' diff --git a/test/blog_test.py b/test/blog_test.py index f88fcaf7..39062d00 100644 --- a/test/blog_test.py +++ b/test/blog_test.py @@ -3,9 +3,13 @@ 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'}) + 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 a317269e..20688bac 100644 --- a/test/carrier_service_test.py +++ b/test/carrier_service_test.py @@ -4,8 +4,12 @@ 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"}) self.assertEqual("Some Postal Service", carrier_service.name) diff --git a/test/cart_test.py b/test/cart_test.py index 8936f828..cd1b4401 100644 --- a/test/cart_test.py +++ b/test/cart_test.py @@ -3,7 +3,6 @@ class CartTest(TestCase): - def test_all_should_return_all_carts(self): self.fake('carts') carts = shopify.Cart.find() diff --git a/test/checkout_test.py b/test/checkout_test.py index 55549b4f..34be1bc5 100644 --- a/test/checkout_test.py +++ b/test/checkout_test.py @@ -3,7 +3,6 @@ class CheckoutTest(TestCase): - def test_all_should_return_all_checkouts(self): self.fake('checkouts') checkouts = shopify.Checkout.find() diff --git a/test/collection_listing_test.py b/test/collection_listing_test.py index efb62708..d6061f7d 100644 --- a/test/collection_listing_test.py +++ b/test/collection_listing_test.py @@ -3,7 +3,6 @@ class CollectionListingTest(TestCase): - def test_get_collection_listings(self): self.fake('collection_listings', method='GET', code=200, body=self.load_fixture('collection_listings')) @@ -31,8 +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 8cc61fcd..15b18497 100644 --- a/test/collection_publication_test.py +++ b/test/collection_publication_test.py @@ -8,7 +8,7 @@ def test_find_all_collection_publications(self): self.fake( 'publications/55650051/collection_publications', method='GET', - body=self.load_fixture('collection_publications') + body=self.load_fixture('collection_publications'), ) collection_publications = shopify.CollectionPublication.find(publication_id=55650051) @@ -20,7 +20,7 @@ def test_find_collection_publication(self): 'publications/55650051/collection_publications/96062799894', method='GET', body=self.load_fixture('collection_publication'), - code=200 + code=200, ) collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051) @@ -33,15 +33,17 @@ def test_create_collection_publication(self): method='POST', headers={'Content-type': 'application/json'}, body=self.load_fixture('collection_publication'), - code=201 + 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': { @@ -58,16 +60,11 @@ def test_destroy_collection_publication(self): 'publications/55650051/collection_publications/96062799894', method='GET', body=self.load_fixture('collection_publication'), - code=200 + 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()) diff --git a/test/currency_test.py b/test/currency_test.py index 4982c06f..a0043ab2 100644 --- a/test/currency_test.py +++ b/test/currency_test.py @@ -3,7 +3,6 @@ class CurrencyTest(TestCase): - def test_get_currencies(self): self.fake('currencies', method='GET', code=200, body=self.load_fixture('currencies')) diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py index 207da549..52baa02f 100644 --- a/test/customer_saved_search_test.py +++ b/test/customer_saved_search_test.py @@ -3,20 +3,23 @@ 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')) + 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) diff --git a/test/customer_test.py b/test/customer_test.py index 5819f292..4045b767 100644 --- a/test/customer_test.py +++ b/test/customer_test.py @@ -10,8 +10,9 @@ def setUp(self): 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' @@ -28,8 +29,11 @@ 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) @@ -37,8 +41,12 @@ def test_search(self): def test_send_invite_with_no_params(self): 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) @@ -47,8 +55,12 @@ def test_send_invite_with_no_params(self): def test_send_invite_with_params(self): 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(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) diff --git a/test/discount_code_test.py b/test/discount_code_test.py index ea93b38b..afacab73 100644 --- a/test/discount_code_test.py +++ b/test/discount_code_test.py @@ -15,15 +15,15 @@ def test_find_a_specific_discount_code(self): 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.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"] - ) + 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') diff --git a/test/disputes_test.py b/test/disputes_test.py index 7484b304..47983c7d 100644 --- a/test/disputes_test.py +++ b/test/disputes_test.py @@ -11,7 +11,6 @@ def test_get_dispute(self): 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) diff --git a/test/draft_order_test.py b/test/draft_order_test.py index b77c0fe8..f1305d32 100644 --- a/test/draft_order_test.py +++ b/test/draft_order_test.py @@ -26,30 +26,51 @@ def test_get_count_draft_orders(self): 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'}) + 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.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'}) + 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.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']) def test_send_invoice_with_no_params(self): 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) @@ -58,10 +79,15 @@ def test_send_invoice_with_no_params(self): def test_send_invoice_with_params(self): 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( - shopify.DraftOrderInvoice(draft_order_invoice['draft_order_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) @@ -72,12 +98,24 @@ def test_delete_draft_order(self): 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.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) diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index cf8d52f8..143e386a 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -4,25 +4,37 @@ 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'}) + 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'}) + {'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')) + 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') + 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 5313be7a..dfe5757d 100644 --- a/test/fulfillment_service_test.py +++ b/test/fulfillment_service_test.py @@ -4,8 +4,12 @@ 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"}) self.assertEqual("SomeService", fulfillment_service.name) diff --git a/test/fulfillment_test.py b/test/fulfillment_test.py index 312ee7d8..deba5f65 100644 --- a/test/fulfillment_test.py +++ b/test/fulfillment_test.py @@ -4,7 +4,6 @@ class FulFillmentTest(TestCase): - def setUp(self): super(FulFillmentTest, self).setUp() self.fake("orders/450789469/fulfillments/255858046", method='GET', body=self.load_fixture('fulfillment')) @@ -14,8 +13,12 @@ def test_able_to_open_fulfillment(self): 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.fake( + "orders/450789469/fulfillments/255858046/open", + method='POST', + headers={'Content-length': '0', 'Content-type': 'application/json'}, + body=success, + ) self.assertEqual('pending', fulfillment.status) fulfillment.open() @@ -26,8 +29,12 @@ def test_able_to_complete_fulfillment(self): 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.fake( + "orders/450789469/fulfillments/255858046/complete", + method='POST', + headers={'Content-length': '0', 'Content-type': 'application/json'}, + body=success, + ) self.assertEqual('pending', fulfillment.status) fulfillment.complete() @@ -38,8 +45,12 @@ def test_able_to_cancel_fulfillment(self): 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.fake( + "orders/450789469/fulfillments/255858046/cancel", + method='POST', + headers={'Content-length': '0', 'Content-type': 'application/json'}, + body=cancelled, + ) self.assertEqual('pending', fulfillment.status) fulfillment.cancel() @@ -56,8 +67,12 @@ def test_update_tracking(self): 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) diff --git a/test/gift_card_test.py b/test/gift_card_test.py index 4897b0c7..bb18fb39 100644 --- a/test/gift_card_test.py +++ b/test/gift_card_test.py @@ -4,10 +4,14 @@ 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'}) + 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) @@ -19,8 +23,13 @@ def test_fetch_gift_cards(self): 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/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() @@ -28,19 +37,29 @@ def test_disable_gift_card(self): 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/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") diff --git a/test/graphql_test.py b/test/graphql_test.py index 5075bdfe..dc43c56f 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -4,7 +4,6 @@ class GraphQLTest(TestCase): - def setUp(self): super(GraphQLTest, self).setUp() shopify.ApiVersion.define_known_versions() @@ -18,8 +17,9 @@ def setUp(self): headers={ 'X-Shopify-Access-Token': 'token', 'Accept': 'application/json', - 'Content-Type': 'application/json' - }) + 'Content-Type': 'application/json', + }, + ) query = ''' { shop { diff --git a/test/image_test.py b/test/image_test.py index 308dd6c1..b4879b35 100644 --- a/test/image_test.py +++ b/test/image_test.py @@ -4,43 +4,61 @@ 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'}) + 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'}) + 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==") + "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): diff --git a/test/inventory_item_test.py b/test/inventory_item_test.py index d2d12ab7..63de7e97 100644 --- a/test/inventory_item_test.py +++ b/test/inventory_item_test.py @@ -3,13 +3,8 @@ 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") @@ -18,7 +13,7 @@ def test_fetch_inventory_item_ids(self): 'inventory_items.json?ids=123456789%2C234567891', extension='', method='GET', - body=self.load_fixture('inventory_items') + body=self.load_fixture('inventory_items'), ) 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 51c69d95..7ad4d93f 100644 --- a/test/inventory_level_test.py +++ b/test/inventory_level_test.py @@ -5,7 +5,6 @@ class InventoryLevelTest(TestCase): - def test_fetch_inventory_level(self): params = {'inventory_item_ids': [808950810, 39072856], 'location_ids': [905684977, 487838322]} @@ -13,15 +12,16 @@ def test_fetch_inventory_level(self): 'inventory_levels.json?location_ids=905684977%2C487838322&inventory_item_ids=808950810%2C39072856', method='GET', extension='', - body=self.load_fixture('inventory_levels') + 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): @@ -29,7 +29,7 @@ def test_inventory_level_adjust(self): 'inventory_levels/adjust', method='POST', body=self.load_fixture('inventory_level'), - headers={'Content-type': 'application/json'} + headers={'Content-type': 'application/json'}, ) inventory_level = shopify.InventoryLevel.adjust(905684977, 808950810, 5) self.assertEqual(inventory_level.available, 6) @@ -40,7 +40,7 @@ def test_inventory_level_connect(self): method='POST', body=self.load_fixture('inventory_level'), headers={'Content-type': 'application/json'}, - code=201 + code=201, ) inventory_level = shopify.InventoryLevel.connect(905684977, 808950810) self.assertEqual(inventory_level.available, 6) @@ -59,10 +59,9 @@ 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']) - 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='{}') diff --git a/test/limits_test.py b/test/limits_test.py index 5651c902..2a96dbd5 100644 --- a/test/limits_test.py +++ b/test/limits_test.py @@ -9,6 +9,7 @@ class LimitsTest(TestCase): Conversion of test/limits_test.rb """ + @classmethod def setUpClass(self): self.original_headers = None @@ -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 f93dbb96..b0165b8e 100644 --- a/test/location_test.py +++ b/test/location_test.py @@ -22,7 +22,7 @@ def test_inventory_levels_returns_all_inventory_levels(self): "locations/%s/inventory_levels" % location.id, method='GET', code=200, - body=self.load_fixture('location_inventory_levels') + body=self.load_fixture('location_inventory_levels'), ) inventory_levels = location.inventory_levels() diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py index 3ee079c9..72763934 100644 --- a/test/marketing_event_test.py +++ b/test/marketing_event_test.py @@ -18,8 +18,12 @@ def test_get_marketing_events(self): 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' @@ -42,8 +46,13 @@ def test_delete_marketing_event(self): 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='PUT', + code=200, + body=self.load_fixture('marketing_event'), + headers={'Content-type': 'application/json'}, + ) marketing_event = shopify.MarketingEvent.find(1) marketing_event.currency = 'USD' @@ -62,21 +71,25 @@ def test_add_engagements(self): method='POST', code=201, body=self.load_fixture('engagement'), - headers={'Content-type': 'application/json'} + 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 - }]) + 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) diff --git a/test/order_risk_test.py b/test/order_risk_test.py index 83dc5afa..7c26c16a 100644 --- a/test/order_risk_test.py +++ b/test/order_risk_test.py @@ -3,10 +3,13 @@ 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'}) + 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" @@ -37,8 +40,12 @@ def test_delete_order_risk(self): 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'}) + 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 diff --git a/test/order_test.py b/test/order_test.py index 46278a72..a761ebdf 100644 --- a/test/order_test.py +++ b/test/order_test.py @@ -5,7 +5,6 @@ class OrderTest(TestCase): - def test_should_be_loaded_correctly_from_order_xml(self): order_xml = """ diff --git a/test/pagination_test.py b/test/pagination_test.py index 43681657..e06715d5 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -4,7 +4,6 @@ class PaginationTest(TestCase): - def setUp(self): super(PaginationTest, self).setUp() prefix = self.http.site + "/admin/api/unstable" @@ -16,26 +15,33 @@ def setUp(self): 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) + 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')) 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) @@ -47,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() @@ -67,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..255df766 100644 --- a/test/payouts_test.py +++ b/test/payouts_test.py @@ -11,8 +11,7 @@ def test_get_payouts(self): 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) diff --git a/test/price_rules_test.py b/test/price_rules_test.py index aef98bef..16a58426 100644 --- a/test/price_rules_test.py +++ b/test/price_rules_test.py @@ -5,7 +5,6 @@ class PriceRuleTest(TestCase): - def setUp(self): super(PriceRuleTest, self).setUp() self.fake('price_rules/1213131', body=self.load_fixture('price_rule')) @@ -17,17 +16,19 @@ def test_get_price_rule(self): 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']) @@ -37,59 +38,71 @@ def test_delete_price_rule(self): 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') 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'}) + 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'])) + 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) 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'}) + 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) 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) diff --git a/test/product_listing_test.py b/test/product_listing_test.py index e67fb3fd..9c078586 100644 --- a/test/product_listing_test.py +++ b/test/product_listing_test.py @@ -3,7 +3,6 @@ class ProductListingTest(TestCase): - def test_get_product_listings(self): self.fake('product_listings', method='GET', code=200, body=self.load_fixture('product_listings')) @@ -30,8 +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 2a76cd13..015d8dd1 100644 --- a/test/product_publication_test.py +++ b/test/product_publication_test.py @@ -6,9 +6,7 @@ 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) @@ -20,7 +18,7 @@ def test_find_product_publication(self): 'publications/55650051/product_publications/647162527768', method='GET', body=self.load_fixture('product_publication'), - code=200 + code=200, ) product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051) @@ -33,15 +31,17 @@ def test_create_product_publication(self): method='POST', headers={'Content-type': 'application/json'}, body=self.load_fixture('product_publication'), - code=201 + 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': { @@ -58,16 +58,11 @@ def test_destroy_product_publication(self): 'publications/55650051/product_publications/647162527768', method='GET', body=self.load_fixture('product_publication'), - code=200 + 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()) diff --git a/test/product_test.py b/test/product_test.py index b7ea1f1a..32c23e0d 100644 --- a/test/product_test.py +++ b/test/product_test.py @@ -3,7 +3,6 @@ class ProductTest(TestCase): - def setUp(self): super(ProductTest, self).setUp() @@ -11,11 +10,19 @@ def setUp(self): 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) @@ -46,8 +53,11 @@ def test_get_metafields_for_product_count(self): 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) @@ -60,9 +70,18 @@ def test_update_loaded_variant(self): 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/recurring_charge_test.py b/test/recurring_charge_test.py index 153a9ea6..e7826372 100644 --- a/test/recurring_charge_test.py +++ b/test/recurring_charge_test.py @@ -5,8 +5,12 @@ 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=" ") + 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() @@ -28,8 +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) @@ -38,8 +45,13 @@ 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')) + 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) diff --git a/test/refund_test.py b/test/refund_test.py index 300d888f..8ca9622e 100644 --- a/test/refund_test.py +++ b/test/refund_test.py @@ -20,8 +20,7 @@ def test_calculate_refund_for_order(self): 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 6768a599..4c060571 100644 --- a/test/report_test.py +++ b/test/report_test.py @@ -3,43 +3,34 @@ 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) 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.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 49e040d9..ce8542cf 100644 --- a/test/resource_feedback_test.py +++ b/test/resource_feedback_test.py @@ -30,8 +30,9 @@ def test_get_resource_feedback_with_product_id(self): 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'}) + self.fake( + 'products/42/resource_feedback', method='POST', body=body, headers={'Content-Type': 'application/json'} + ) feedback = shopify.ResourceFeedback({'product_id': 42}) feedback.save() diff --git a/test/session_test.py b/test/session_test.py index f45933b5..d7eb0bbd 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -8,7 +8,6 @@ class SessionTest(TestCase): - @classmethod def setUpClass(self): shopify.ApiVersion.define_known_versions() @@ -92,46 +91,62 @@ def test_create_permission_url_returns_correct_url_with_single_scope_and_redirec 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)) + 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): 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)) + 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): shopify.Session.setup(api_key="My_test_key", secret="My test secret") 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)) + 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), + ) def test_create_permission_url_returns_correct_url_with_no_scope_and_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') 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)) + 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), + ) def test_create_permission_url_returns_correct_url_with_single_scope_and_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') 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)) + 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) + 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'}) @@ -199,8 +214,13 @@ def test_return_token_if_hmac_is_valid(self): 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) + 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') token = session.request_token(params) self.assertEqual("token", token) diff --git a/test/shop_test.py b/test/shop_test.py index 140e90da..85346849 100644 --- a/test/shop_test.py +++ b/test/shop_test.py @@ -26,11 +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 d8a2edad..8e223dc2 100644 --- a/test/storefront_access_token_test.py +++ b/test/storefront_access_token_test.py @@ -4,15 +4,20 @@ 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'}) + 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') @@ -20,8 +25,9 @@ def test_get_and_delete_storefront_access_token(self): 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/test_helper.py b/test/test_helper.py index 26213c1f..2abaa606 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -7,7 +7,6 @@ class TestCase(unittest.TestCase): - def setUp(self): ActiveResource.site = None ActiveResource.headers = None @@ -21,14 +20,15 @@ def setUp(self): 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.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, + ) def load_fixture(self, name, format='json'): with open(os.path.dirname(__file__) + '/fixtures/%s.%s' % (name, format), 'rb') as f: @@ -40,7 +40,7 @@ def fake(self, endpoint, **kwargs): 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')) @@ -61,5 +61,5 @@ def fake(self, endpoint, **kwargs): 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/transactions_test.py b/test/transactions_test.py index 62044238..7936842a 100644 --- a/test/transactions_test.py +++ b/test/transactions_test.py @@ -6,7 +6,6 @@ class TransactionsTest(TestCase): 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 47cca7bb..13f3ac16 100644 --- a/test/usage_charge_test.py +++ b/test/usage_charge_test.py @@ -4,17 +4,25 @@ 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) 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) diff --git a/test/user_test.py b/test/user_test.py index 35caac26..3f0e2385 100644 --- a/test/user_test.py +++ b/test/user_test.py @@ -3,7 +3,6 @@ class UserTest(TestCase): - def test_get_all_users(self): self.fake('users', body=self.load_fixture('users')) users = shopify.User.find() diff --git a/test/variant_test.py b/test/variant_test.py index 7474c2dd..0d8d1967 100644 --- a/test/variant_test.py +++ b/test/variant_test.py @@ -3,7 +3,6 @@ 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) @@ -16,19 +15,31 @@ 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='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'}) + 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() From 8f68c09cdb0542fb8339e201ffb98e4a2d078ba9 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 24 Feb 2021 18:49:48 -0500 Subject: [PATCH 145/259] Remove black args that are duplicates from pyproject.toml --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f8a792e..66662ec0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,4 +14,3 @@ repos: rev: 20.8b1 hooks: - id: black - args: [--line-length=120] From 006e3e645f5d58ddf75e140ef2d4c42e65934a7d Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Fri, 26 Feb 2021 10:49:45 -0500 Subject: [PATCH 146/259] Add optional customer.orders() support with tests --- shopify/resources/customer.py | 4 ++++ test/customer_test.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/shopify/resources/customer.py b/shopify/resources/customer.py index 80be7fc5..e52c8d80 100644 --- a/shopify/resources/customer.py +++ b/shopify/resources/customer.py @@ -1,6 +1,7 @@ from ..base import ShopifyResource from shopify import mixins from .customer_invite import CustomerInvite +from .order import Order class Customer(ShopifyResource, mixins.Metafields): @@ -24,3 +25,6 @@ def search(cls, **kwargs): def send_invite(self, customer_invite=CustomerInvite()): resource = self.post("send_invite", customer_invite.encode()) return CustomerInvite(Customer.format.decode(resource.body)) + + def orders(self): + return Order.find(customer_id=self.id) diff --git a/test/customer_test.py b/test/customer_test.py index 5819f292..9547b835 100644 --- a/test/customer_test.py +++ b/test/customer_test.py @@ -53,3 +53,12 @@ def test_send_invite_with_params(self): 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) + + 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) From 7aa4b4b4413734fa1d756fc23fdf1804cae2601b Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 1 Mar 2021 15:14:51 -0500 Subject: [PATCH 147/259] Run black to format all quotes --- bin/shopify_api.py | 4 +- pyproject.toml | 1 - scripts/shopify_api.py | 20 +-- setup.py | 62 ++++----- shopify/api_version.py | 22 +-- shopify/base.py | 46 +++---- shopify/collection.py | 6 +- shopify/limits.py | 6 +- shopify/resources/api_permission.py | 2 +- shopify/resources/article.py | 4 +- shopify/resources/asset.py | 2 +- shopify/resources/balance.py | 2 +- shopify/resources/collection_listing.py | 2 +- shopify/resources/custom_collection.py | 2 +- shopify/resources/discount_code_creation.py | 2 +- shopify/resources/draft_order.py | 4 +- shopify/resources/fulfillment.py | 4 +- shopify/resources/fulfillment_event.py | 28 ++-- shopify/resources/gift_card_adjustment.py | 4 +- shopify/resources/graphql.py | 10 +- shopify/resources/image.py | 8 +- shopify/resources/inventory_level.py | 30 ++-- shopify/resources/marketing_event.py | 4 +- shopify/resources/price_rule.py | 2 +- shopify/resources/product.py | 16 +-- shopify/resources/product_listing.py | 2 +- shopify/resources/refund.py | 8 +- shopify/resources/user.py | 2 +- shopify/resources/variant.py | 16 +-- shopify/session.py | 32 ++--- shopify/version.py | 2 +- shopify/yamlobjects.py | 2 +- test/access_scope_test.py | 4 +- test/api_permission_test.py | 2 +- test/api_version_test.py | 22 +-- test/application_credit_test.py | 16 +-- test/article_test.py | 36 ++--- test/asset_test.py | 58 ++++---- test/balance_test.py | 4 +- test/base_test.py | 66 ++++----- test/blog_test.py | 10 +- test/carrier_service_test.py | 12 +- test/cart_test.py | 4 +- test/checkout_test.py | 2 +- test/collection_listing_test.py | 12 +- test/collection_publication_test.py | 46 +++---- test/currency_test.py | 2 +- test/customer_saved_search_test.py | 8 +- test/customer_test.py | 46 +++---- test/discount_code_creation_test.py | 10 +- test/discount_code_test.py | 18 +-- test/disputes_test.py | 8 +- test/draft_order_test.py | 108 +++++++-------- test/event_test.py | 2 +- test/fulfillment_event_test.py | 22 +-- test/fulfillment_service_test.py | 12 +- test/fulfillment_test.py | 52 +++---- test/gift_card_test.py | 38 +++--- test/graphql_test.py | 18 +-- test/image_test.py | 40 +++--- test/inventory_item_test.py | 12 +- test/inventory_level_test.py | 46 +++---- test/limits_test.py | 14 +- test/location_test.py | 10 +- test/marketing_event_test.py | 80 +++++------ test/order_risk_test.py | 24 ++-- test/order_test.py | 12 +- test/pagination_test.py | 8 +- test/payouts_test.py | 10 +- test/price_rules_test.py | 66 ++++----- test/product_listing_test.py | 12 +- test/product_publication_test.py | 42 +++--- test/product_test.py | 32 ++--- test/publication_test.py | 2 +- test/recurring_charge_test.py | 20 +-- test/refund_test.py | 8 +- test/report_test.py | 20 +-- test/resource_feedback_test.py | 22 +-- test/session_test.py | 144 ++++++++++---------- test/shipping_zone_test.py | 2 +- test/shop_test.py | 8 +- test/storefront_access_token_test.py | 18 +-- test/tender_transaction_test.py | 2 +- test/test_helper.py | 46 +++---- test/transaction_test.py | 2 +- test/transactions_test.py | 4 +- test/usage_charge_test.py | 16 +-- test/user_test.py | 6 +- test/variant_test.py | 28 ++-- 89 files changed, 875 insertions(+), 876 deletions(-) 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/pyproject.toml b/pyproject.toml index 661cc7d4..02c844ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,3 @@ max_line_length = 120 [tool.black] line-length = 120 -skip-string-normalization = true diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 0f6f0665..3fd63109 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -16,7 +16,7 @@ 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), {}) + console = type("shopify " + shopify.version.VERSION, (code.InteractiveConsole, object), {}) import readline console(variables).interact() @@ -54,7 +54,7 @@ def filter_func(item): return cls def run_task(cls, task=None, *args): - if task in [None, '-h', '--help']: + if task in [None, "-h", "--help"]: cls.help() return @@ -120,22 +120,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 = dict(protocol="https") domain = input("Domain? (leave blank for %s.myshopify.com) " % (connection)) if not domain.strip(): domain = "%s.myshopify.com" % (connection) - config['domain'] = domain + 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) diff --git a/setup.py b/setup.py index e1b7d816..84ec9c24 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from setuptools import setup -NAME = 'ShopifyAPI' -exec(open('shopify/version.py').read()) -DESCRIPTION = 'Shopify API for Python' +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 @@ -15,38 +15,38 @@ 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', + 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.2', - 'PyYAML', - 'six', + "pyactiveresource>=2.2.2", + "PyYAML", + "six", ], - test_suite='test', + test_suite="test", tests_require=[ - 'mock>=1.0.1', + "mock>=1.0.1", ], - platforms='Any', + 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', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', + "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", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", ], ) diff --git a/shopify/api_version.py b/shopify/api_version.py index be8bf887..44a8e9b6 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -30,13 +30,13 @@ def define_version(cls, version): 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': + 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(Release(release["handle"])) @classmethod def clear_defined_versions(cls): @@ -60,15 +60,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): @@ -77,9 +77,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 328d2f1b..47f40cb3 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -38,7 +38,7 @@ class ShopifyResourceMeta(ResourceMeta): 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 @@ -54,7 +54,7 @@ def connection(cls): 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 @@ -63,7 +63,7 @@ def set_user(cls, value): user = property(get_user, set_user, None, "The username for HTTP Basic Auth.") def get_password(cls): - return getattr(cls._threadlocal, 'password', ShopifyResource._password) + return getattr(cls._threadlocal, "password", ShopifyResource._password) def set_password(cls, value): cls._threadlocal.connection = None @@ -72,7 +72,7 @@ def set_password(cls, value): password = property(get_password, set_password, None, "The password for HTTP Basic Auth.") def get_site(cls): - return getattr(cls._threadlocal, 'site', ShopifyResource._site) + return getattr(cls._threadlocal, "site", ShopifyResource._site) def set_site(cls, value): cls._threadlocal.connection = None @@ -82,49 +82,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 @@ -133,33 +133,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 @@ -182,7 +182,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): @@ -191,7 +191,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 ee8ef1fc..42bc8ebb 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -30,11 +30,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 diff --git a/shopify/limits.py b/shopify/limits.py index 79a6c2a4..0246c793 100644 --- a/shopify/limits.py +++ b/shopify/limits.py @@ -11,7 +11,7 @@ class Limits(object): # 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): @@ -22,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/resources/api_permission.py b/shopify/resources/api_permission.py index 08780fa7..1c936451 100644 --- a/shopify/resources/api_permission.py +++ b/shopify/resources/api_permission.py @@ -4,6 +4,6 @@ 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/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 ef1840eb..d5156a5a 100644 --- a/shopify/resources/asset.py +++ b/shopify/resources/asset.py @@ -19,7 +19,7 @@ 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._prefix(prefix_options) + "/", cls.plural, cls.format.extension, cls._query_string(query_options), 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/collection_listing.py b/shopify/resources/collection_listing.py index 497dc041..00567489 100644 --- a/shopify/resources/collection_listing.py +++ b/shopify/resources/collection_listing.py @@ -5,4 +5,4 @@ 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/custom_collection.py b/shopify/resources/custom_collection.py index a058037d..85bcbc4a 100644 --- a/shopify/resources/custom_collection.py +++ b/shopify/resources/custom_collection.py @@ -8,7 +8,7 @@ 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/discount_code_creation.py b/shopify/resources/discount_code_creation.py index 43def613..e72de283 100644 --- a/shopify/resources/discount_code_creation.py +++ b/shopify/resources/discount_code_creation.py @@ -10,7 +10,7 @@ def discount_codes(self): from_="%s/price_rules/%s/batch/%s/discount_codes.%s" % ( ShopifyResource.site, - self._prefix_options['price_rule_id'], + 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 a8aa7bc4..878cb7af 100644 --- a/shopify/resources/draft_order.py +++ b/shopify/resources/draft_order.py @@ -9,7 +9,7 @@ def send_invoice(self, draft_order_invoice=DraftOrderInvoice()): 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')) + 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")) diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index 8bdf8c83..fcf74863 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -25,8 +25,8 @@ class FulfillmentOrders(ShopifyResource): 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}} diff --git a/shopify/resources/fulfillment_event.py b/shopify/resources/fulfillment_event.py index 5982a383..fbd2ece7 100644 --- a/shopify/resources/fulfillment_event.py +++ b/shopify/resources/fulfillment_event.py @@ -3,30 +3,30 @@ 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) def save(self): - status = self.attributes['status'] + 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', + "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_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 a22edc5c..c8110ead 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -17,16 +17,16 @@ def merge_headers(self, *headers): def execute(self, query, variables=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} - 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 894f2fe1..1a4d13fb 100644 --- a/shopify/resources/image.py +++ b/shopify/resources/image.py @@ -30,12 +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'} + 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)) + 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 ff84859a..5b7f4b0a 100644 --- a/shopify/resources/inventory_level.py +++ b/shopify/resources/inventory_level.py @@ -5,7 +5,7 @@ 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): @@ -13,7 +13,7 @@ def _element_path(cls, prefix_options={}, query_options=None): prefix_options, query_options = cls._split_options(prefix_options) return "%s%s.%s%s" % ( - cls._prefix(prefix_options) + '/', + cls._prefix(prefix_options) + "/", cls.plural, cls.format.extension, cls._query_string(query_options), @@ -22,32 +22,32 @@ def _element_path(cls, prefix_options={}, query_options=None): @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/marketing_event.py b/shopify/resources/marketing_event.py index d5c1b70b..6b629449 100644 --- a/shopify/resources/marketing_event.py +++ b/shopify/resources/marketing_event.py @@ -4,5 +4,5 @@ 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/price_rule.py b/shopify/resources/price_rule.py index f6090c13..41fe3e04 100644 --- a/shopify/resources/price_rule.py +++ b/shopify/resources/price_rule.py @@ -13,7 +13,7 @@ def discount_codes(self): return DiscountCode.find(price_rule_id=self.id) def create_batch(self, codes=[]): - codes_json = json.dumps({'discount_codes': codes}) + codes_json = json.dumps({"discount_codes": codes}) response = self.post("batch", codes_json.encode()) return DiscountCodeCreation(PriceRule.format.decode(response.body)) diff --git a/shopify/resources/product.py b/shopify/resources/product.py index a9b4b5a1..cc16e3e3 100644 --- a/shopify/resources/product.py +++ b/shopify/resources/product.py @@ -27,17 +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 119c23eb..3e59d6c1 100644 --- a/shopify/resources/product_listing.py +++ b/shopify/resources/product_listing.py @@ -6,4 +6,4 @@ class ProductListing(ShopifyResource): @classmethod def product_ids(cls, **kwargs): - return cls.get('product_ids', **kwargs) + return cls.get("product_ids", **kwargs) diff --git a/shopify/resources/refund.py b/shopify/resources/refund.py index e8c439d2..124036b3 100644 --- a/shopify/resources/refund.py +++ b/shopify/resources/refund.py @@ -22,8 +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} + 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}) + return cls(cls.format.decode(resource.body), prefix_options={"order_id": order_id}) diff --git a/shopify/resources/user.py b/shopify/resources/user.py index f9b14b54..a1b50cb5 100644 --- a/shopify/resources/user.py +++ b/shopify/resources/user.py @@ -4,4 +4,4 @@ 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 ba6e8089..743b071b 100644 --- a/shopify/resources/variant.py +++ b/shopify/resources/variant.py @@ -14,15 +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/session.py b/shopify/session.py index 4a6a2d85..7e1c13ac 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -21,8 +21,8 @@ class ValidationException(Exception): class Session(object): api_key = None secret = None - protocol = 'https' - myshopify_domain = 'myshopify.com' + protocol = "https" + myshopify_domain = "myshopify.com" port = None @classmethod @@ -36,7 +36,7 @@ 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) @@ -54,7 +54,7 @@ def __init__(self, shop_url, version=None, token=None): 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 + query_params["state"] = state return "https://%s/admin/oauth/authorize?%s" % (self.url, urllib.parse.urlencode(query_params)) def request_token(self, params): @@ -62,17 +62,17 @@ 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')) + 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'] + self.token = json.loads(response.read().decode("utf-8"))["access_token"] return self.token else: raise Exception(response.msg) @@ -112,18 +112,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. @@ -150,17 +150,17 @@ def __encoded_params_for_signature(cls, params): def encoded_pairs(params): for k, v in six.iteritems(params): - if k == 'hmac': + if k == "hmac": continue - if k.endswith('[]'): + if k.endswith("[]"): # foo[]=1&foo[]=2 has to be transformed as foo=["1", "2"] note the whitespace after comma - k = k.rstrip('[]') + 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/version.py b/shopify/version.py index 1010d782..c0a494d8 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = '8.2.0' +VERSION = "8.2.0" diff --git a/shopify/yamlobjects.py b/shopify/yamlobjects.py index b3aedac9..d050877f 100644 --- a/shopify/yamlobjects.py +++ b/shopify/yamlobjects.py @@ -8,7 +8,7 @@ import yaml class YAMLHashWithIndifferentAccess(yaml.YAMLObject): - yaml_tag = '!map:ActiveSupport::HashWithIndifferentAccess' + yaml_tag = "!map:ActiveSupport::HashWithIndifferentAccess" yaml_loader = yaml.SafeLoader @classmethod diff --git a/test/access_scope_test.py b/test/access_scope_test.py index e8d67491..e931263a 100644 --- a/test/access_scope_test.py +++ b/test/access_scope_test.py @@ -4,7 +4,7 @@ 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') + 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) + self.assertEqual("read_products", scopes[0].handle) diff --git a/test/api_permission_test.py b/test/api_permission_test.py index d63eec25..1e93ee74 100644 --- a/test/api_permission_test.py +++ b/test/api_permission_test.py @@ -4,6 +4,6 @@ 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 59cbe7b4..3089daee 100644 --- a/test/api_version_test.py +++ b/test/api_version_test.py @@ -13,35 +13,35 @@ def tearDown(self): 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'), + "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") 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'), + "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 index 567edbdc..23656b60 100644 --- a/test/application_credit_test.py +++ b/test/application_credit_test.py @@ -5,27 +5,27 @@ class ApplicationCreditTest(TestCase): def test_get_application_credit(self): - self.fake('application_credits/445365009', method='GET', body=self.load_fixture('application_credit'), code=200) + 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) + 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) + 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'}, + "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} + {"description": "application credit for refund", "amount": 5.0} ) expected_body = {"application_credit": {"description": "application credit for refund", "amount": 5.0}} diff --git a/test/article_test.py b/test/article_test.py index 9a3e9f11..5ee53bbe 100644 --- a/test/article_test.py +++ b/test/article_test.py @@ -6,70 +6,70 @@ 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'}, + method="POST", + body=self.load_fixture("article"), + headers={"Content-type": "application/json"}, ) - article = shopify.Article({'blog_id': 1008414260}) + 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'}, + "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 3435b317..19606a4b 100644 --- a/test/asset_test.py +++ b/test/asset_test.py @@ -6,57 +6,57 @@ 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'), + method="GET", + body=self.load_fixture("asset"), ) - v = shopify.Asset.find('templates/index.liquid') + 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'), + method="GET", + body=self.load_fixture("asset"), ) - v = shopify.Asset.find('templates/index.liquid') + 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')) + 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'), + method="GET", + body=self.load_fixture("asset"), ) - v = shopify.Asset.find('templates/index.liquid', theme_id=1) + 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'), + method="GET", + body=self.load_fixture("asset"), ) - v = shopify.Asset.find('templates/index.liquid', theme_id=1) + 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'}, + method="PUT", + body=self.load_fixture("asset"), + headers={"Content-type": "application/json"}, ) v.save() @@ -64,27 +64,27 @@ 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'), + method="GET", + body=self.load_fixture("asset"), ) - v = shopify.Asset.find('templates/index.liquid', theme_id=1) + 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="{}" + "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'}, + method="PUT", + body=self.load_fixture("asset"), + headers={"Content-type": "application/json"}, ) - attachment = b'dGVzdCBiaW5hcnkgZGF0YTogAAE=' - key = 'assets/test.jpeg' + 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 ee382741..1ee677ce 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -9,9 +9,9 @@ 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): @@ -27,18 +27,18 @@ 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): @@ -50,8 +50,8 @@ 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, @@ -61,57 +61,57 @@ def test_activate_session_with_one_session_then_clearing_and_activating_with_ano 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'}) + 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": u"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 39062d00..e3a912f0 100644 --- a/test/blog_test.py +++ b/test/blog_test.py @@ -5,11 +5,11 @@ class BlogTest(TestCase): def test_blog_creation(self): self.fake( - 'blogs', - method='POST', + "blogs", + method="POST", code=202, - body=self.load_fixture('blog'), - headers={'Content-type': 'application/json'}, + body=self.load_fixture("blog"), + headers={"Content-type": "application/json"}, ) - blog = shopify.Blog.create({'title': "Test Blog"}) + 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 20688bac..a5aea4f6 100644 --- a/test/carrier_service_test.py +++ b/test/carrier_service_test.py @@ -6,16 +6,16 @@ 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'}, + 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) @@ -23,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 cd1b4401..67836330 100644 --- a/test/cart_test.py +++ b/test/cart_test.py @@ -4,10 +4,10 @@ class CartTest(TestCase): def test_all_should_return_all_carts(self): - self.fake('carts') + 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) + self.assertEqual("test", carts[0].line_items[0].title) diff --git a/test/checkout_test.py b/test/checkout_test.py index 34be1bc5..5b21d560 100644 --- a/test/checkout_test.py +++ b/test/checkout_test.py @@ -4,7 +4,7 @@ class CheckoutTest(TestCase): def test_all_should_return_all_checkouts(self): - self.fake('checkouts') + self.fake("checkouts") checkouts = shopify.Checkout.find() self.assertEqual(1, len(checkouts)) self.assertEqual(450789469, checkouts[0].id) diff --git a/test/collection_listing_test.py b/test/collection_listing_test.py index d6061f7d..4bea7fac 100644 --- a/test/collection_listing_test.py +++ b/test/collection_listing_test.py @@ -4,7 +4,7 @@ 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 @@ -31,10 +31,10 @@ def test_reload_collection_listing(self): def test_get_collection_listing_product_ids(self): self.fake( - 'collection_listings/1/product_ids', - method='GET', + "collection_listings/1/product_ids", + method="GET", code=200, - body=self.load_fixture('collection_listing_product_ids'), + body=self.load_fixture("collection_listing_product_ids"), ) collection_listing = shopify.CollectionListing() diff --git a/test/collection_publication_test.py b/test/collection_publication_test.py index 15b18497..4bd6dc7d 100644 --- a/test/collection_publication_test.py +++ b/test/collection_publication_test.py @@ -6,9 +6,9 @@ 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) @@ -17,9 +17,9 @@ 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'), + "publications/55650051/collection_publications/96062799894", + method="GET", + body=self.load_fixture("collection_publication"), code=200, ) collection_publication = shopify.CollectionPublication.find(96062799894, publication_id=55650051) @@ -29,27 +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'), + "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, + "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, } } @@ -57,14 +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'), + "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 a0043ab2..2d3b47c9 100644 --- a/test/currency_test.py +++ b/test/currency_test.py @@ -4,7 +4,7 @@ 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)) diff --git a/test/customer_saved_search_test.py b/test/customer_saved_search_test.py index 52baa02f..48873f15 100644 --- a/test/customer_saved_search_test.py +++ b/test/customer_saved_search_test.py @@ -9,21 +9,21 @@ def setUp(self): def test_get_customers_from_customer_saved_search(self): self.fake( - 'customer_saved_searches/8899730/customers', body=self.load_fixture('customer_saved_search_customers') + "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', + "customer_saved_searches/8899730/customers.json?limit=1", extension=False, - body=self.load_fixture('customer_saved_search_customers'), + 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 6676e56a..04f2a0c8 100644 --- a/test/customer_test.py +++ b/test/customer_test.py @@ -6,24 +6,24 @@ 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'} + "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) @@ -32,44 +32,44 @@ def test_search(self): self.fake( "customers/search.json?query=Bob+country%3AUnited+States", extension=False, - body=self.load_fixture('customers_search'), + 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', + "customers/207119551/send_invite", + method="POST", body=customer_invite_fixture, - headers={'Content-type': 'application/json'}, + 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', + "customers/207119551/send_invite", + method="POST", body=customer_invite_fixture, - headers={'Content-type': 'application/json'}, + headers={"Content-type": "application/json"}, ) - customer_invite_response = self.customer.send_invite(shopify.CustomerInvite(customer_invite['customer_invite'])) + 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')) + 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')) + 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) diff --git a/test/discount_code_creation_test.py b/test/discount_code_creation_test.py index 6a8a078d..ce2e3156 100644 --- a/test/discount_code_creation_test.py +++ b/test/discount_code_creation_test.py @@ -4,14 +4,14 @@ 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 afacab73..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,18 +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.discount_code.code = "BOGO" self.fake( - 'price_rules/1213131/discount_codes/34', - method='PUT', + "price_rules/1213131/discount_codes/34", + method="PUT", code=200, - body=self.load_fixture('discount_code'), - headers={'Content-type': 'application/json'}, + 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"]) + 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.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("DELETE", self.http.request.get_method()) diff --git a/test/disputes_test.py b/test/disputes_test.py index 47983c7d..71fd01d0 100644 --- a/test/disputes_test.py +++ b/test/disputes_test.py @@ -3,14 +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 f1305d32..12a359bd 100644 --- a/test/draft_order_test.py +++ b/test/draft_order_test.py @@ -6,32 +6,32 @@ 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', + "draft_orders", + method="POST", code=201, - body=self.load_fixture('draft_order'), - headers={'Content-type': 'application/json'}, + 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( @@ -41,73 +41,73 @@ def test_create_draft_order(self): def test_create_draft_order_202(self): self.fake( - 'draft_orders', - method='POST', + "draft_orders", + method="POST", code=202, - body=self.load_fixture('draft_order'), - headers={'Content-type': 'application/json'}, + 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.draft_order.note = "Test new note" self.fake( - 'draft_orders/517119332', - method='PUT', + "draft_orders/517119332", + method="PUT", code=200, - body=self.load_fixture('draft_order'), - headers={'Content-type': 'application/json'}, + 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', + "draft_orders/517119332/send_invoice", + method="POST", body=draft_order_invoice_fixture, - headers={'Content-type': 'application/json'}, + 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', + "draft_orders/517119332/send_invoice", + method="POST", body=draft_order_invoice_fixture, - headers={'Content-type': 'application/json'}, + headers={"Content-type": "application/json"}, ) draft_order_invoice_response = self.draft_order.send_invoice( - shopify.DraftOrderInvoice(draft_order_invoice['draft_order_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', + "draft_orders/517119332/metafields", + method="POST", code=201, - body=self.load_fixture('metafield'), - headers={'Content-type': 'application/json'}, + 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'} + {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"} ) ) self.assertEqual( @@ -117,34 +117,34 @@ def test_add_metafields_to_draft_order(self): 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.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 7c36aade..cbc28802 100644 --- a/test/event_test.py +++ b/test/event_test.py @@ -4,7 +4,7 @@ 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): diff --git a/test/fulfillment_event_test.py b/test/fulfillment_event_test.py index 143e386a..df92c3b6 100644 --- a/test/fulfillment_event_test.py +++ b/test/fulfillment_event_test.py @@ -6,8 +6,8 @@ class FulFillmentEventTest(TestCase): def test_get_fulfillment_event(self): self.fake( "orders/2776493818019/fulfillments/2608403447971/events", - method='GET', - body=self.load_fixture('fulfillment_event'), + 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)) @@ -15,26 +15,26 @@ def test_get_fulfillment_event(self): 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'}, + method="POST", + body=self.load_fixture("fulfillment_event"), + headers={"Content-type": "application/json"}, ) new_fulfillment_event = shopify.FulfillmentEvent( - {'order_id': '2776493818019', 'fulfillment_id': '2608403447971'} + {"order_id": "2776493818019", "fulfillment_id": "2608403447971"} ) - new_fulfillment_event.status = 'ready_for_pickup' + 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'), + method="GET", + body=self.load_fixture("fulfillment_event"), ) - incorrect_status = 'asdf' + incorrect_status = "asdf" fulfillment_event = shopify.FulfillmentEvent.find( - 12584341209251, order_id='2776493818019', fulfillment_id='2608403447971' + 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 dfe5757d..7519b3b9 100644 --- a/test/fulfillment_service_test.py +++ b/test/fulfillment_service_test.py @@ -6,16 +6,16 @@ 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'}, + 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) @@ -23,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 deba5f65..8bb84efb 100644 --- a/test/fulfillment_test.py +++ b/test/fulfillment_test.py @@ -6,55 +6,55 @@ 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') + 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'}, + 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') + 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'}, + 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') + 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'}, + 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) @@ -62,15 +62,15 @@ def test_update_tracking(self): 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'}, + headers={"Content-type": "application/json"}, body=update_tracking, ) @@ -79,5 +79,5 @@ def test_update_tracking(self): 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 bb18fb39..2b5e6055 100644 --- a/test/gift_card_test.py +++ b/test/gift_card_test.py @@ -6,29 +6,29 @@ class GiftCardTest(TestCase): def test_gift_card_creation(self): self.fake( - 'gift_cards', - method='POST', + "gift_cards", + method="POST", code=202, - body=self.load_fixture('gift_card'), - headers={'Content-type': 'application/json'}, + body=self.load_fixture("gift_card"), + headers={"Content-type": "application/json"}, ) - gift_card = shopify.GiftCard.create({'code': 'd7a2bcggda89c293', 'note': "Gift card note."}) + 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", method="GET", code=200, body=self.load_fixture("gift_card")) self.fake( - 'gift_cards/4208208/disable', - method='POST', + "gift_cards/4208208/disable", + method="POST", code=200, - body=self.load_fixture('gift_card_disabled'), - headers={'Content-length': '0', 'Content-type': 'application/json'}, + 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) @@ -36,20 +36,20 @@ def test_disable_gift_card(self): 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", method="GET", code=200, body=self.load_fixture("gift_card")) self.fake( - 'gift_cards/4208208/adjustments', - method='POST', + "gift_cards/4208208/adjustments", + method="POST", code=201, - body=self.load_fixture('gift_card_adjustment'), - headers={'Content-type': 'application/json'}, + 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, + "amount": 100, } ) ) @@ -58,8 +58,8 @@ def test_adjust_gift_card(self): def test_search(self): self.fake( - "gift_cards/search.json?query=balance%3A10", extension=False, body=self.load_fixture('gift_cards_search') + "gift_cards/search.json?query=balance%3A10", extension=False, body=self.load_fixture("gift_cards_search") ) - results = shopify.GiftCard.search(query='balance:10') + 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 dc43c56f..50f89416 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -7,28 +7,28 @@ 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.fake( - 'graphql', - method='POST', + "graphql", + method="POST", code=201, headers={ - 'X-Shopify-Access-Token': 'token', - 'Accept': 'application/json', - 'Content-Type': 'application/json', + "X-Shopify-Access-Token": "token", + "Accept": "application/json", + "Content-Type": "application/json", }, ) - query = ''' + query = """ { shop { name id } } - ''' + """ self.result = client.execute(query) def test_fetch_shop_with_graphql(self): - self.assertTrue(json.loads(self.result)['shop']['name'] == 'Apple Computers') + self.assertTrue(json.loads(self.result)["shop"]["name"] == "Apple Computers") diff --git a/test/image_test.py b/test/image_test.py index b4879b35..3dad3817 100644 --- a/test/image_test.py +++ b/test/image_test.py @@ -7,38 +7,38 @@ 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'}, + method="POST", + body=self.load_fixture("image"), + headers={"Content-type": "application/json"}, ) - image = shopify.Image({'product_id': 632910392}) + 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 + "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'}, + method="POST", + body=self.load_fixture("image"), + headers={"Content-type": "application/json"}, ) - image = shopify.Image({'product_id': 632910392}) + 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') + 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 + "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) @@ -46,9 +46,9 @@ def test_attach_image(self): 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'}, + method="POST", + body=self.load_fixture("image"), + headers={"Content-type": "application/json"}, ) image = shopify.Image() image.position = 1 @@ -57,25 +57,25 @@ def test_create_image_then_add_parent_id(self): image.save() self.assertEqual( - 'http://cdn.shopify.com/s/files/1/0006/9093/3842/products/ipod-nano.png?v=1389388540', image.src + "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 63de7e97..61c66e60 100644 --- a/test/inventory_item_test.py +++ b/test/inventory_item_test.py @@ -4,16 +4,16 @@ 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 7ad4d93f..cf621b7c 100644 --- a/test/inventory_level_test.py +++ b/test/inventory_level_test.py @@ -6,40 +6,40 @@ 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'] + 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'}, + "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) @@ -47,22 +47,22 @@ def test_inventory_level_connect(self): 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} + {"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 2a96dbd5..0d705abb 100644 --- a/test/limits_test.py +++ b/test/limits_test.py @@ -16,7 +16,7 @@ def setUpClass(self): 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 @@ -30,36 +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 b0165b8e..8550b1af 100644 --- a/test/location_test.py +++ b/test/location_test.py @@ -5,24 +5,24 @@ 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)) 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") 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() diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py index 72763934..a3753d63 100644 --- a/test/marketing_event_test.py +++ b/test/marketing_event_test.py @@ -8,93 +8,93 @@ 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'}, + "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="GET", code=200, body=self.load_fixture("marketing_event")) self.fake( - 'marketing_events/1', - method='PUT', + "marketing_events/1", + method="PUT", code=200, - body=self.load_fixture('marketing_event'), - headers={'Content-type': 'application/json'}, + 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', + "marketing_events/1/engagements", + method="POST", code=201, - body=self.load_fixture('engagement'), - headers={'Content-type': 'application/json'}, + 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, + "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'] + 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 7c26c16a..4ca6dbff 100644 --- a/test/order_risk_test.py +++ b/test/order_risk_test.py @@ -6,11 +6,11 @@ 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'}, + method="POST", + body=self.load_fixture("order_risk"), + headers={"Content-type": "application/json"}, ) - v = shopify.OrderRisk({'order_id': 450789469}) + v = shopify.OrderRisk({"order_id": 450789469}) v.message = "This order was placed from a proxy IP" v.recommendation = "cancel" v.score = "1.0" @@ -23,28 +23,28 @@ def test_create_order_risk(self): self.assertEqual(284138680, v.id) def test_get_order_risks(self): - self.fake("orders/450789469/risks", method='GET', body=self.load_fixture('order_risks')) + 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')) + 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") + 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="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'}, + method="PUT", + body=self.load_fixture("order_risk"), + headers={"Content-type": "application/json"}, ) v = shopify.OrderRisk.find(284138680, order_id=450789469) diff --git a/test/order_test.py b/test/order_test.py index 45e2e90d..257ab08a 100644 --- a/test/order_test.py +++ b/test/order_test.py @@ -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,19 +37,19 @@ 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) + 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) diff --git a/test/pagination_test.py b/test/pagination_test.py index e06715d5..72fc78e9 100644 --- a/test/pagination_test.py +++ b/test/pagination_test.py @@ -7,13 +7,13 @@ 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\""} + next_headers = {"Link": "<" + self.next_page_url + '>; rel="next"'} + prev_headers = {"Link": "<" + self.prev_page_url + '>; rel="previous"'} self.fake( "products", @@ -35,7 +35,7 @@ def setUp(self): ) 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) diff --git a/test/payouts_test.py b/test/payouts_test.py index 255df766..f82851c2 100644 --- a/test/payouts_test.py +++ b/test/payouts_test.py @@ -3,15 +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 16a58426..a28b15de 100644 --- a/test/price_rules_test.py +++ b/test/price_rules_test.py @@ -7,43 +7,43 @@ 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', + "price_rules/1213131", + method="PUT", code=200, - body=self.load_fixture('price_rule'), - headers={'Content-type': 'application/json'}, + 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', + "price_rules", + method="POST", code=202, - body=self.load_fixture('price_rule'), - headers={'Content-type': 'application/json'}, + body=self.load_fixture("price_rule"), + headers={"Content-type": "application/json"}, ) price_rule = shopify.PriceRule.create( { @@ -53,8 +53,8 @@ def test_price_rule_creation(self): "allocation_method": "across", "value_type": "percentage", "value": -100, - "once_per_customer": 'true', - "customer_selection": 'all', + "once_per_customer": "true", + "customer_selection": "all", } ) self.assertEqual("BOGO", price_rule.title) @@ -62,48 +62,48 @@ def test_price_rule_creation(self): def test_get_discount_codes(self): self.fake( - 'price_rules/1213131/discount_codes', method='GET', code=200, body=self.load_fixture('discount_codes') + "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', + "price_rules/1213131/discount_codes", + method="POST", body=price_rule_discount_fixture, - headers={'Content-type': 'application/json'}, + headers={"Content-type": "application/json"}, ) price_rule_discount_response = self.price_rule.add_discount_code( - shopify.DiscountCode(discount_code['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', + "price_rules/1213131/batch", + method="POST", code=201, - body=self.load_fixture('discount_code_creation'), - headers={'Content-type': 'application/json'}, + body=self.load_fixture("discount_code_creation"), + headers={"Content-type": "application/json"}, ) - batch = self.price_rule.create_batch([{'code': 'SUMMER1'}, {'code': 'SUMMER2'}, {'code': 'SUMMER3'}]) + 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', + "price_rules/1213131/batch/989355119", + method="GET", code=200, - body=self.load_fixture('discount_code_creation'), + 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 9c078586..dcdc048d 100644 --- a/test/product_listing_test.py +++ b/test/product_listing_test.py @@ -4,7 +4,7 @@ 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 @@ -30,10 +30,10 @@ def test_reload_product_listing(self): def test_get_product_listing_product_ids(self): self.fake( - 'product_listings/product_ids', - method='GET', + "product_listings/product_ids", + method="GET", status=200, - body=self.load_fixture('product_listing_product_ids'), + 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 015d8dd1..671e7786 100644 --- a/test/product_publication_test.py +++ b/test/product_publication_test.py @@ -6,7 +6,7 @@ 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) @@ -15,9 +15,9 @@ 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'), + "publications/55650051/product_publications/647162527768", + method="GET", + body=self.load_fixture("product_publication"), code=200, ) product_publication = shopify.ProductPublication.find(647162527768, publication_id=55650051) @@ -27,27 +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'), + "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, + "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, } } @@ -55,14 +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'), + "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 32c23e0d..de183691 100644 --- a/test/product_test.py +++ b/test/product_test.py @@ -6,21 +6,21 @@ 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', + method="POST", code=201, - body=self.load_fixture('metafield'), - headers={'Content-type': 'application/json'}, + 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"} + {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"} ) ) @@ -30,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() @@ -39,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)) @@ -47,7 +47,7 @@ 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) @@ -56,14 +56,14 @@ 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'), + 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" @@ -72,16 +72,16 @@ def test_update_loaded_variant(self): def test_add_variant_to_product(self): self.fake( "products/632910392/variants", - method='POST', - body=self.load_fixture('variant'), - headers={'Content-type': 'application/json'}, + method="POST", + body=self.load_fixture("variant"), + headers={"Content-type": "application/json"}, ) self.fake( "products/632910392/variants/808950810", - method='PUT', + method="PUT", code=200, - body=self.load_fixture('variant'), - headers={'Content-type': 'application/json'}, + 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 d352952d..dab26fc5 100644 --- a/test/publication_test.py +++ b/test/publication_test.py @@ -4,7 +4,7 @@ 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 e7826372..c785388e 100644 --- a/test/recurring_charge_test.py +++ b/test/recurring_charge_test.py @@ -7,11 +7,11 @@ 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'}, + method="POST", + headers={"Content-length": "0", "Content-type": "application/json"}, body=" ", ) - charge = shopify.RecurringApplicationCharge({'id': 35463}) + charge = shopify.RecurringApplicationCharge({"id": 35463}) charge.activate() def test_current_method_returns_active_charge(self): @@ -34,8 +34,8 @@ def test_usage_charges_method_returns_associated_usage_charges(self): self.fake( "recurring_application_charges/455696195/usage_charges", - method='GET', - body=self.load_fixture('usage_charges'), + method="GET", + body=self.load_fixture("usage_charges"), ) usage_charges = charge.usage_charges() self.assertEqual(len(usage_charges), 2) @@ -48,16 +48,16 @@ def test_customize_method_increases_capped_amount(self): 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'), + 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 8ca9622e..905bbabc 100644 --- a/test/refund_test.py +++ b/test/refund_test.py @@ -5,7 +5,7 @@ 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) @@ -16,11 +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 4c060571..bfb66935 100644 --- a/test/report_test.py +++ b/test/report_test.py @@ -4,22 +4,22 @@ 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', + "reports", + method="POST", code=201, - body=self.load_fixture('report'), - headers={'Content-type': 'application/json'}, + body=self.load_fixture("report"), + headers={"Content-type": "application/json"}, ) report = shopify.Report.create( { @@ -27,10 +27,10 @@ def test_create_report(self): "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.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 ce8542cf..47ee9f92 100644 --- a/test/resource_feedback_test.py +++ b/test/resource_feedback_test.py @@ -5,36 +5,36 @@ 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': {}}) + body = json.dumps({"resource_feedback": {}}) self.fake( - 'products/42/resource_feedback', method='POST', body=body, headers={'Content-Type': 'application/json'} + "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 d7eb0bbd..de77aeeb 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -11,36 +11,36 @@ 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): 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,15 +80,15 @@ 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): 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( @@ -98,7 +98,7 @@ def test_create_permission_url_returns_correct_url_with_single_scope_and_redirec def test_create_permission_url_returns_correct_url_with_dual_scope_and_redirect_uri(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", "write_customers"] permission_url = session.create_permission_url(scope, "my_redirect_uri.com") self.assertEqual( @@ -108,7 +108,7 @@ def test_create_permission_url_returns_correct_url_with_dual_scope_and_redirect_ def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_uri(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( @@ -118,7 +118,7 @@ def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_ur def test_create_permission_url_returns_correct_url_with_no_scope_and_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') + session = shopify.Session("http://localhost.myshopify.com", "unstable") scope = [] permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate") self.assertEqual( @@ -128,7 +128,7 @@ def test_create_permission_url_returns_correct_url_with_no_scope_and_redirect_ur def test_create_permission_url_returns_correct_url_with_single_scope_and_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') + 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( @@ -138,123 +138,123 @@ def test_create_permission_url_returns_correct_url_with_single_scope_and_redirec 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') + session = shopify.Session("http://localhost.myshopify.com", "unstable") self.fake( None, - url='https://localhost.myshopify.com/admin/oauth/access_token', - method='POST', + 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()} + shopify.Session.secret = "secret" + params = {"code": "any-code", "timestamp": time.time()} hmac = shopify.Session.calculate_hmac(params) - params['hmac'] = hmac + params["hmac"] = hmac self.fake( None, - url='https://localhost.myshopify.com/admin/oauth/access_token', - method='POST', + 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') + session = shopify.Session("http://localhost.myshopify.com", "unstable") token = session.request_token(params) self.assertEqual("token", token) 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 normalize_url(self, url): diff --git a/test/shipping_zone_test.py b/test/shipping_zone_test.py index 80781354..3d1e1e4d 100644 --- a/test/shipping_zone_test.py +++ b/test/shipping_zone_test.py @@ -4,7 +4,7 @@ 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") diff --git a/test/shop_test.py b/test/shop_test.py index 85346849..3a88a2a6 100644 --- a/test/shop_test.py +++ b/test/shop_test.py @@ -28,15 +28,15 @@ def test_get_metafields_for_shop(self): def test_add_metafield(self): self.fake( "metafields", - method='POST', + method="POST", code=201, - body=self.load_fixture('metafield'), - headers={'Content-type': 'application/json'}, + 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"} + {"namespace": "contact", "key": "email", "value": "123@example.com", "value_type": "string"} ) ) diff --git a/test/storefront_access_token_test.py b/test/storefront_access_token_test.py index 8e223dc2..ce5ef805 100644 --- a/test/storefront_access_token_test.py +++ b/test/storefront_access_token_test.py @@ -5,28 +5,28 @@ 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_tokens", + method="POST", + body=self.load_fixture("storefront_access_token"), + headers={"Content-type": "application/json"}, ) - storefront_access_token = shopify.StorefrontAccessToken.create({'title': 'Test'}) + 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') + "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') + "storefront_access_tokens", method="GET", code=200, body=self.load_fixture("storefront_access_tokens") ) tokens = shopify.StorefrontAccessToken.find() diff --git a/test/tender_transaction_test.py b/test/tender_transaction_test.py index e1b6851b..fe73c633 100644 --- a/test/tender_transaction_test.py +++ b/test/tender_transaction_test.py @@ -5,7 +5,7 @@ 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 2abaa606..9f098a26 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -18,48 +18,48 @@ 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.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', + "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'), + response_headers={"Content-type": "application/json"}, + body=self.load_fixture("api_version"), has_user_agent=False, ) - 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') + 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) 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 - 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 6b44b53b..e02fae00 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -5,7 +5,7 @@ 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 7936842a..9b0d54b7 100644 --- a/test/transactions_test.py +++ b/test/transactions_test.py @@ -3,9 +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 13f3ac16..a816636b 100644 --- a/test/usage_charge_test.py +++ b/test/usage_charge_test.py @@ -6,23 +6,23 @@ 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'}, + 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} + {"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'), + method="GET", + body=self.load_fixture("usage_charge"), ) charge = shopify.UsageCharge.find(359376002, recurring_application_charge_id=654381177) - self.assertEqual('1000 emails', charge.description) + self.assertEqual("1000 emails", charge.description) diff --git a/test/user_test.py b/test/user_test.py index 3f0e2385..efb90d30 100644 --- a/test/user_test.py +++ b/test/user_test.py @@ -4,7 +4,7 @@ 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/variant_test.py b/test/variant_test.py index 0d8d1967..63ecb639 100644 --- a/test/variant_test.py +++ b/test/variant_test.py @@ -4,46 +4,46 @@ class VariantTest(TestCase): def test_get_variants(self): - self.fake("products/632910392/variants", method='GET', body=self.load_fixture('variants')) + 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')) + 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')) + 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'}, + 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'}, + method="POST", + body=self.load_fixture("variant"), + headers={"Content-type": "application/json"}, ) - v = shopify.Variant({'product_id': 632910392}) + 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'}, + 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) From 38f2ea661ad18a13a3f263f31f3d1366063bfbee Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 1 Mar 2021 17:14:41 -0500 Subject: [PATCH 148/259] Add pylint hook and config file --- .pre-commit-config.yaml | 4 + pylintrc | 444 ++++++++++++++++++++++++++++++++++++++++ scripts/shopify_api.py | 2 +- 3 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 pylintrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66662ec0..c60459d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,3 +14,7 @@ repos: rev: 20.8b1 hooks: - id: black +- repo: https://github.com/PyCQA/pylint + rev: pylint-2.7.2 + hooks: + - id: pylint diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..7b0abf4d --- /dev/null +++ b/pylintrc @@ -0,0 +1,444 @@ +# 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, + invalid-name, + 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/scripts/shopify_api.py b/scripts/shopify_api.py index 3fd63109..355987ad 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -215,7 +215,7 @@ 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) From 1317b950a7a79435dc7d5f36e93b2150a85dfc4e Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 1 Mar 2021 17:31:53 -0500 Subject: [PATCH 149/259] Remove invalid-name from list of ignored errors and make changes --- pylintrc | 1 - test/base_test.py | 2 +- test/session_test.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pylintrc b/pylintrc index 7b0abf4d..97de81bf 100644 --- a/pylintrc +++ b/pylintrc @@ -76,7 +76,6 @@ disable=abstract-method, global-statement, hex-method, idiv-method, - invalid-name, implicit-str-concat-in-sequence, import-error, import-self, diff --git a/test/base_test.py b/test/base_test.py index 1ee677ce..a07368b6 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -41,7 +41,7 @@ def test_activate_session_should_set_site_given_version(self): 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() diff --git a/test/session_test.py b/test/session_test.py index de77aeeb..60fb6678 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -50,7 +50,7 @@ def test_setup_api_key_and_secret_for_all_sessions(self): def test_use_https_protocol_by_default_for_all_sessions(self): 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") shopify.ShopifyResource.activate_session(session1) From eb10efc4a18c3e3580181fe621db2c2e274ea2f4 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Tue, 2 Mar 2021 11:53:25 -0500 Subject: [PATCH 150/259] Empty commit From 5f5bb491e9e8967e2e90d078ece9827bb02dca9b Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Tue, 2 Mar 2021 12:16:07 -0500 Subject: [PATCH 151/259] Add pre-commit badge to repo --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5ef0e121..746aa724 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) [![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/master/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/master/LICENSE) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library From e5e522e57e35f7fde19d3e343b17f8eaf2b3a62a Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 9 Mar 2021 17:39:49 -0500 Subject: [PATCH 152/259] Create SessionTokenUtility object to decode a session token Add PyJWT dependency Support python 2.7 Remove contradictory autopep8 formatter Create custom exceptions for SessionTokenUtility Address comments Restructure utils into a subpackage --- .pre-commit-config.yaml | 4 -- setup.py | 2 + shopify/utils/__init__.py | 2 + shopify/utils/exceptions.py | 10 +++++ shopify/utils/utilities.py | 64 ++++++++++++++++++++++++++ test/utils/utilities_test.py | 87 ++++++++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 shopify/utils/__init__.py create mode 100644 shopify/utils/exceptions.py create mode 100644 shopify/utils/utilities.py create mode 100644 test/utils/utilities_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c60459d6..6aec2f5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,10 +6,6 @@ repos: hooks: - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.4 - hooks: - - id: autopep8 - repo: https://github.com/psf/black rev: 20.8b1 hooks: diff --git a/setup.py b/setup.py index 84ec9c24..c0a935b4 100755 --- a/setup.py +++ b/setup.py @@ -23,6 +23,8 @@ license="MIT License", install_requires=[ "pyactiveresource>=2.2.2", + "PyJWT <= 1.7.1; python_version == '2.7'", + "PyJWT >= 2.0.0; python_version >= '3.6'", "PyYAML", "six", ], diff --git a/shopify/utils/__init__.py b/shopify/utils/__init__.py new file mode 100644 index 00000000..3e7b5ab3 --- /dev/null +++ b/shopify/utils/__init__.py @@ -0,0 +1,2 @@ +from .exceptions import * +from .utilities import SessionTokenUtility diff --git a/shopify/utils/exceptions.py b/shopify/utils/exceptions.py new file mode 100644 index 00000000..f74704e2 --- /dev/null +++ b/shopify/utils/exceptions.py @@ -0,0 +1,10 @@ +class InvalidIssuerError(Exception): + pass + + +class MismatchedHostsError(Exception): + pass + + +class TokenAuthenticationError(Exception): + pass diff --git a/shopify/utils/utilities.py b/shopify/utils/utilities.py new file mode 100644 index 00000000..6de85c7b --- /dev/null +++ b/shopify/utils/utilities.py @@ -0,0 +1,64 @@ +from .exceptions import * + +import jwt +import re +import sys + +if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 + from urlparse import urljoin +else: + from urllib.parse import urljoin + + +class SessionTokenUtility: + ALGORITHM = "HS256" + PREFIX = "Bearer " + REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] + + @classmethod + def get_decoded_session_token(cls, authorization_header, api_key, secret): + session_token = cls.__extract_session_token(authorization_header) + decoded_payload = cls.__decode_session_token(session_token, api_key, secret) + cls.__validate_issuer(decoded_payload) + + return decoded_payload + + @classmethod + def __extract_session_token(cls, authorization_header): + if not authorization_header.startswith(cls.PREFIX): + raise TokenAuthenticationError("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token") + + return authorization_header[len(cls.PREFIX) :] + + @classmethod + def __decode_session_token(cls, session_token, api_key, secret): + return jwt.decode( + session_token, + secret, + audience=api_key, + algorithms=[cls.ALGORITHM], + options={"require": cls.REQUIRED_FIELDS}, + ) + + @classmethod + def __validate_issuer(cls, decoded_payload): + cls.__validate_issuer_hostname(decoded_payload) + cls.__validate_issuer_and_dest_match(decoded_payload) + + @classmethod + def __validate_issuer_hostname(cls, decoded_payload): + hostname_pattern = r"[a-z0-9][a-z0-9-]*[a-z0-9]" + shop_domain_re = re.compile(r"^https://{h}\.myshopify\.com/$".format(h=hostname_pattern)) + + issuer_root = urljoin(decoded_payload["iss"], "/") + + if not shop_domain_re.match(issuer_root): + raise InvalidIssuerError() + + @classmethod + def __validate_issuer_and_dest_match(cls, decoded_payload): + issuer_root = urljoin(decoded_payload["iss"], "/") + dest_root = urljoin(decoded_payload["dest"], "/") + + if issuer_root != dest_root: + raise MismatchedHostsError() diff --git a/test/utils/utilities_test.py b/test/utils/utilities_test.py new file mode 100644 index 00000000..4561a6c3 --- /dev/null +++ b/test/utils/utilities_test.py @@ -0,0 +1,87 @@ +from shopify.utils import * +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 TestSessionTokenUtilityGetDecodedSessionToken(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(TokenAuthenticationError): + SessionTokenUtility.get_decoded_session_token( + authorization_header, api_key=self.api_key, secret=self.secret + ) + + def test_raises_jwt_error_if_session_token_is_expired(self): + self.payload["exp"] = timestamp((datetime.now() + timedelta(0, -10))) + + with self.assertRaises(jwt.exceptions.ExpiredSignatureError): + SessionTokenUtility.get_decoded_session_token( + self.build_auth_header(), api_key=self.api_key, secret=self.secret + ) + + def test_raises_if_aud_doesnt_match_api_key(self): + self.payload["aud"] = "bad audience" + + with self.assertRaises(jwt.exceptions.InvalidAudienceError): + SessionTokenUtility.get_decoded_session_token( + self.build_auth_header(), api_key=self.api_key, secret=self.secret + ) + + def test_raises_if_issuer_hostname_is_invalid(self): + self.payload["iss"] = "bad_shop_hostname" + + with self.assertRaises(InvalidIssuerError): + SessionTokenUtility.get_decoded_session_token( + self.build_auth_header(), api_key=self.api_key, secret=self.secret + ) + + def test_raises_if_iss_and_dest_dont_match(self): + self.payload["dest"] = "bad_shop.myshopify.com" + + with self.assertRaises(MismatchedHostsError): + SessionTokenUtility.get_decoded_session_token( + self.build_auth_header(), api_key=self.api_key, secret=self.secret + ) + + def test_returns_decoded_payload(self): + decoded_payload = SessionTokenUtility.get_decoded_session_token( + self.build_auth_header(), api_key=self.api_key, secret=self.secret + ) + + self.assertEqual(self.payload, decoded_payload) From d335e7b1c52e69dff57231fbeccee443f49922ae Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 16 Mar 2021 11:33:26 -0400 Subject: [PATCH 153/259] Create a session_token file with helper methods --- shopify/session_token.py | 81 +++++++++++++++++++ shopify/utils/__init__.py | 2 - shopify/utils/exceptions.py | 10 --- shopify/utils/utilities.py | 64 --------------- ...tilities_test.py => session_token_test.py} | 46 +++++------ 5 files changed, 104 insertions(+), 99 deletions(-) create mode 100644 shopify/session_token.py delete mode 100644 shopify/utils/__init__.py delete mode 100644 shopify/utils/exceptions.py delete mode 100644 shopify/utils/utilities.py rename test/{utils/utilities_test.py => session_token_test.py} (55%) diff --git a/shopify/session_token.py b/shopify/session_token.py new file mode 100644 index 00000000..6d50b30d --- /dev/null +++ b/shopify/session_token.py @@ -0,0 +1,81 @@ +import jwt +import re +import six +import sys + +if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 + from urlparse import urljoin +else: + from urllib.parse import urljoin + + +HOSTNAME_PATTERN = r"[a-z0-9][a-z0-9-]*[a-z0-9]" +SHOP_DOMAIN_RE = re.compile(r"^https://{h}\.myshopify\.com/$".format(h=HOSTNAME_PATTERN)) + +ALGORITHM = "HS256" +PREFIX = "Bearer " +REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] + + +class SessionTokenError(Exception): + pass + + +class InvalidIssuerError(SessionTokenError): + pass + + +class MismatchedHostsError(SessionTokenError): + pass + + +class TokenAuthenticationError(SessionTokenError): + pass + + +def get_decoded_session_token(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], + 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_DOMAIN_RE.match(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 deleted file mode 100644 index 3e7b5ab3..00000000 --- a/shopify/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .exceptions import * -from .utilities import SessionTokenUtility diff --git a/shopify/utils/exceptions.py b/shopify/utils/exceptions.py deleted file mode 100644 index f74704e2..00000000 --- a/shopify/utils/exceptions.py +++ /dev/null @@ -1,10 +0,0 @@ -class InvalidIssuerError(Exception): - pass - - -class MismatchedHostsError(Exception): - pass - - -class TokenAuthenticationError(Exception): - pass diff --git a/shopify/utils/utilities.py b/shopify/utils/utilities.py deleted file mode 100644 index 6de85c7b..00000000 --- a/shopify/utils/utilities.py +++ /dev/null @@ -1,64 +0,0 @@ -from .exceptions import * - -import jwt -import re -import sys - -if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0 - from urlparse import urljoin -else: - from urllib.parse import urljoin - - -class SessionTokenUtility: - ALGORITHM = "HS256" - PREFIX = "Bearer " - REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] - - @classmethod - def get_decoded_session_token(cls, authorization_header, api_key, secret): - session_token = cls.__extract_session_token(authorization_header) - decoded_payload = cls.__decode_session_token(session_token, api_key, secret) - cls.__validate_issuer(decoded_payload) - - return decoded_payload - - @classmethod - def __extract_session_token(cls, authorization_header): - if not authorization_header.startswith(cls.PREFIX): - raise TokenAuthenticationError("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token") - - return authorization_header[len(cls.PREFIX) :] - - @classmethod - def __decode_session_token(cls, session_token, api_key, secret): - return jwt.decode( - session_token, - secret, - audience=api_key, - algorithms=[cls.ALGORITHM], - options={"require": cls.REQUIRED_FIELDS}, - ) - - @classmethod - def __validate_issuer(cls, decoded_payload): - cls.__validate_issuer_hostname(decoded_payload) - cls.__validate_issuer_and_dest_match(decoded_payload) - - @classmethod - def __validate_issuer_hostname(cls, decoded_payload): - hostname_pattern = r"[a-z0-9][a-z0-9-]*[a-z0-9]" - shop_domain_re = re.compile(r"^https://{h}\.myshopify\.com/$".format(h=hostname_pattern)) - - issuer_root = urljoin(decoded_payload["iss"], "/") - - if not shop_domain_re.match(issuer_root): - raise InvalidIssuerError() - - @classmethod - def __validate_issuer_and_dest_match(cls, decoded_payload): - issuer_root = urljoin(decoded_payload["iss"], "/") - dest_root = urljoin(decoded_payload["dest"], "/") - - if issuer_root != dest_root: - raise MismatchedHostsError() diff --git a/test/utils/utilities_test.py b/test/session_token_test.py similarity index 55% rename from test/utils/utilities_test.py rename to test/session_token_test.py index 4561a6c3..8e90ff58 100644 --- a/test/utils/utilities_test.py +++ b/test/session_token_test.py @@ -1,4 +1,4 @@ -from shopify.utils import * +from shopify import session_token from test.test_helper import TestCase from datetime import datetime, timedelta @@ -13,7 +13,7 @@ def timestamp(date): return time.mktime(date.timetuple()) if sys.version_info[0] < 3 else date.timestamp() -class TestSessionTokenUtilityGetDecodedSessionToken(TestCase): +class TestSessionTokenGetDecodedSessionToken(TestCase): @classmethod def setUpClass(self): self.secret = "API Secret" @@ -42,45 +42,45 @@ def build_auth_header(self): def test_raises_if_token_authentication_header_is_not_bearer(self): authorization_header = "Bad auth header" - with self.assertRaises(TokenAuthenticationError): - SessionTokenUtility.get_decoded_session_token( - authorization_header, api_key=self.api_key, secret=self.secret - ) + with self.assertRaises(session_token.TokenAuthenticationError) as cm: + session_token.get_decoded_session_token(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, -10))) - with self.assertRaises(jwt.exceptions.ExpiredSignatureError): - SessionTokenUtility.get_decoded_session_token( - self.build_auth_header(), api_key=self.api_key, secret=self.secret - ) + with self.assertRaises(session_token.SessionTokenError, msg="Expird") as cm: + session_token.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + + self.assertEqual("Signature has expired", str(cm.exception)) def test_raises_if_aud_doesnt_match_api_key(self): self.payload["aud"] = "bad audience" - with self.assertRaises(jwt.exceptions.InvalidAudienceError): - SessionTokenUtility.get_decoded_session_token( - self.build_auth_header(), api_key=self.api_key, secret=self.secret - ) + with self.assertRaises(session_token.SessionTokenError) as cm: + session_token.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + + self.assertEqual("Invalid audience", str(cm.exception)) def test_raises_if_issuer_hostname_is_invalid(self): self.payload["iss"] = "bad_shop_hostname" - with self.assertRaises(InvalidIssuerError): - SessionTokenUtility.get_decoded_session_token( - self.build_auth_header(), api_key=self.api_key, secret=self.secret - ) + with self.assertRaises(session_token.InvalidIssuerError) as cm: + session_token.get_decoded_session_token(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(MismatchedHostsError): - SessionTokenUtility.get_decoded_session_token( - self.build_auth_header(), api_key=self.api_key, secret=self.secret - ) + with self.assertRaises(session_token.MismatchedHostsError) as cm: + session_token.get_decoded_session_token(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 = SessionTokenUtility.get_decoded_session_token( + decoded_payload = session_token.get_decoded_session_token( self.build_auth_header(), api_key=self.api_key, secret=self.secret ) From f6a0b3052d754266af9ca25f3bd874b7a324ef48 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 16 Mar 2021 12:13:01 -0400 Subject: [PATCH 154/259] Add usage section for session_token in README --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 746aa724..bbc57c22 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,60 @@ _Note: Your application must be public to test the billing process. To test on a has_been_billed = activated_charge.status == 'active' ``` +### 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 `get_decoded_session_token` function to help extract and decode a session token from an HTTP Authorization header. + +#### Basic usage + +```python +from shopify import session_token + +decoded_payload = session_token.get_decoded_session_token( + authorization_header=your_auth_request_header, + api_key=your_api_key, + secret=your_api_secret, +) +``` + +#### Use session_token to create a decorator + +Here's a sample use-case of building a Django decorator using `session_token`: + +```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: + session_token = session_token.get_decoded_session_token( + authorization_header = request.headers.get('Authorization'), + api_key = SHOPIFY_API_KEY, + secret = SHOPIFY_API_SECRET + ) + with shopify_session(session_token): + return func(*args, **kwargs) + except session_token.SessionTokenError: + return generate_http_401_response() + + return wrapper + + +def shopify_session(session_token): + shopify_domain = 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 +def products(request): + products = shopify.Product.find() + ... +``` + ### Advanced Usage It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which is a port of rails/ActiveResource to Python and upon which this package relies heavily. From 8bc8132b30e2b81f8f126914d583339a1c514fa7 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 16 Mar 2021 15:17:54 -0400 Subject: [PATCH 155/259] Change session_token decode signature --- README.md | 23 ++++++++++++----------- shopify/session_token.py | 2 +- test/session_token_test.py | 32 +++++++++++++++++++++++++------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index bbc57c22..5a2cac93 100644 --- a/README.md +++ b/README.md @@ -122,23 +122,23 @@ _Note: Your application must be public to test the billing process. To test on a ### 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 `get_decoded_session_token` function to help extract and decode a session token from an HTTP Authorization header. +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.get_decoded_session_token( +decoded_payload = session_token.decode_from_header( authorization_header=your_auth_request_header, api_key=your_api_key, secret=your_api_secret, ) ``` -#### Use session_token to create a decorator +#### Create a decorator using `session_token` -Here's a sample use-case of building a Django 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 @@ -148,27 +148,28 @@ def session_token_required(func): def wrapper(*args, **kwargs): request = args[0] # Or flask.request if you use Flask try: - session_token = session_token.get_decoded_session_token( + 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(session_token): + with shopify_session(decoded_session_token): return func(*args, **kwargs) - except session_token.SessionTokenError: - return generate_http_401_response() + except session_token.SessionTokenError as e: + # Log the error here + return unauthorized_401_response() return wrapper -def shopify_session(session_token): - shopify_domain = session_token.get("dest") +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 +@session_token_required # Requests to /products require session tokens def products(request): products = shopify.Product.find() ... diff --git a/shopify/session_token.py b/shopify/session_token.py index 6d50b30d..6ab8c17c 100644 --- a/shopify/session_token.py +++ b/shopify/session_token.py @@ -33,7 +33,7 @@ class TokenAuthenticationError(SessionTokenError): pass -def get_decoded_session_token(authorization_header, api_key, secret): +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) diff --git a/test/session_token_test.py b/test/session_token_test.py index 8e90ff58..38e43808 100644 --- a/test/session_token_test.py +++ b/test/session_token_test.py @@ -43,23 +43,41 @@ 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.get_decoded_session_token(authorization_header, api_key=self.api_key, secret=self.secret) + 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, -10))) - with self.assertRaises(session_token.SessionTokenError, msg="Expird") as cm: - session_token.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + 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.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret) self.assertEqual("Invalid audience", str(cm.exception)) @@ -67,7 +85,7 @@ 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.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret) self.assertEqual("Invalid issuer", str(cm.exception)) @@ -75,12 +93,12 @@ 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.get_decoded_session_token(self.build_auth_header(), api_key=self.api_key, secret=self.secret) + 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.get_decoded_session_token( + decoded_payload = session_token.decode_from_header( self.build_auth_header(), api_key=self.api_key, secret=self.secret ) From 984849ab33b37e9c6318175e350c75f3cdd5c712 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 9 Mar 2021 17:39:49 -0500 Subject: [PATCH 156/259] Create sanitize_shop_domain utility method --- shopify/session_token.py | 7 +++--- shopify/utils/__init__.py | 0 shopify/utils/shop_url.py | 20 +++++++++++++++++ test/utils/shop_url_test.py | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 shopify/utils/__init__.py create mode 100644 shopify/utils/shop_url.py create mode 100644 test/utils/shop_url_test.py diff --git a/shopify/session_token.py b/shopify/session_token.py index 6ab8c17c..19f3105b 100644 --- a/shopify/session_token.py +++ b/shopify/session_token.py @@ -3,15 +3,14 @@ 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 -HOSTNAME_PATTERN = r"[a-z0-9][a-z0-9-]*[a-z0-9]" -SHOP_DOMAIN_RE = re.compile(r"^https://{h}\.myshopify\.com/$".format(h=HOSTNAME_PATTERN)) - ALGORITHM = "HS256" PREFIX = "Bearer " REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] @@ -69,7 +68,7 @@ def _validate_issuer(decoded_payload): def _validate_issuer_hostname(decoded_payload): issuer_root = urljoin(decoded_payload["iss"], "/") - if not SHOP_DOMAIN_RE.match(issuer_root): + if not shop_url.sanitize_shop_domain(issuer_root): raise InvalidIssuerError("Invalid issuer") 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..4bf2d1ab --- /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).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/test/utils/shop_url_test.py b/test/utils/shop_url_test.py new file mode 100644 index 00000000..6ec95e2a --- /dev/null +++ b/test/utils/shop_url_test.py @@ -0,0 +1,44 @@ +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)) From 22e7fcb2731800d9d064546babf6da53765fabe2 Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Wed, 17 Mar 2021 18:09:56 -0400 Subject: [PATCH 157/259] Add issue templates --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 39 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/ENHANCEMENT.md | 26 +++++++++++++++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md | 30 +++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md create mode 100644 .github/ISSUE_TEMPLATE/ENHANCEMENT.md create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md 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) From 227a56cee140617a089cd3e22efcf2fcc8cfe8e3 Mon Sep 17 00:00:00 2001 From: Rezaan Syed Date: Thu, 11 Mar 2021 16:46:14 -0500 Subject: [PATCH 158/259] Introduce ApiAccess for representing access scopes --- README.md | 73 +++++-------------- docs/api-access.md | 73 +++++++++++++++++++ docs/session-tokens.md | 54 ++++++++++++++ shopify/__init__.py | 1 + shopify/api_access.py | 52 ++++++++++++++ test/api_access_test.py | 153 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 351 insertions(+), 55 deletions(-) create mode 100644 docs/api-access.md create mode 100644 docs/session-tokens.md create mode 100644 shopify/api_access.py create mode 100644 test/api_access_test.py diff --git a/README.md b/README.md index 5a2cac93..6dc85ff6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,24 @@ To easily install or upgrade to the latest release, use [pip](http://www.pip-ins pip install --upgrade ShopifyAPI ``` +### Table of Contents + +- [Getting Started](#getting-started) + - [Public and Custom Apps](#public-and-custom-apps) + - [Private Apps](#private-apps) +- [Billing](#billing) +- [Session Tokens](docs/session-tokens) +- [Handling Access Scope Operations](docs/api-access.md) +- [Advanced Usage](#advanced-usage) +- [Prefix Options](#prefix-options) +- [Console](#console) +- [GraphQL](#graphql) +- [Using Development Version](#using-development-version) +- [Relative Cursor Pagination](#relative-cursor-pagination) +- [Limitations](#limitations) +- [Additional Resources](#additional-resources) + + ### Getting Started #### Public and Custom Apps @@ -120,61 +138,6 @@ _Note: Your application must be public to test the billing process. To test on a has_been_billed = activated_charge.status == 'active' ``` -### 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() - ... -``` - ### Advanced Usage It is recommended to have at least a basic grasp on the principles of the [pyactiveresource](https://github.com/Shopify/pyactiveresource) library, which is a port of rails/ActiveResource to Python and upon which this package relies heavily. diff --git a/docs/api-access.md b/docs/api-access.md new file mode 100644 index 00000000..41de61db --- /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_delmited_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_paramer(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/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..35495a4b --- /dev/null +++ b/shopify/api_access.py @@ -0,0 +1,52 @@ +import re + + +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 type(scopes) == str: + 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/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))) From c7238b3484bc3de9b1cdccd3f6c57fd07cd3c699 Mon Sep 17 00:00:00 2001 From: Rezaan Syed Date: Thu, 11 Mar 2021 16:46:14 -0500 Subject: [PATCH 159/259] Add access_scopes attribute to Session --- shopify/api_access.py | 11 +++++++++-- shopify/session.py | 20 ++++++++++++++++++-- test/session_test.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/shopify/api_access.py b/shopify/api_access.py index 35495a4b..d5ffbe35 100644 --- a/shopify/api_access.py +++ b/shopify/api_access.py @@ -1,4 +1,12 @@ 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): @@ -12,7 +20,7 @@ class ApiAccess: IMPLIED_SCOPE_RE = re.compile(r"\A(?Punauthenticated_)?write_(?P.*)\Z") def __init__(self, scopes): - if type(scopes) == str: + if isinstance(scopes, basestring_type()): scopes = scopes.split(self.SCOPE_DELIMITER) self.__store_scopes(scopes) @@ -32,7 +40,6 @@ def __eq__(self, other): 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) diff --git a/shopify/session.py b/shopify/session.py index 7e1c13ac..39ce5f7b 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -10,6 +10,7 @@ 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 @@ -45,10 +46,11 @@ 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): @@ -72,7 +74,10 @@ def request_token(self, params): 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) @@ -89,6 +94,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() == ""): diff --git a/test/session_test.py b/test/session_test.py index 60fb6678..806d551b 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -208,7 +208,7 @@ def test_param_validation_of_param_values_with_lists(self): } self.assertEqual(True, shopify.Session.validate_hmac(params)) - def test_return_token_if_hmac_is_valid(self): + 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) @@ -218,12 +218,13 @@ def test_return_token_if_hmac_is_valid(self): None, url="https://localhost.myshopify.com/admin/oauth/access_token", method="POST", - body='{"access_token" : "token"}', + 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" @@ -257,6 +258,32 @@ def test_raise_error_if_timestamp_is_too_old(self): 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("&"))) From c28d6147e874fb5fcf213fc629dc4443b0689c4c Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Wed, 17 Mar 2021 11:33:55 -0400 Subject: [PATCH 160/259] Release v8.3.0 --- CHANGELOG | 7 +++++++ shopify/version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6d863d7d..13e69e4f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +== 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)) diff --git a/shopify/version.py b/shopify/version.py index c0a494d8..f11dae2f 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.2.0" +VERSION = "8.3.0" From e270654cd50aec66b45135832bba5ac9c3d11448 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Mon, 22 Mar 2021 11:24:28 -0400 Subject: [PATCH 161/259] Add shopify/utils to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c0a935b4..cbd2a336 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ author="Shopify", author_email="developers@shopify.com", url="https://github.com/Shopify/shopify_python_api", - packages=["shopify", "shopify/resources"], + packages=["shopify", "shopify/resources", "shopify/utils"], scripts=["scripts/shopify_api.py"], license="MIT License", install_requires=[ From 8ef678e94e2f60718d64ba44f62e387e8a0703e6 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Mon, 22 Mar 2021 11:35:03 -0400 Subject: [PATCH 162/259] Release v8.3.1 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 13e69e4f..e052fc58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== 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 diff --git a/shopify/version.py b/shopify/version.py index f11dae2f..783a142b 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.3.0" +VERSION = "8.3.1" From fe9afdd07bbb95921533217a6408f5976d3459ce Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 25 Mar 2021 13:46:34 -0400 Subject: [PATCH 163/259] Revert "Merge pull request #441 from smubbs/dynamic-api-version" This reverts commit e34d0954a41ae77018010f8413d8be99fe91dc49, reversing changes made to fa6bbd3b8cf89dd34725e2fa74102895df962175. --- shopify/api_version.py | 16 +++++----------- test/fixtures/api_version.json | 1 - test/test_helper.py | 20 +++++++------------- 3 files changed, 12 insertions(+), 25 deletions(-) delete mode 100644 test/fixtures/api_version.json diff --git a/shopify/api_version.py b/shopify/api_version.py index 44a8e9b6..6641b269 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -1,6 +1,4 @@ import re -import json -from six.moves.urllib import request class InvalidVersionError(Exception): @@ -28,15 +26,11 @@ 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("2020-01")) + cls.define_version(Release("2020-04")) + cls.define_version(Release("2020-07")) + cls.define_version(Release("2020-10")) @classmethod def clear_defined_versions(cls): diff --git a/test/fixtures/api_version.json b/test/fixtures/api_version.json deleted file mode 100644 index 14150e29..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}]}]} diff --git a/test/test_helper.py b/test/test_helper.py index 9f098a26..666ac792 100644 --- a/test/test_helper.py +++ b/test/test_helper.py @@ -20,15 +20,6 @@ def setUp(self): 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, - ) def load_fixture(self, name, format="json"): with open(os.path.dirname(__file__) + "/fixtures/%s.%s" % (name, format), "rb") as f: @@ -44,10 +35,13 @@ def fake(self, endpoint, **kwargs): 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) + + 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]) From 682a44a92dafb27c6818863120ed83f8f1f9d54c Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 25 Mar 2021 14:07:22 -0400 Subject: [PATCH 164/259] Update versions --- shopify/api_version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index 6641b269..0fcb4330 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -31,6 +31,8 @@ def define_known_versions(cls): cls.define_version(Release("2020-04")) cls.define_version(Release("2020-07")) cls.define_version(Release("2020-10")) + cls.define_version(Release("2021-01")) + cls.define_version(Release("2021-04")) @classmethod def clear_defined_versions(cls): From 52b4a6e06d4d028ca84b7e0a51d739d5b360e120 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 25 Mar 2021 14:10:22 -0400 Subject: [PATCH 165/259] Update changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e052fc58..060896ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +== Unreleased +- 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)) From 4a413a271e7ce5bd63b15fec1a625d3e8768214c Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 25 Mar 2021 14:31:31 -0400 Subject: [PATCH 166/259] Release v8.4.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 060896ff..0693f3c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ == Unreleased + +== Version 8.4.0 - Revert Feature #441 Dynamic API Versioning ([#495](https://github.com/Shopify/shopify_python_api/pull/495)) == Version 8.3.1 diff --git a/shopify/version.py b/shopify/version.py index 783a142b..65dc005e 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.3.1" +VERSION = "8.4.0" From ae54eb7eaa03574887f3170728aa25245c7cda87 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 25 Mar 2021 14:36:15 -0400 Subject: [PATCH 167/259] Add automated reminder for API updates --- .github/api_update_reminder.md | 26 +++++++++++++++++++++++ .github/workflows/api_update_reminder.yml | 16 ++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 .github/api_update_reminder.md create mode 100644 .github/workflows/api_update_reminder.yml diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md new file mode 100644 index 00000000..7a348e9f --- /dev/null +++ b/.github/api_update_reminder.md @@ -0,0 +1,26 @@ +--- +title: A new release of the Shopify API is due soon +assignees: andyw8 # @platform/dev-tools-education +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 Stable API version, and aim to release an update of the library within one week. + +Thank you! diff --git a/.github/workflows/api_update_reminder.yml b/.github/workflows/api_update_reminder.yml new file mode 100644 index 00000000..47b29b61 --- /dev/null +++ b/.github/workflows/api_update_reminder.yml @@ -0,0 +1,16 @@ +on: + workflow_dispatch: # to allow for manual testing + # 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 From adfb40b8e5c13bd9a5eb3522814042f1de01ab86 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 25 Mar 2021 15:39:08 -0400 Subject: [PATCH 168/259] Update .github/api_update_reminder.md Co-authored-by: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> --- .github/api_update_reminder.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md index 7a348e9f..c6f281d2 100644 --- a/.github/api_update_reminder.md +++ b/.github/api_update_reminder.md @@ -21,6 +21,6 @@ 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 Stable API version, and aim to release an update of the library within one week. +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! From b17aba987c36914c60f00c59a78ae9e956f275ee Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 26 Mar 2021 13:05:21 -0400 Subject: [PATCH 169/259] Add reminder for release day --- .github/api_update_reminder_on_release.md | 20 +++++++++++++++++++ .../api_update_reminder_on_release.yml | 15 ++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .github/api_update_reminder_on_release.md create mode 100644 .github/workflows/api_update_reminder_on_release.yml diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md new file mode 100644 index 00000000..b9d6dd51 --- /dev/null +++ b/.github/api_update_reminder_on_release.md @@ -0,0 +1,20 @@ +--- +title: A new release of the Shopify API occurred +assignees: @platform/dev-tools-education +labels: automated +--- + +This is an automated reminder for the maintainers that a new Stable release of the Shopify API was scheduled for today, +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 releaesd + +Aim to release an update of the library within one week. + +Thank you! 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..1be466f5 --- /dev/null +++ b/.github/workflows/api_update_reminder_on_release.yml @@ -0,0 +1,15 @@ +on: + schedule: + - cron: "0 0 1 1,4,7,10 *" # At 00:00 on 1st of January, April, July, and October + +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_on_release.md From ab9b4b9eeac74fc6a836efeb71b5df1e60aa9d6e Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 26 Mar 2021 13:07:34 -0400 Subject: [PATCH 170/259] Enable the cron schedule --- .github/api_update_reminder.md | 2 +- .github/workflows/api_update_reminder.yml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md index c6f281d2..38dcddc8 100644 --- a/.github/api_update_reminder.md +++ b/.github/api_update_reminder.md @@ -1,6 +1,6 @@ --- title: A new release of the Shopify API is due soon -assignees: andyw8 # @platform/dev-tools-education +assignees: @platform/dev-tools-education labels: automated --- diff --git a/.github/workflows/api_update_reminder.yml b/.github/workflows/api_update_reminder.yml index 47b29b61..b2242968 100644 --- a/.github/workflows/api_update_reminder.yml +++ b/.github/workflows/api_update_reminder.yml @@ -1,7 +1,6 @@ on: - workflow_dispatch: # to allow for manual testing - # schedule: - # - cron: "0 0 1 3,6,9,12 *" # At 00:00 on 1st of March, June, September, and December + 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: From e50d34ba44e9e39c79baa6ba23a673b003290ad9 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Fri, 26 Mar 2021 11:22:38 -0400 Subject: [PATCH 171/259] Return None when sanitizing shop domain if None passed in --- shopify/utils/shop_url.py | 2 +- test/utils/shop_url_test.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/shopify/utils/shop_url.py b/shopify/utils/shop_url.py index 4bf2d1ab..76e088a7 100644 --- a/shopify/utils/shop_url.py +++ b/shopify/utils/shop_url.py @@ -10,7 +10,7 @@ def sanitize_shop_domain(shop_domain, myshopify_domain="myshopify.com"): - name = str(shop_domain).lower().strip() + 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) diff --git a/test/utils/shop_url_test.py b/test/utils/shop_url_test.py index 6ec95e2a..ea7a77e9 100644 --- a/test/utils/shop_url_test.py +++ b/test/utils/shop_url_test.py @@ -42,3 +42,6 @@ def test_returns_hostname_for_custom_shop_domains(self): ] 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)) From b3df5de2db97dce0808d586e7a3046982c145f8d Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Mon, 29 Mar 2021 09:40:25 -0400 Subject: [PATCH 172/259] Release v8.4.1 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 0693f3c5..db415d1a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ == Unreleased +== 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)) diff --git a/shopify/version.py b/shopify/version.py index 65dc005e..e86d6cd0 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.4.0" +VERSION = "8.4.1" From 90a42276ff1662e120a01098199b4530f11736bb Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 1 Apr 2021 09:25:12 -0400 Subject: [PATCH 173/259] Fix API update reminder action --- .github/api_update_reminder.md | 2 +- .github/api_update_reminder_on_release.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md index 38dcddc8..6d8fd54f 100644 --- a/.github/api_update_reminder.md +++ b/.github/api_update_reminder.md @@ -1,6 +1,6 @@ --- title: A new release of the Shopify API is due soon -assignees: @platform/dev-tools-education +assignees: Shopify/platform-dev-tools-education labels: automated --- diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md index b9d6dd51..10527ca2 100644 --- a/.github/api_update_reminder_on_release.md +++ b/.github/api_update_reminder_on_release.md @@ -1,6 +1,6 @@ --- title: A new release of the Shopify API occurred -assignees: @platform/dev-tools-education +assignees: Shopify/platform-dev-tools-education labels: automated --- From 4812b246d20df43a01ae40cc00ccf20a1fd95210 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 1 Apr 2021 10:03:46 -0400 Subject: [PATCH 174/259] Allow manual trigger of workflow --- .github/api_update_reminder.md | 2 +- .github/api_update_reminder_on_release.md | 2 +- .github/workflows/api_update_reminder.yml | 1 + .github/workflows/api_update_reminder_on_release.yml | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md index 6d8fd54f..2409401b 100644 --- a/.github/api_update_reminder.md +++ b/.github/api_update_reminder.md @@ -1,6 +1,6 @@ --- title: A new release of the Shopify API is due soon -assignees: Shopify/platform-dev-tools-education +assignees: "Shopify/platform-dev-tools-education" labels: automated --- diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md index 10527ca2..e119cbf0 100644 --- a/.github/api_update_reminder_on_release.md +++ b/.github/api_update_reminder_on_release.md @@ -1,6 +1,6 @@ --- title: A new release of the Shopify API occurred -assignees: Shopify/platform-dev-tools-education +assignees: "Shopify/platform-dev-tools-education" labels: automated --- diff --git a/.github/workflows/api_update_reminder.yml b/.github/workflows/api_update_reminder.yml index b2242968..b4cfb456 100644 --- a/.github/workflows/api_update_reminder.yml +++ b/.github/workflows/api_update_reminder.yml @@ -1,4 +1,5 @@ on: + workflow_dispatch: ~ schedule: - cron: "0 0 1 3,6,9,12 *" # At 00:00 on 1st of March, June, September, and December diff --git a/.github/workflows/api_update_reminder_on_release.yml b/.github/workflows/api_update_reminder_on_release.yml index 1be466f5..aaf04e09 100644 --- a/.github/workflows/api_update_reminder_on_release.yml +++ b/.github/workflows/api_update_reminder_on_release.yml @@ -1,4 +1,5 @@ on: + workflow_dispatch: ~ schedule: - cron: "0 0 1 1,4,7,10 *" # At 00:00 on 1st of January, April, July, and October From 2e8ef54d0e30b04a0c3c3ca5ce2e93b9a103c565 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 1 Apr 2021 10:42:48 -0400 Subject: [PATCH 175/259] Remove assignees field --- .github/api_update_reminder.md | 1 - .github/api_update_reminder_on_release.md | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/api_update_reminder.md b/.github/api_update_reminder.md index 2409401b..1e7927ed 100644 --- a/.github/api_update_reminder.md +++ b/.github/api_update_reminder.md @@ -1,6 +1,5 @@ --- title: A new release of the Shopify API is due soon -assignees: "Shopify/platform-dev-tools-education" labels: automated --- diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md index e119cbf0..a24fe1f4 100644 --- a/.github/api_update_reminder_on_release.md +++ b/.github/api_update_reminder_on_release.md @@ -1,6 +1,5 @@ --- title: A new release of the Shopify API occurred -assignees: "Shopify/platform-dev-tools-education" labels: automated --- From 2db357ac697b96f4aca8021b29f3452a304b2f0c Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 1 Apr 2021 10:57:36 -0400 Subject: [PATCH 176/259] Adjust wording --- .github/api_update_reminder_on_release.md | 4 ++-- .github/workflows/api_update_reminder_on_release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md index a24fe1f4..9f7b1c81 100644 --- a/.github/api_update_reminder_on_release.md +++ b/.github/api_update_reminder_on_release.md @@ -3,8 +3,8 @@ 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 was scheduled for today, -so a new release of the library is now due. +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. diff --git a/.github/workflows/api_update_reminder_on_release.yml b/.github/workflows/api_update_reminder_on_release.yml index aaf04e09..5e3053f9 100644 --- a/.github/workflows/api_update_reminder_on_release.yml +++ b/.github/workflows/api_update_reminder_on_release.yml @@ -3,7 +3,7 @@ on: schedule: - cron: "0 0 1 1,4,7,10 *" # At 00:00 on 1st of January, April, July, and October -name: API update reminder +name: API update reminder on release jobs: reminder: runs-on: ubuntu-latest From b4b6e803182c5995dbc6b2dcb5a60c8fe9ac8577 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 1 Apr 2021 11:19:16 -0400 Subject: [PATCH 177/259] Fix typo in issue content --- .github/api_update_reminder_on_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/api_update_reminder_on_release.md b/.github/api_update_reminder_on_release.md index e119cbf0..3543b764 100644 --- a/.github/api_update_reminder_on_release.md +++ b/.github/api_update_reminder_on_release.md @@ -13,7 +13,7 @@ Review the changelog again and consider if anything in the library needs to chan https://shopify.dev/changelog -Test against the new release by using the Stable version just releaesd +Test against the new release by using the Stable version just released. Aim to release an update of the library within one week. From f52d07e905c3431ab95e213da7d0f2fd0da09429 Mon Sep 17 00:00:00 2001 From: Nabeel Ahsen Date: Tue, 13 Apr 2021 15:23:58 -0400 Subject: [PATCH 178/259] Add a sample Django app to README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6dc85ff6..a2beec0f 100644 --- a/README.md +++ b/README.md @@ -234,3 +234,6 @@ Currently there is no support for: * [developers.shopify.com](https://developers.shopify.com) * [Shopify.dev](https://shopify.dev) * [Ask questions on the Shopify forums](http://ecommerce.shopify.com/c/shopify-apis-and-technology) + +### Sample apps built using this library +* [Sample Django app](https://github.com/shopify/sample-django-app) From a8e488332ff86bde5a37f782ae4bcb4a08358c9a Mon Sep 17 00:00:00 2001 From: Paulina Khew Date: Mon, 26 Apr 2021 16:01:02 -0400 Subject: [PATCH 179/259] Update build.yml --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77870e16..ea9c6395 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,5 @@ jobs: - name: upload coverage to Codecov uses: codecov/codecov-action@v1 with: - token: ${{ secrets.CODECOV_TOKEN }} name: codecov-umbrella fail_ci_if_error: true From 56cefc65a497074f47e85c46fb7e9480cf5f6177 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 29 Apr 2021 13:06:17 -0400 Subject: [PATCH 180/259] Add dependabot --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..88afc92a --- /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: '*' From e83fac61803218aec713845f6f1c9c0c723e6de9 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Thu, 29 Apr 2021 13:15:35 -0400 Subject: [PATCH 181/259] Error in tabs --- .github/dependabot.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 88afc92a..4af31a55 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,13 +1,13 @@ version: 2 updates: -- package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - time: "00:00" - timezone: "UTC" - reviewers: - - Shopify/core-build-learn + - 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 From 1c53d75e15cd660b70f887e42114dd9d4832040c Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 1 Jun 2021 10:27:13 -0400 Subject: [PATCH 182/259] Update 2021-07 version --- shopify/api_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/api_version.py b/shopify/api_version.py index 0fcb4330..8deac0e8 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,12 +27,12 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2020-01")) cls.define_version(Release("2020-04")) cls.define_version(Release("2020-07")) cls.define_version(Release("2020-10")) cls.define_version(Release("2021-01")) cls.define_version(Release("2021-04")) + cls.define_version(Release("2021-07")) @classmethod def clear_defined_versions(cls): From f29ef11fc2dc20fa596ae905c4613ce5d925c70a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 12 Jul 2021 09:16:37 -0400 Subject: [PATCH 183/259] Update Changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index db415d1a..901b9e34 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- 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)) From a582012f84fa863c2cdaf2085765b9b8b9578370 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 12 Jul 2021 12:56:48 -0400 Subject: [PATCH 184/259] Release v8.4.2 --- CHANGELOG | 1 + shopify/version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 901b9e34..65b6b8c0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ == Unreleased +== 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 diff --git a/shopify/version.py b/shopify/version.py index e86d6cd0..f33ca55d 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.4.1" +VERSION = "8.4.2" From 0014881a39d2640819da4dde193cae61f701a519 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 16 Sep 2021 06:26:50 +1000 Subject: [PATCH 185/259] docs: Fix a few typos There are small typos in: - README.md - docs/api-access.md - scripts/shopify_api.py Fixes: - Should read `unambiguous` rather than `unambigious`. - Should read `received` rather than `recieved`. - Should read `delimited` rather than `delmited`. - Should read `parameter` rather than `paramer`. --- README.md | 2 +- docs/api-access.md | 4 ++-- scripts/shopify_api.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6dc85ff6..aac8a503 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ pip install --upgrade ShopifyAPI # 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) diff --git a/docs/api-access.md b/docs/api-access.md index 41de61db..52424a24 100644 --- a/docs/api-access.md +++ b/docs/api-access.md @@ -24,7 +24,7 @@ another_api_access = ApiAccess("read_products, write_products, unauthenticated_r api_access = ApiAccess(["read_products", "write_orders", "unauthenticated_read_themes"]) access_scopes_list = list(api_access) # ["read_products", "write_orders", "unauthenticated_read_themes"] -comma_delmited_access_scopes = str(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 @@ -60,7 +60,7 @@ from shopify import ApiAccess def oauth_on_access_scopes_mismatch(func): def wrapper(*args, **kwargs): - shop_domain = get_shop_query_paramer(request) # shop query param when loading app + 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) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 355987ad..141f4bee 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -58,7 +58,7 @@ def run_task(cls, task=None, *args): 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) list_of_matches = list(matches) From 6ce668c331572113436942cb881d9df928842b0e Mon Sep 17 00:00:00 2001 From: David Lindquist Date: Mon, 4 Oct 2021 18:36:58 -0700 Subject: [PATCH 186/259] Fix doc link to Session Tokens The link did not include needed ".md" suffix. https://github.com/Shopify/shopify_python_api/blob/master/docs/session-tokens -> https://github.com/Shopify/shopify_python_api/blob/master/docs/session-tokens.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dc85ff6..94fb864d 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pip install --upgrade ShopifyAPI - [Public and Custom Apps](#public-and-custom-apps) - [Private Apps](#private-apps) - [Billing](#billing) -- [Session Tokens](docs/session-tokens) +- [Session Tokens](docs/session-tokens.md) - [Handling Access Scope Operations](docs/api-access.md) - [Advanced Usage](#advanced-usage) - [Prefix Options](#prefix-options) From 4991e6c43a8be2e410504ee042e1a886a673694b Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 15 Oct 2021 09:46:00 -0400 Subject: [PATCH 187/259] Drop Python 2 support --- .github/workflows/build.yml | 2 +- CHANGELOG | 2 ++ setup.py | 5 +---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea9c6395..b45a1dfc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [2.7, 3.6, 3.8, 3.9] + version: [3.6, 3.10.0] steps: - name: Checkout diff --git a/CHANGELOG b/CHANGELOG index 65b6b8c0..6f3884f0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- Drop Python 2 support ([#549](https://github.com/Shopify/shopify_python_api/pull/549)) + == 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)) diff --git a/setup.py b/setup.py index cbd2a336..a798cb78 100755 --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ license="MIT License", install_requires=[ "pyactiveresource>=2.2.2", - "PyJWT <= 1.7.1; python_version == '2.7'", - "PyJWT >= 2.0.0; python_version >= '3.6'", + "PyJWT >= 2.0.0", "PyYAML", "six", ], @@ -39,8 +38,6 @@ "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", From 8fcac316c5bb4d927811237e30d9754fea600552 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 15 Oct 2021 10:02:26 -0400 Subject: [PATCH 188/259] Update to 2021-10 release --- CHANGELOG | 1 + shopify/api_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6f3884f0..15ca265e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ == Unreleased - 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)) diff --git a/shopify/api_version.py b/shopify/api_version.py index 8deac0e8..ce43ab13 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,12 +27,12 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2020-04")) cls.define_version(Release("2020-07")) cls.define_version(Release("2020-10")) cls.define_version(Release("2021-01")) cls.define_version(Release("2021-04")) cls.define_version(Release("2021-07")) + cls.define_version(Release("2021-10")) @classmethod def clear_defined_versions(cls): From 935c71d008d5f343f116725f715864a6e317eea1 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 15 Oct 2021 11:48:35 -0400 Subject: [PATCH 189/259] Release v9.0.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 15ca265e..840a6043 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +== 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)) diff --git a/shopify/version.py b/shopify/version.py index f33ca55d..6039f61e 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "8.4.2" +VERSION = "9.0.0" From 2074a7acef03e777107c52e82bb2c12e063b270a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 4 Jan 2022 11:25:14 -0500 Subject: [PATCH 190/259] Add 2022-01 to api version --- CHANGELOG | 2 ++ shopify/api_version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 840a6043..cfcc8512 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ == Unreleased +- Update API version with 2022-01 release, remove API version 2020-07 + == Version 9.0.0 diff --git a/shopify/api_version.py b/shopify/api_version.py index ce43ab13..deeb9181 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,12 +27,12 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2020-07")) cls.define_version(Release("2020-10")) cls.define_version(Release("2021-01")) cls.define_version(Release("2021-04")) cls.define_version(Release("2021-07")) cls.define_version(Release("2021-10")) + cls.define_version(Release("2022-01")) @classmethod def clear_defined_versions(cls): From b9d89e7a2157b7dc9eaa768dca1c508ff54d3a2c Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 5 Jan 2022 11:09:47 -0500 Subject: [PATCH 191/259] Release v10.0.0 --- CHANGELOG | 3 ++- shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cfcc8512..1810b146 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ == Unreleased -- Update API version with 2022-01 release, remove API version 2020-07 +== Version 10.0.0 +- Update API version with 2022-01 release, remove API version 2020-07 == Version 9.0.0 diff --git a/shopify/version.py b/shopify/version.py index 6039f61e..b91ab216 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "9.0.0" +VERSION = "10.0.0" From 9d90302aa6557b429026a62993ea9d28950a012e Mon Sep 17 00:00:00 2001 From: Jonathan Sphar Date: Fri, 25 Feb 2022 12:38:08 -0800 Subject: [PATCH 192/259] Add operation_name param to Graphql.execute, with test and readme example --- README.md | 39 ++++++++++++++++++++++++++++++++++++ shopify/resources/graphql.py | 4 ++-- test/graphql_test.py | 20 ++++++++++++++---- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index acbc252c..32d209f3 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,45 @@ This library also supports Shopify's new [GraphQL API](https://help.shopify.com/ 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 chose 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 diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py index c8110ead..33525ef1 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -15,11 +15,11 @@ 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"} 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) diff --git a/test/graphql_test.py b/test/graphql_test.py index 50f89416..dc32b935 100644 --- a/test/graphql_test.py +++ b/test/graphql_test.py @@ -9,7 +9,7 @@ def setUp(self): shopify.ApiVersion.define_known_versions() 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", @@ -20,6 +20,8 @@ def setUp(self): "Content-Type": "application/json", }, ) + + def test_fetch_shop_with_graphql(self): query = """ { shop { @@ -28,7 +30,17 @@ def setUp(self): } } """ - 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") + 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") From 7b4b43522a569c45da423937cf8596a994b04917 Mon Sep 17 00:00:00 2001 From: Jonathan Sphar Date: Wed, 2 Mar 2022 10:35:10 -0800 Subject: [PATCH 193/259] Run pre-commit hook to strip trailing spaces --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 32d209f3..7808e317 100644 --- a/README.md +++ b/README.md @@ -183,9 +183,9 @@ This library also supports Shopify's new [GraphQL API](https://help.shopify.com/ result = shopify.GraphQL().execute('{ shop { name id } }') ``` -You can perform more complex operations using the `variables` and `operation_name` parameters of `execute`. +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: +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 @@ -195,13 +195,13 @@ For example, this GraphQL document uses a fragment to construct two named querie name createdAt } - + query GetOneOrder($order_id: ID!){ node(id: $order_id){ ...OrderInfo } } - + query GetManyOrders($order_ids: [ID]!){ nodes(ids: $order_ids){ ...OrderInfo @@ -209,7 +209,7 @@ For example, this GraphQL document uses a fragment to construct two named querie } ``` -Now you can chose which operation to execute: +Now you can chose which operation to execute: ```python # Load the document with both queries From 6a3d6b09c6b3d391c8d822592fa1228f6671033a Mon Sep 17 00:00:00 2001 From: Jonathan Sphar <80364060+JJSphar@users.noreply.github.com> Date: Fri, 11 Mar 2022 14:13:12 -0800 Subject: [PATCH 194/259] Fix typo in README graphql example Co-authored-by: Melanie Wang --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7808e317..a68764d5 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ For example, this GraphQL document uses a fragment to construct two named querie ```graphql # ./order_queries.graphql - fragment orderInfo on Order { + fragment OrderInfo on Order { id name createdAt From 1a3534358f653e14c2e95f88c2737f12efe17250 Mon Sep 17 00:00:00 2001 From: Jonathan Sphar Date: Mon, 14 Mar 2022 10:35:34 -0700 Subject: [PATCH 195/259] Use PascalCase for GraphQL fragments --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a68764d5..d473a015 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ For example, this GraphQL document uses a fragment to construct two named querie } ``` -Now you can chose which operation to execute: +Now you can choose which operation to execute: ```python # Load the document with both queries From d3caebfaca0fa27d152f24134833b915afcdc279 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 4 Apr 2022 11:40:33 -0400 Subject: [PATCH 196/259] Add version 2022-04, remove 2020-10 --- CHANGELOG | 1 + shopify/api_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 1810b146..fd1068f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ == Unreleased +- Update API version with 2022-04 release, remove API version 2020-10 == Version 10.0.0 - Update API version with 2022-01 release, remove API version 2020-07 diff --git a/shopify/api_version.py b/shopify/api_version.py index deeb9181..079b87c3 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,12 +27,12 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2020-10")) cls.define_version(Release("2021-01")) cls.define_version(Release("2021-04")) cls.define_version(Release("2021-07")) cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) + cls.define_version(Release("2022-04")) @classmethod def clear_defined_versions(cls): From e0f5b173c067954c688648ba4d1e7256f42b41f7 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Mon, 4 Apr 2022 14:22:42 -0400 Subject: [PATCH 197/259] Update black version --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6aec2f5b..f8b66bfe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black - repo: https://github.com/PyCQA/pylint From 77e6ef40d83e417aaeaffd46a843c518636e8552 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Apr 2022 10:56:15 -0400 Subject: [PATCH 198/259] Apply black formatting --- shopify/yamlobjects.py | 1 - test/base_test.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/shopify/yamlobjects.py b/shopify/yamlobjects.py index d050877f..c7438c42 100644 --- a/shopify/yamlobjects.py +++ b/shopify/yamlobjects.py @@ -15,6 +15,5 @@ class YAMLHashWithIndifferentAccess(yaml.YAMLObject): def from_yaml(cls, loader, node): return loader.construct_mapping(node, cls) - except ImportError: pass diff --git a/test/base_test.py b/test/base_test.py index a07368b6..5817872e 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -107,7 +107,7 @@ def test_setting_with_user_and_pass_strips_them(self): url="https://this-is-my-test-show.myshopify.com/admin/shop.json", method="GET", body=self.load_fixture("shop"), - headers={"Authorization": u"Basic dXNlcjpwYXNz"}, + headers={"Authorization": "Basic dXNlcjpwYXNz"}, ) API_KEY = "user" PASSWORD = "pass" From 18bb653aeff9b1fcd1b3eefdc17507d463109c1e Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Apr 2022 10:59:18 -0400 Subject: [PATCH 199/259] Remove support for version 2021-01,04 --- CHANGELOG | 3 ++- shopify/api_version.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fd1068f7..9f0ebaaa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ == Unreleased -- Update API version with 2022-04 release, remove API version 2020-10 +- 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 diff --git a/shopify/api_version.py b/shopify/api_version.py index 079b87c3..b3a703e7 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,8 +27,6 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2021-01")) - cls.define_version(Release("2021-04")) cls.define_version(Release("2021-07")) cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) From 8efdafd7a57aad782e7e93b5b16cde47d350da2a Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Tue, 5 Apr 2022 11:23:57 -0400 Subject: [PATCH 200/259] Release v11.0.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 9f0ebaaa..ed4a7f8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ == Unreleased + +== 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 diff --git a/shopify/version.py b/shopify/version.py index b91ab216..cbbc18cb 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "10.0.0" +VERSION = "11.0.0" From e0a57e631f6dc5abce69c7146fcafbff42a31835 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Mon, 4 Jul 2022 13:47:40 -0400 Subject: [PATCH 201/259] Update API version --- CHANGELOG | 2 ++ dev.yml | 2 +- shopify/api_version.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ed4a7f8d..8fade131 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- Update API version with 2022-04 release, remove API version 2021-07 + == 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 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/shopify/api_version.py b/shopify/api_version.py index b3a703e7..72740b73 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,10 +27,10 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2021-07")) cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) cls.define_version(Release("2022-04")) + cls.define_version(Release("2022-07")) @classmethod def clear_defined_versions(cls): From c10efe3cc51edb6fd3edc52502b6615e7e8ff173 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Mon, 4 Jul 2022 14:02:10 -0400 Subject: [PATCH 202/259] Release v12.0.0 --- CHANGELOG | 3 ++- shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8fade131..4a283388 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ == Unreleased -- Update API version with 2022-04 release, remove API version 2021-07 +== 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 diff --git a/shopify/version.py b/shopify/version.py index cbbc18cb..2a8345bc 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "11.0.0" +VERSION = "12.0.0" From e603e33f7969fdbc03e3389ccd129eef1965feb9 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Mon, 4 Jul 2022 14:12:13 -0400 Subject: [PATCH 203/259] Rename shipit file --- shipit.yml => shipit.pypi.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename shipit.yml => shipit.pypi.yml (100%) diff --git a/shipit.yml b/shipit.pypi.yml similarity index 100% rename from shipit.yml rename to shipit.pypi.yml From 517ed19bb1141b7755e4da69cd5b7f6115894d75 Mon Sep 17 00:00:00 2001 From: Yevhenii Huselietov Date: Sat, 16 Jul 2022 11:58:22 -0400 Subject: [PATCH 204/259] Remove CLA from probot and use new GitHub action --- .github/probots.yml | 2 -- .github/workflows/cla.yml | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) delete mode 100644 .github/probots.yml create mode 100644 .github/workflows/cla.yml 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/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..ecd71ce2 --- /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 }} From 15c01411798d59ab6d19467f27dc0e62b271df8e Mon Sep 17 00:00:00 2001 From: Steve Martinelli <4118756+stevemar@users.noreply.github.com> Date: Wed, 3 Aug 2022 12:40:23 -0400 Subject: [PATCH 205/259] remove whitespace --- .github/workflows/cla.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index ecd71ce2..2c3a4042 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -10,10 +10,10 @@ jobs: cla: runs-on: ubuntu-latest if: | - (github.event.issue.pull_request + (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 From 3c5354d85534edfc457dcc0175ba4446c2918ac7 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Wed, 21 Sep 2022 13:55:21 -0400 Subject: [PATCH 206/259] add workflows --- .../close-waiting-for-response-issues.yml | 20 ++++++++++++ .../workflows/remove-labels-on-activity.yml | 16 ++++++++++ .github/workflows/stale.yml | 31 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .github/workflows/close-waiting-for-response-issues.yml create mode 100644 .github/workflows/remove-labels-on-activity.yml create mode 100644 .github/workflows/stale.yml 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/remove-labels-on-activity.yml b/.github/workflows/remove-labels-on-activity.yml new file mode 100644 index 00000000..48872e95 --- /dev/null +++ b/.github/workflows/remove-labels-on-activity.yml @@ -0,0 +1,16 @@ +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..eeab0d6e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,31 @@ +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: 90 + days-before-issue-close: 14 + stale-issue-label: "Stale" + stale-issue-message: > + This issue is stale because it has been open for 90 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/master/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" From b280c629b68765b90bd0b78c788a8dfaee00cc74 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Tue, 27 Sep 2022 12:43:39 -0400 Subject: [PATCH 207/259] changed to 60 days --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index eeab0d6e..eae8f16f 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/stale@v5 with: - days-before-issue-stale: 90 + days-before-issue-stale: 60 days-before-issue-close: 14 stale-issue-label: "Stale" stale-issue-message: > From 19ab56da1cd5687a595a123166094ed89d6a5b48 Mon Sep 17 00:00:00 2001 From: xiao Date: Mon, 3 Oct 2022 09:25:30 +0900 Subject: [PATCH 208/259] Fix #600: Accept 5 seconds clock skew to avoid `ImmatureSignatureError` --- CHANGELOG | 1 + shopify/session_token.py | 4 ++++ test/session_token_test.py | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4a283388..bc0fa94b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ == Unreleased +- Fix: Accept 10 seconds clock skew to avoid `ImmatureSignatureError` == 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)) diff --git a/shopify/session_token.py b/shopify/session_token.py index 19f3105b..91a4970b 100644 --- a/shopify/session_token.py +++ b/shopify/session_token.py @@ -14,6 +14,7 @@ ALGORITHM = "HS256" PREFIX = "Bearer " REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"] +LEEWAY_SECONDS = 10 class SessionTokenError(Exception): @@ -54,6 +55,9 @@ def _decode_session_token(session_token, api_key, secret): 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: diff --git a/test/session_token_test.py b/test/session_token_test.py index 38e43808..f94fe0b2 100644 --- a/test/session_token_test.py +++ b/test/session_token_test.py @@ -48,7 +48,7 @@ def test_raises_if_token_authentication_header_is_not_bearer(self): 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, -10))) + 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) @@ -103,3 +103,8 @@ def test_returns_decoded_payload(self): ) 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) From 67b50d0bfea4e7388b938b1abaca41d7428c297a Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 17 Oct 2022 10:13:53 -0400 Subject: [PATCH 209/259] Format changelog --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bc0fa94b..7fa7a41d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ == Unreleased -- Fix: Accept 10 seconds clock skew to avoid `ImmatureSignatureError` +- 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)) From c43e6dd324d4cc538a016718d4df05f45595c605 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 17 Oct 2022 10:15:20 -0400 Subject: [PATCH 210/259] Release v12.0.1 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7fa7a41d..e4f2619f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ == Unreleased + +== Version 12.0.1 - Allow up to 10 seconds clock skew to avoid `ImmatureSignatureError` ([#609](https://github.com/Shopify/shopify_python_api/pull/609)) diff --git a/shopify/version.py b/shopify/version.py index 2a8345bc..08d8f853 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.0.0" +VERSION = "12.0.1" From e7bef94e1acc129d9b6611a95e42195d5f30cff1 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 17 Oct 2022 10:18:13 -0400 Subject: [PATCH 211/259] Bump python version for build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b45a1dfc..4c9bc93e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [3.6, 3.10.0] + version: [3.7, 3.10.0] steps: - name: Checkout From 298ddc9838305625d6abcf71593dc967a191f5a7 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Tue, 18 Oct 2022 15:38:16 -0400 Subject: [PATCH 212/259] lint --- .github/workflows/remove-labels-on-activity.yml | 1 - .github/workflows/stale.yml | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/remove-labels-on-activity.yml b/.github/workflows/remove-labels-on-activity.yml index 48872e95..948801e7 100644 --- a/.github/workflows/remove-labels-on-activity.yml +++ b/.github/workflows/remove-labels-on-activity.yml @@ -13,4 +13,3 @@ jobs: with: labels: | Waiting for Response - diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index eae8f16f..41e94f6d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,10 +16,10 @@ jobs: days-before-issue-close: 14 stale-issue-label: "Stale" stale-issue-message: > - This issue is stale because it has been open for 90 days with no activity. It will be closed if no further action occurs in 14 days. + This issue is stale because it has been open for 90 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. + 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/master/CONTRIBUTING.md) file for guidelines From 4c6910373e24f2ad123d5e4d216ea5bfa16fd39c Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Tue, 18 Oct 2022 15:44:51 -0400 Subject: [PATCH 213/259] change msg --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 41e94f6d..c194ded1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: days-before-issue-close: 14 stale-issue-label: "Stale" stale-issue-message: > - This issue is stale because it has been open for 90 days with no activity. It will be closed if no further action occurs in 14 days. + 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. From f7f7e92758cf495213ea663273f304c6e26ebe27 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Wed, 7 Dec 2022 09:16:44 -0500 Subject: [PATCH 214/259] add reason --- .github/workflows/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index c194ded1..6b670540 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -29,3 +29,4 @@ jobs: days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} exempt-issue-labels: "feature request" + close-issue-reason: "not_planned" \ No newline at end of file From 20bca2195530206719557461126ed7452a2e758a Mon Sep 17 00:00:00 2001 From: Dylan Thacker-Smith Date: Mon, 12 Dec 2022 09:58:05 -0500 Subject: [PATCH 215/259] Trigger CI on PR events --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c9bc93e..8127de2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,5 @@ name: CI -on: [push] +on: [push, pull_request] jobs: build: runs-on: ubuntu-latest From 2a2b0cd77023433d52e1763e5a3c58cdf40ba3b1 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Mon, 12 Dec 2022 14:25:26 -0500 Subject: [PATCH 216/259] Update 3.10.0 to 3.10 to trigger CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8127de2c..75aad488 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: [3.7, 3.10.0] + version: ["3.7", "3.10"] steps: - name: Checkout From 13ae8ed7bdf7908c9f21896a396325f59411ed90 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Mon, 12 Dec 2022 16:09:27 -0500 Subject: [PATCH 217/259] add new line --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 6b670540..cd1f94c0 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -29,4 +29,4 @@ jobs: days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} exempt-issue-labels: "feature request" - close-issue-reason: "not_planned" \ No newline at end of file + close-issue-reason: "not_planned" From a4670ebae6fb36af4bdfa684c250c91cff315a53 Mon Sep 17 00:00:00 2001 From: Bill Klenotiz Date: Mon, 12 Dec 2022 16:44:41 -0500 Subject: [PATCH 218/259] upgrade pylint --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8b66bfe..5adbcb8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,6 @@ repos: hooks: - id: black - repo: https://github.com/PyCQA/pylint - rev: pylint-2.7.2 + rev: pylint-2.15.8 hooks: - id: pylint From ebb55d6cb53742b622d0da8dc5b6d6538bc9ff95 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Tue, 13 Dec 2022 09:17:58 -0500 Subject: [PATCH 219/259] Update pylint version in CI --- .pre-commit-config.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8b66bfe..43267923 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +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 + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 hooks: - - id: end-of-file-fixer - - id: trailing-whitespace -- repo: https://github.com/psf/black + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black rev: 22.3.0 hooks: - - id: black -- repo: https://github.com/PyCQA/pylint - rev: pylint-2.7.2 + - id: black + - repo: https://github.com/PyCQA/pylint + rev: v2.15.8 hooks: - - id: pylint + - id: pylint From 8d248d653052ef07f88211c42ef179ea4bfa242b Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Tue, 13 Dec 2022 09:28:39 -0500 Subject: [PATCH 220/259] Fix linting errors --- shopify/base.py | 3 --- test/base_test.py | 3 --- test/marketing_event_test.py | 3 --- 3 files changed, 9 deletions(-) diff --git a/shopify/base.py b/shopify/base.py index 47f40cb3..449e288b 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -17,9 +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: diff --git a/test/base_test.py b/test/base_test.py index 5817872e..5cc19a60 100644 --- a/test/base_test.py +++ b/test/base_test.py @@ -17,9 +17,6 @@ def setUpClass(self): def tearDownClass(self): shopify.ApiVersion.clear_defined_versions() - def setUp(self): - super(BaseTest, self).setUp() - def tearDown(self): shopify.ShopifyResource.clear_session() diff --git a/test/marketing_event_test.py b/test/marketing_event_test.py index a3753d63..2f73029d 100644 --- a/test/marketing_event_test.py +++ b/test/marketing_event_test.py @@ -4,9 +4,6 @@ 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")) marketing_event = shopify.MarketingEvent.find(1) From 805892e3929bd11c7be27b08f00d1f62f49792e6 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 28 Oct 2022 16:36:14 +0530 Subject: [PATCH 221/259] feat: update API version to 2022-10 --- CHANGELOG | 1 + shopify/api_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e4f2619f..088dd17f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ == Unreleased +- Update API version with 2022-10 release, remove API version 2021-10 == Version 12.0.1 - Allow up to 10 seconds clock skew to avoid `ImmatureSignatureError` diff --git a/shopify/api_version.py b/shopify/api_version.py index 72740b73..5c506c31 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,10 +27,10 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) - cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) cls.define_version(Release("2022-04")) cls.define_version(Release("2022-07")) + cls.define_version(Release("2022-10")) @classmethod def clear_defined_versions(cls): From 37a639c8735d9b213c4b38249a7e5ad04f57ff0b Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Tue, 13 Dec 2022 13:54:34 -0500 Subject: [PATCH 222/259] Add 2022-10 back into api_version.py --- CHANGELOG | 2 +- shopify/api_version.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 088dd17f..c7abf352 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ == Unreleased -- Update API version with 2022-10 release, remove API version 2021-10 +- Add API version with 2022-10 release == Version 12.0.1 - Allow up to 10 seconds clock skew to avoid `ImmatureSignatureError` diff --git a/shopify/api_version.py b/shopify/api_version.py index 5c506c31..8beee194 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,6 +27,7 @@ def define_version(cls, version): @classmethod def define_known_versions(cls): cls.define_version(Unstable()) + cls.define_version(Release("2021-10")) cls.define_version(Release("2022-01")) cls.define_version(Release("2022-04")) cls.define_version(Release("2022-07")) From 72c9ecc4d3b62db709a267d57cece7376e5b8918 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Tue, 13 Dec 2022 14:01:02 -0500 Subject: [PATCH 223/259] Release v12.1.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c7abf352..76b2277f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ == Unreleased + +== Version 12.1.0 - Add API version with 2022-10 release == Version 12.0.1 diff --git a/shopify/version.py b/shopify/version.py index 08d8f853..910a54d5 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.0.1" +VERSION = "12.1.0" From c8906a380afd0480ac72dc1c62b70eca78121143 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Thu, 5 Jan 2023 09:42:06 -0500 Subject: [PATCH 224/259] Add support for version 2023-01 --- CHANGELOG | 282 +++++++++++++++++++++++------------------ shopify/api_version.py | 1 + 2 files changed, 162 insertions(+), 121 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 76b2277f..ab2b1e6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,20 +1,27 @@ == Unreleased +- Update API version with 2023-01 release ([#](https://github.com/Shopify/shopify_python_api/pull/)) + == 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)) + ([#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 @@ -23,18 +30,23 @@ - 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)) @@ -48,297 +60,325 @@ - 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)) + 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/shopify/api_version.py b/shopify/api_version.py index 8beee194..df2bc5bb 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -32,6 +32,7 @@ def define_known_versions(cls): cls.define_version(Release("2022-04")) cls.define_version(Release("2022-07")) cls.define_version(Release("2022-10")) + cls.define_version(Release("2023-01")) @classmethod def clear_defined_versions(cls): From b00e5061da834e74d49bd311b80760e755e99782 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Thu, 5 Jan 2023 11:39:00 -0500 Subject: [PATCH 225/259] Release v12.2.0 --- CHANGELOG | 4 +++- shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ab2b1e6b..0fede9d6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ == Unreleased -- Update API version with 2023-01 release ([#](https://github.com/Shopify/shopify_python_api/pull/)) +== 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 diff --git a/shopify/version.py b/shopify/version.py index 910a54d5..e77ed636 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.1.0" +VERSION = "12.2.0" From 4f21cbc843df44e41f1a3adc8b66fdbf65c3f762 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Wed, 12 Apr 2023 07:51:42 +0100 Subject: [PATCH 226/259] Remove unused cgi import --- CHANGELOG | 2 ++ shopify/collection.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0fede9d6..cacf47b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- Remove `cgi` import to avoid triggering a `DeprecationWarning` on Python 3.11. + == Version 12.2.0 - Update API version with 2023-01 release ([#631](https://github.com/Shopify/shopify_python_api/pull/631)) diff --git a/shopify/collection.py b/shopify/collection.py index 42bc8ebb..62728eb9 100644 --- a/shopify/collection.py +++ b/shopify/collection.py @@ -1,6 +1,4 @@ from pyactiveresource.collection import Collection -from six.moves.urllib.parse import urlparse, parse_qs -import cgi class PaginatedCollection(Collection): From 5f72f840f02db4e2bec9c094f4c9544bfc2a2ba7 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Wed, 12 Apr 2023 11:41:51 -0400 Subject: [PATCH 227/259] Add API release 2023-04 --- shopify/api_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/shopify/api_version.py b/shopify/api_version.py index df2bc5bb..049ee45c 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -33,6 +33,7 @@ def define_known_versions(cls): cls.define_version(Release("2022-07")) cls.define_version(Release("2022-10")) cls.define_version(Release("2023-01")) + cls.define_version(Release("2023-04")) @classmethod def clear_defined_versions(cls): From 91945a47639b85013f81223349fe61655dc5f8de Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Wed, 12 Apr 2023 11:43:53 -0400 Subject: [PATCH 228/259] Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 0fede9d6..796971ff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- 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)) From cfc417fbd5e9c268da92cd00a25a74b6b3ffec95 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Wed, 12 Apr 2023 11:46:16 -0400 Subject: [PATCH 229/259] Add 3.8, 3.9 and 3.11 to test matrix --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 75aad488..2a44a442 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,13 +6,13 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: ["3.7", "3.10"] + version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 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 From 8d8e01e0330c1d9e256f4eee73d4b8b5d2116cd3 Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Wed, 12 Apr 2023 11:52:12 -0400 Subject: [PATCH 230/259] Update setup.py with additional versions --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a798cb78..dae5a810 100755 --- a/setup.py +++ b/setup.py @@ -39,11 +39,11 @@ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", + "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", "Topic :: Software Development", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", From 5f295932bebbdde1835d35c4865093ff83564cdc Mon Sep 17 00:00:00 2001 From: Kevin O'Sullivan Date: Wed, 12 Apr 2023 12:10:00 -0400 Subject: [PATCH 231/259] Release v12.3.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 796971ff..868d99c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +== 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 diff --git a/shopify/version.py b/shopify/version.py index e77ed636..a48d177f 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.2.0" +VERSION = "12.3.0" From df252c81c47e993d3a07cb0bb62c643f483dec02 Mon Sep 17 00:00:00 2001 From: Lewis Morris <36413348+lewis-morris@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:51:35 +0100 Subject: [PATCH 232/259] Update README.md no imports listed in guide --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d473a015..cc61c076 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ pip install --upgrade ShopifyAPI 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: From 454481d70cfb8acc23ef67ddddaa097ecca9c88f Mon Sep 17 00:00:00 2001 From: Alessandro <72794815+alemrcc@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:05:17 +0100 Subject: [PATCH 233/259] I added a bit more information about the usage of the method find() and the usage of parameters since it took me a while to figure out how to do it. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d473a015..3f9aa737 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,12 @@ 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. From 5165407f50d7784c21c32d12e3b47c875647792d Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:22:02 -0300 Subject: [PATCH 234/259] Add support for missing API versions --- .github/workflows/pre-commit.yml | 2 +- CHANGELOG | 2 ++ shopify/api_version.py | 3 +++ test/session_token_test.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 54fc61d5..048bc5a2 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -12,6 +12,6 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Setup - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Pre-commit uses: pre-commit/action@v2.0.0 diff --git a/CHANGELOG b/CHANGELOG index 868d99c1..3fb31f2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +- 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)) diff --git a/shopify/api_version.py b/shopify/api_version.py index 049ee45c..ba137d57 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -34,6 +34,9 @@ def define_known_versions(cls): 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")) @classmethod def clear_defined_versions(cls): diff --git a/test/session_token_test.py b/test/session_token_test.py index f94fe0b2..0df7147f 100644 --- a/test/session_token_test.py +++ b/test/session_token_test.py @@ -79,7 +79,7 @@ def test_raises_if_aud_doesnt_match_api_key(self): 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("Invalid audience", str(cm.exception)) + self.assertEqual("Audience doesn't match", str(cm.exception)) def test_raises_if_issuer_hostname_is_invalid(self): self.payload["iss"] = "bad_shop_hostname" From 2b1561b9f4a08676410dee6299c6365bd0649b06 Mon Sep 17 00:00:00 2001 From: Melanie Wang Date: Wed, 10 Jan 2024 16:04:36 -0500 Subject: [PATCH 235/259] Release v12.4.0 --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 3fb31f2c..fb65b5a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == Unreleased +== 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 diff --git a/shopify/version.py b/shopify/version.py index a48d177f..7c16e69e 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.3.0" +VERSION = "12.4.0" From c1df76338fb779784b31a0c79c79d2faa0605446 Mon Sep 17 00:00:00 2001 From: Fai Date: Fri, 19 Jan 2024 23:31:32 +0800 Subject: [PATCH 236/259] Add ipython support for console interaction --- scripts/shopify_api.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 141f4bee..5dfab93a 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -16,10 +16,18 @@ 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 + try: + from IPython import start_ipython + from traitlets.config.loader import Config - console(variables).interact() + 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): From 65b2408d97e03e646de66094e61f2766b4bcdd52 Mon Sep 17 00:00:00 2001 From: Luke Singham Date: Wed, 24 Jan 2024 13:31:22 +0000 Subject: [PATCH 237/259] Add code highlighting code block --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d473a015..eb051f4c 100644 --- a/README.md +++ b/README.md @@ -242,7 +242,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() From 832a53d1f964004a677f29e3bc1727180883c21a Mon Sep 17 00:00:00 2001 From: Ju Liu Date: Tue, 13 Feb 2024 11:33:08 +0000 Subject: [PATCH 238/259] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d67ba2b..2329b18c 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,14 @@ pip install --upgrade ShopifyAPI ```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-01' state = binascii.b2a_hex(os.urandom(15)).decode("utf-8") redirect_uri = "http://myapp.com/auth/shopify/callback" scopes = ['read_products', 'read_orders'] From 8c2931fe535b183fe5616bb4b668a97b92f5dfff Mon Sep 17 00:00:00 2001 From: Ju Liu Date: Tue, 13 Feb 2024 11:34:28 +0000 Subject: [PATCH 239/259] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2329b18c..7dc84ed5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ pip install --upgrade ShopifyAPI ```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: From 81252346c63cb51d25ebdd1e0d44c85b68aa7479 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Mon, 8 Apr 2024 13:43:41 -0500 Subject: [PATCH 240/259] Add support for April24 API version --- CHANGELOG | 1 + shopify/api_version.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index c7309ca8..0a0c47e4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ == Unreleased - Remove `cgi` import to avoid triggering a `DeprecationWarning` on Python 3.11. +- Update API version with 2024-04 release. == Version 12.4.0 diff --git a/shopify/api_version.py b/shopify/api_version.py index ba137d57..a0a1531e 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -37,6 +37,7 @@ def define_known_versions(cls): cls.define_version(Release("2023-07")) cls.define_version(Release("2023-10")) cls.define_version(Release("2024-01")) + cls.define_version(Release("2024-04")) @classmethod def clear_defined_versions(cls): From 74373bfc93ddc2539b2bd442f5ea2876ec5345ec Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Mon, 8 Apr 2024 14:58:51 -0500 Subject: [PATCH 241/259] Update codecov action Testing to see if this resolves the pipeline failures --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a44a442..58a2a8cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: - name: Run Tests run: python -m pytest -v - name: upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: name: codecov-umbrella fail_ci_if_error: true From 8029a749cafeac18a95fe5546867a369a4c719c8 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Tue, 9 Apr 2024 09:53:00 -0500 Subject: [PATCH 242/259] turn off CI failure for codecov We are getting increasing failures because of this task --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58a2a8cb..e7ec6d36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: - name: Run Tests run: python -m pytest -v - name: upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v1 with: name: codecov-umbrella - fail_ci_if_error: true + fail_ci_if_error: false From 65f5ce2a6f30d49995c8af6ff52796f5a0ffe9cc Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Tue, 9 Apr 2024 10:28:01 -0500 Subject: [PATCH 243/259] Preparing for release --- CHANGELOG | 3 ++- shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0a0c47e4..c4462f6e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ == Unreleased +== Version 12.5.0 - Remove `cgi` import to avoid triggering a `DeprecationWarning` on Python 3.11. -- Update API version with 2024-04 release. +- Update API version with 2024-04 release.([710](https://github.com/Shopify/shopify_python_api/pull/710)) == Version 12.4.0 diff --git a/shopify/version.py b/shopify/version.py index 7c16e69e..3958d1de 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.4.0" +VERSION = "12.5.0" From 2ef7f2219fe06260e7af4d7ca531f9a8f9f3db3d Mon Sep 17 00:00:00 2001 From: bvoelsch Date: Thu, 11 Jan 2024 15:08:14 -0700 Subject: [PATCH 244/259] Update __init__.py Add FulfillmentV2 method to init --- shopify/resources/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 16220739..0d420b38 100644 --- a/shopify/resources/__init__.py +++ b/shopify/resources/__init__.py @@ -38,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 From a9277784740ef03c2789c868dfb957b3e670725a Mon Sep 17 00:00:00 2001 From: Matteo Depalo Date: Wed, 22 May 2024 15:43:20 +0100 Subject: [PATCH 245/259] Rename master to main --- .github/workflows/pre-commit.yml | 2 +- .github/workflows/stale.yml | 2 +- README.md | 34 ++++++++++++++++++++------------ RELEASING | 2 +- SECURITY.md | 2 +- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 048bc5a2..f92848d7 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -3,7 +3,7 @@ name: pre-commit on: pull_request: push: - branches: [master] + branches: [main] jobs: pre-commit: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index cd1f94c0..2d0a3ec7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,7 +22,7 @@ jobs: 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/master/CONTRIBUTING.md) file for guidelines + 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 diff --git a/README.md b/README.md index 7dc84ed5..d8519b1b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) -[![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/master/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/master/LICENSE) +[![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/main/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/main/LICENSE) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) The [Shopify Admin API](https://shopify.dev/docs/admin-api) Python Library @@ -23,20 +23,28 @@ pip install --upgrade ShopifyAPI ### Table of Contents -- [Getting Started](#getting-started) - - [Public and Custom Apps](#public-and-custom-apps) - - [Private Apps](#private-apps) -- [Billing](#billing) -- [Session Tokens](docs/session-tokens.md) -- [Handling Access Scope Operations](docs/api-access.md) -- [Advanced Usage](#advanced-usage) -- [Prefix Options](#prefix-options) -- [Console](#console) -- [GraphQL](#graphql) +- [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 @@ -263,7 +271,7 @@ 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 `master` branch. If you want to run pre-commit locally, install it and set up the git hook scripts +[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 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 From 741555f072d41309d1233fe07e08977715498aa6 Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:19:41 -0400 Subject: [PATCH 246/259] Add support for 2024-07 API version --- .github/workflows/build.yml | 4 ++-- CHANGELOG | 3 +++ README.md | 2 +- shopify/api_version.py | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7ec6d36..c0cd5ab9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Python ${{ matrix.version }} strategy: matrix: - version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout @@ -18,7 +18,7 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install pytest mock pytest-cov + pip install pytest mock pytest-cov setuptools python setup.py install pytest --cov=./ --cov-report=xml - name: Run Tests diff --git a/CHANGELOG b/CHANGELOG index c4462f6e..24fb1752 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ == Unreleased +- Update API version with 2024-07 release ([#](https://github.com/Shopify/shopify_python_api/pull/)) + == 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)) diff --git a/README.md b/README.md index d8519b1b..066bd343 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ pip install --upgrade ShopifyAPI ```python shop_url = "SHOP_NAME.myshopify.com" - api_version = '2024-01' + api_version = '2024-07' state = binascii.b2a_hex(os.urandom(15)).decode("utf-8") redirect_uri = "http://myapp.com/auth/shopify/callback" scopes = ['read_products', 'read_orders'] diff --git a/shopify/api_version.py b/shopify/api_version.py index a0a1531e..22df6052 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -38,6 +38,7 @@ def define_known_versions(cls): cls.define_version(Release("2023-10")) cls.define_version(Release("2024-01")) cls.define_version(Release("2024-04")) + cls.define_version(Release("2024-07")) @classmethod def clear_defined_versions(cls): From 05d59c88c82994dfebf683c35acdebea9a8fab7b Mon Sep 17 00:00:00 2001 From: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:38:26 -0400 Subject: [PATCH 247/259] Release v12.6.0 --- CHANGELOG | 4 +++- shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 24fb1752..cd636df3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ == Unreleased -- Update API version with 2024-07 release ([#](https://github.com/Shopify/shopify_python_api/pull/)) +== 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 diff --git a/shopify/version.py b/shopify/version.py index 3958d1de..7293b298 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.5.0" +VERSION = "12.6.0" From f7fe4c663c4d341fbbbed0b9691354ba288fa7ba Mon Sep 17 00:00:00 2001 From: James Gilmore Date: Tue, 9 Jul 2024 10:53:52 +0100 Subject: [PATCH 248/259] Add support for building the package in Python 3.12 As discussed in the [issue](https://github.com/Shopify/shopify_python_api/issues/725) there is an issue when trying to install this package when using Python 3.12. The issue is caused by [PEP-632](https://peps.python.org/pep-0632/) removing `distutils` from the built-in libraries this version of python is shipped with. In versions 6.0.0 and lower of PyYaml, this causes the build the fail. As mentioned in their [issue](https://github.com/yaml/pyyaml/issues/756) using v6.0.1 or later will resolve this issue. So updating our `setup.py` install_requires dependency specifications to reflect this. While I'm here, also update the classifiers to include support for PY3.12 and show this more clearly on the README of the repo. --- README.md | 1 + setup.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 066bd343..a8fa6d74 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://github.com/Shopify/shopify_python_api/workflows/CI/badge.svg)](https://github.com/Shopify/shopify_python_api/actions) [![PyPI version](https://badge.fury.io/py/ShopifyAPI.svg)](https://badge.fury.io/py/ShopifyAPI) +![Supported Python Versions](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-brightgreen) [![codecov](https://codecov.io/gh/Shopify/shopify_python_api/branch/main/graph/badge.svg?token=pNTx0TARUx)](https://codecov.io/gh/Shopify/shopify_python_api) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Shopify/shopify_python_api/blob/main/LICENSE) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) diff --git a/setup.py b/setup.py index dae5a810..eb23ab08 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,8 @@ install_requires=[ "pyactiveresource>=2.2.2", "PyJWT >= 2.0.0", - "PyYAML", + "PyYAML>=6.0.1; python_version>='3.12'", + "PyYAML; python_version<'3.12'", "six", ], test_suite="test", @@ -44,6 +45,7 @@ "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", From fdc155e64bcc86504834b1a01d0871232b7c5e08 Mon Sep 17 00:00:00 2001 From: Elizabeth Kenyon Date: Tue, 24 Sep 2024 13:36:25 -0500 Subject: [PATCH 249/259] Make API version more flexible Allow the use of API versions that are not previsouly defined --- CHANGELOG | 1 + shopify/api_version.py | 4 ++++ test/api_version_test.py | 14 ++++++++++++++ test/session_test.py | 13 +++++++++++++ 4 files changed, 32 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index cd636df3..5a5ed5db 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ == Unreleased +- 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 diff --git a/shopify/api_version.py b/shopify/api_version.py index 22df6052..32276668 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -17,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 @@ -39,6 +42,7 @@ def define_known_versions(cls): 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): diff --git a/test/api_version_test.py b/test/api_version_test.py index 3089daee..9dce8cb2 100644 --- a/test/api_version_test.py +++ b/test/api_version_test.py @@ -29,6 +29,20 @@ def test_coerce_to_version_raises_with_string_that_does_not_match_known_version( with self.assertRaises(shopify.VersionNotFoundError): 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): def test_raises_if_format_invalid(self): diff --git a/test/session_test.py b/test/session_test.py index 806d551b..d7cd5c3d 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -288,3 +288,16 @@ 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") From 7229004441645b1bc0a4c848cc1a6593fc9d6281 Mon Sep 17 00:00:00 2001 From: Matteo Depalo Date: Wed, 30 Oct 2024 10:46:27 +0000 Subject: [PATCH 250/259] Release v12.7.0 --- CHANGELOG | 3 +++ shopify/version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5a5ed5db..50cae06e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,7 @@ == Unreleased + +== Version 12.7.0 + - Remove requirement to use a predefined API version. Now you can use any valid API version string. ([#737](https://github.com/Shopify/shopify_python_api/pull/737)) == Version 12.6.0 diff --git a/shopify/version.py b/shopify/version.py index 7293b298..126c3ab4 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.6.0" +VERSION = "12.7.0" From 9451fe5f45c6d809787384e9ceaa294fec1b66c6 Mon Sep 17 00:00:00 2001 From: "yuichi.nasukawa" Date: Wed, 8 Jan 2025 22:01:30 +0900 Subject: [PATCH 251/259] Add REST API deprecation notice to README --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a8fa6d74..6226bcb1 100644 --- a/README.md +++ b/README.md @@ -87,10 +87,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 } }") ``` @@ -150,6 +151,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. @@ -157,6 +165,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 @@ -182,6 +191,7 @@ new_orders = shopify.Order.find(status="open", limit="50") 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) ``` @@ -196,6 +206,9 @@ 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 } }') ``` From 024d94691ed889d047704d2f1615f40a03cc02ac Mon Sep 17 00:00:00 2001 From: Tyler J Date: Sat, 11 Jan 2025 13:31:05 -0500 Subject: [PATCH 252/259] Refactor `create_permission_url` to handle optional `scope`. Modified `create_permission_url` to make `scope` optional, allowing it to be omitted when specified in the app's configuration (TOML). Updated the README to reflect this change and clarify usage. This improves flexibility and simplifies configuration management. --- README.md | 4 +++- shopify/session.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a8fa6d74..0b680d07 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,12 @@ pip install --upgrade ShopifyAPI 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 ``` diff --git a/shopify/session.py b/shopify/session.py index 39ce5f7b..52dc83f4 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -53,10 +53,11 @@ def __init__(self, shop_url, version=None, token=None, access_scopes=None): 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 = dict(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): From cdaa10d2d6a82d44652468d0d78aba537df08baa Mon Sep 17 00:00:00 2001 From: "yuichi.nasukawa" Date: Sun, 12 Jan 2025 08:40:56 +0900 Subject: [PATCH 253/259] chore: upgrade pre-commit dependencies --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43267923..f3500257 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,15 +2,15 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v5.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/PyCQA/pylint - rev: v2.15.8 + rev: v3.3.3 hooks: - id: pylint From 7b044441d59fe9087abc02554cbf267fb9a6d923 Mon Sep 17 00:00:00 2001 From: "yuichi.nasukawa" Date: Sun, 12 Jan 2025 08:44:30 +0900 Subject: [PATCH 254/259] fix: resolve new pylint warnings - Fix warnings introduced by pylint v3.3.3 upgrade --- scripts/shopify_api.py | 2 +- shopify/api_access.py | 1 - shopify/mixins.py | 2 +- shopify/session.py | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/shopify_api.py b/scripts/shopify_api.py index 5dfab93a..bab35f15 100755 --- a/scripts/shopify_api.py +++ b/scripts/shopify_api.py @@ -128,7 +128,7 @@ 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) diff --git a/shopify/api_access.py b/shopify/api_access.py index d5ffbe35..19b80671 100644 --- a/shopify/api_access.py +++ b/shopify/api_access.py @@ -14,7 +14,6 @@ class ApiAccessError(Exception): 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") diff --git a/shopify/mixins.py b/shopify/mixins.py index 54496dbf..5a13ca3a 100644 --- a/shopify/mixins.py +++ b/shopify/mixins.py @@ -24,7 +24,7 @@ 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 diff --git a/shopify/session.py b/shopify/session.py index 39ce5f7b..c3ec6d4b 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -54,7 +54,7 @@ def __init__(self, shop_url, version=None, token=None, access_scopes=None): 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) + query_params = {"client_id": self.api_key, "scope": ",".join(scope), "redirect_uri": redirect_uri} if state: query_params["state"] = state return "https://%s/admin/oauth/authorize?%s" % (self.url, urllib.parse.urlencode(query_params)) @@ -69,7 +69,7 @@ def request_token(self, params): 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) + 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) From ceada2433b004365b7d9f74669b9c1067e299ccb Mon Sep 17 00:00:00 2001 From: Tyler J Date: Sun, 12 Jan 2025 11:33:31 -0500 Subject: [PATCH 255/259] Update to version 12.7.1 and update the CHANGELOG. --- CHANGELOG | 2 ++ shopify/version.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 50cae06e..e9910c2e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ == 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)) diff --git a/shopify/version.py b/shopify/version.py index 126c3ab4..dfb0b4e4 100644 --- a/shopify/version.py +++ b/shopify/version.py @@ -1 +1 @@ -VERSION = "12.7.0" +VERSION = "12.7.1" From 07d6c47146e00c35bb39a83d86033f9b250623af Mon Sep 17 00:00:00 2001 From: Tyler J Date: Sun, 12 Jan 2025 11:48:02 -0500 Subject: [PATCH 256/259] Fix typo in method signature of create_permission_url Removed an unnecessary extra space in the method signature of `create_permission_url`. --- shopify/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shopify/session.py b/shopify/session.py index 52dc83f4..eec40517 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -53,7 +53,7 @@ def __init__(self, shop_url, version=None, token=None, access_scopes=None): self.access_scopes = access_scopes return - def create_permission_url(self, redirect_uri, scope=None, state=None): + def create_permission_url(self, redirect_uri, scope=None, state=None): query_params = dict(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) From adaf770a0b5a99ae791048967f39d89e13ea500f Mon Sep 17 00:00:00 2001 From: Tyler J Date: Fri, 17 Jan 2025 14:28:20 -0500 Subject: [PATCH 257/259] Update tests for `create_permission_url` method. Updated tests to improve clarity and consistency in naming and arguments. Modified `create_permission_url` calls to match new positional order for `redirect_uri` and `scope`. Enhanced assertion coverage for edge cases like empty scopes and added tests for state parameter handling. --- test/session_test.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/test/session_test.py b/test/session_test.py index d7cd5c3d..04d30748 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -86,51 +86,69 @@ def test_temp_works_without_currently_active_session(self): 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") scope = ["write_products"] - permission_url = session.create_permission_url(scope, "my_redirect_uri.com") + 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") + 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_no_scope_and_redirect_uri(self): + 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") scope = [] - permission_url = session.create_permission_url(scope, "my_redirect_uri.com") + 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_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&scope=", + "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_no_scope_and_redirect_uri_and_state(self): + 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") scope = [] - permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate") + 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=&state=mystate", + "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") scope = ["write_customers"] - permission_url = session.create_permission_url(scope, "my_redirect_uri.com", state="mystate") + 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), From 12e933ba9cadd2ba5b834fe5143cb70438e9e34b Mon Sep 17 00:00:00 2001 From: Tyler J Date: Mon, 20 Jan 2025 08:32:20 -0500 Subject: [PATCH 258/259] Fix linting errors Removes extra white space in parameters in session_test.py and changes conditional formatting in shopify/session.py. --- shopify/session.py | 8 +++++--- test/session_test.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/shopify/session.py b/shopify/session.py index eec40517..dcb41d41 100644 --- a/shopify/session.py +++ b/shopify/session.py @@ -54,10 +54,12 @@ def __init__(self, shop_url, version=None, token=None, access_scopes=None): return def create_permission_url(self, redirect_uri, scope=None, state=None): - query_params = dict(client_id=self.api_key, redirect_uri=redirect_uri) + 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 + 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): diff --git a/test/session_test.py b/test/session_test.py index 04d30748..8d73e293 100644 --- a/test/session_test.py +++ b/test/session_test.py @@ -148,7 +148,7 @@ def test_create_permission_url_returns_correct_url_with_redirect_uri_and_single_ shopify.Session.setup(api_key="My_test_key", secret="My test secret") session = shopify.Session("http://localhost.myshopify.com", "unstable") scope = ["write_customers"] - permission_url = session.create_permission_url( "my_redirect_uri.com", scope=scope, state="mystate") + 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), From 2f998c691bd15608603ef25e31238f6c22abc544 Mon Sep 17 00:00:00 2001 From: Tyler J Date: Mon, 20 Jan 2025 08:40:04 -0500 Subject: [PATCH 259/259] Fix linting errors Removes extra white space in parameters in session_test.py and changes conditional formatting in shopify/session.py. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8378880..cadda24e 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ _Note: Your application must be public to test the billing process. To test on a > **⚠️ 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.