diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..615aafb0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "/usr/bin/python3" +} \ No newline at end of file diff --git a/shopify/api_version.py b/shopify/api_version.py index 049ee45c..90fa3d6d 100644 --- a/shopify/api_version.py +++ b/shopify/api_version.py @@ -27,13 +27,14 @@ 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")) - cls.define_version(Release("2023-01")) - cls.define_version(Release("2023-04")) + cls.define_version(Release("2025-01")) + cls.define_version(Release("2025-04")) + cls.define_version(Release("2025-07")) + cls.define_version(Release("2025-10")) + cls.define_version(Release("2026-01")) + cls.define_version(Release("2026-04")) + cls.define_version(Release("2026-07")) + cls.define_version(Release("2026-10")) @classmethod def clear_defined_versions(cls): diff --git a/shopify/base.py b/shopify/base.py index 449e288b..e58baf23 100644 --- a/shopify/base.py +++ b/shopify/base.py @@ -1,9 +1,11 @@ +import logging import pyactiveresource.connection from pyactiveresource.activeresource import ActiveResource, ResourceMeta, formats import shopify.yamlobjects import shopify.mixins as mixins import shopify import threading +import time import sys from six.moves import urllib import six @@ -19,12 +21,33 @@ class ShopifyConnection(pyactiveresource.connection.Connection): def _open(self, *args, **kwargs): self.response = None - try: - self.response = super(ShopifyConnection, self)._open(*args, **kwargs) - except pyactiveresource.connection.ConnectionError as err: - self.response = err.response - raise - return self.response + count_server_error = 0 + while True: + try: + self.response = super(ShopifyConnection, self)._open(*args, **kwargs) + return self.response + except pyactiveresource.connection.ClientError as e: + if e.response.code == 429: + retry_after = 2 + logging.warning("%s limited, will retry in %d seconds", e.url, retry_after) + time.sleep(retry_after) + else: + raise e + except pyactiveresource.connection.ServerError as se: + # For handling Server Errors like 500, 502, 503 etc + if 500 <= se.code <= 599: + retry_after = 2 + logging.warning("Server responded with error code %d will retry to send request in %d seconds", se.code, retry_after) + time.sleep(retry_after) + if count_server_error >= 3: + logging.error("Error connecting to Shopify API, please try again") + self.response = se.response + raise + else: + logging.warning("Server Error Patch Retrying. Current Count - %d", count_server_error) + count_server_error += 1 + else: + raise se # Inherit from pyactiveresource's metaclass in order to use ShopifyConnection diff --git a/shopify/resources/__init__.py b/shopify/resources/__init__.py index 16220739..32cc9be3 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 FulfillmentV2 from .fulfillment_event import FulfillmentEvent from .fulfillment_service import FulfillmentService from .carrier_service import CarrierService @@ -78,5 +78,7 @@ from .collection_publication import CollectionPublication from .product_publication import ProductPublication from .graphql import GraphQL +from .application_credit import ApplicationCredit +from .fulfillment_order import FulfillmentOrder from ..base import ShopifyResource diff --git a/shopify/resources/application_credit.py b/shopify/resources/application_credit.py index ecc12fa0..fe48ca47 100644 --- a/shopify/resources/application_credit.py +++ b/shopify/resources/application_credit.py @@ -2,4 +2,4 @@ class ApplicationCredit(ShopifyResource): - pass + pass diff --git a/shopify/resources/fulfillment.py b/shopify/resources/fulfillment.py index fcf74863..72c43488 100644 --- a/shopify/resources/fulfillment.py +++ b/shopify/resources/fulfillment.py @@ -2,32 +2,25 @@ import json -class Fulfillment(ShopifyResource): - _prefix_source = "/orders/$order_id/" - - def cancel(self): - self._load_attributes_from_response(self.post("cancel")) - - def complete(self): - self._load_attributes_from_response(self.post("complete")) - - def open(self): - self._load_attributes_from_response(self.post("open")) - - def update_tracking(self, tracking_info, notify_customer): - fulfill = FulfillmentV2() - fulfill.id = self.id - self._load_attributes_from_response(fulfill.update_tracking(tracking_info, notify_customer)) - - -class FulfillmentOrders(ShopifyResource): - _prefix_source = "/orders/$order_id/" - - class FulfillmentV2(ShopifyResource): _singular = "fulfillment" _plural = "fulfillments" + @classmethod + def find(cls, key=None, **kwargs): + order_id = kwargs.get("order_id") + fulfillment_order = kwargs.get("fulfillment_order_id") + if order_id: + if key: + return super(FulfillmentV2, cls).find_one(from_="%s/orders/%s/fulfillments/%s.json" % ( cls.site, order_id, key), **kwargs) + else: + return super(FulfillmentV2, cls).find(from_="%s/orders/%s/fulfillments.json" % ( cls.site, order_id), **kwargs) + elif fulfillment_order: + return super(FulfillmentV2, cls).find(from_="%s/fulfillment_orders/%s/fulfillments.json" % ( cls.site, fulfillment_order), **kwargs) + + def cancel(self): + self._load_attributes_from_response(self.post("cancel")) + def update_tracking(self, tracking_info, notify_customer): body = {"fulfillment": {"tracking_info": tracking_info, "notify_customer": notify_customer}} return self.post("update_tracking", json.dumps(body).encode()) diff --git a/shopify/resources/fulfillment_order.py b/shopify/resources/fulfillment_order.py new file mode 100644 index 00000000..caf40a51 --- /dev/null +++ b/shopify/resources/fulfillment_order.py @@ -0,0 +1,30 @@ +from ..base import ShopifyResource +from .location import Location +import json + +class FulfillmentOrder(ShopifyResource): + + @classmethod + def find(cls, key=None, **kwargs): + order_id = kwargs.get("order_id") + if order_id: + return super(FulfillmentOrder, cls).find(from_="%s/orders/%s/fulfillment_orders.json" % ( cls.site, order_id), **kwargs) + else: + return super(FulfillmentOrder, cls).find(key) + + @classmethod + def locations_for_move(cls, key=None, **kwargs): + return Location.find(from_="%s/fulfillment_orders/%s/locations_for_move.json" % ( + ShopifyResource.site, key), **kwargs) + + def cancel(self): + self._load_attributes_from_response(self.post("cancel")) + + def close(self): + self._load_attributes_from_response(self.post("close")) + + def move(self, new_location_id, fulfillment_order_line_items = []): + body = { 'fulfillment_order' : { 'new_location_id' : new_location_id } } + if fulfillment_order_line_items: + body['fulfillment_order']['fulfillment_order_line_items'] = fulfillment_order_line_items + self.post("move", body=json.dumps(body).encode()) diff --git a/shopify/resources/graphql.py b/shopify/resources/graphql.py index 33525ef1..41b9103c 100644 --- a/shopify/resources/graphql.py +++ b/shopify/resources/graphql.py @@ -1,5 +1,4 @@ import shopify -from ..base import ShopifyResource from six.moves import urllib import json @@ -16,7 +15,6 @@ def merge_headers(self, *headers): return merged_headers def execute(self, query, variables=None, operation_name=None): - endpoint = self.endpoint default_headers = {"Accept": "application/json", "Content-Type": "application/json"} headers = self.merge_headers(default_headers, self.headers) data = {"query": query, "variables": variables, "operationName": operation_name} @@ -25,7 +23,7 @@ def execute(self, query, variables=None, operation_name=None): try: response = urllib.request.urlopen(req) - return response.read().decode("utf-8") + return json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as e: print((e.read())) print("") diff --git a/shopify/resources/order.py b/shopify/resources/order.py index 2e31a8c3..6609b4f9 100644 --- a/shopify/resources/order.py +++ b/shopify/resources/order.py @@ -1,7 +1,7 @@ from ..base import ShopifyResource from shopify import mixins from .transaction import Transaction - +from .fulfillment_order import FulfillmentOrder class Order(ShopifyResource, mixins.Metafields, mixins.Events): _prefix_source = "/customers/$customer_id/" @@ -28,3 +28,8 @@ def transactions(self): def capture(self, amount=""): return Transaction.create({"amount": amount, "kind": "capture", "order_id": self.id}) + + def fulfillment_orders(self, **kwargs): + return FulfillmentOrder.find(from_="%s/orders/%s/fulfillment_orders.json" % ( + ShopifyResource.site, self.id), **kwargs) +