From 3ec308dc56a1d1ba2acb29abed6c5b1113a0c797 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Fri, 11 Jun 2021 16:47:30 -0500 Subject: [PATCH 1/4] work in progress - debugging test --- .../models/track_pacakge/__init__.py | 16 +++++++--- shipengine_sdk/services/track_package.py | 6 ++-- shipengine_sdk/shipengine.py | 10 +++--- shipengine_sdk/util/iso_string.py | 29 ++++++++++++----- tests/services/test_track_package.py | 14 +++++++-- tests/util/test_helpers.py | 31 ++++++++++++++++--- 6 files changed, 79 insertions(+), 27 deletions(-) diff --git a/shipengine_sdk/models/track_pacakge/__init__.py b/shipengine_sdk/models/track_pacakge/__init__.py index da95495..6e9b43a 100644 --- a/shipengine_sdk/models/track_pacakge/__init__.py +++ b/shipengine_sdk/models/track_pacakge/__init__.py @@ -117,11 +117,17 @@ class TrackingEvent: def __init__(self, event: Dict[str, any]) -> None: """Tracking event object.""" - self.date_time = IsoString(iso_string=event["timestamp"]).to_datetime_object() + if IsoString(event["timestamp"]).has_timezone(): + self.date_time = IsoString(iso_string=event["timestamp"]).to_datetime_object() + else: + self.date_time = datetime.fromisoformat(event["timestamp"]) - self.carrier_date_time = IsoString( - iso_string=event["carrierTimestamp"] - ).to_datetime_object() + if IsoString(iso_string=event["carrierTimestamp"]).has_timezone(): + self.carrier_date_time = IsoString( + iso_string=event["carrierTimestamp"] + ).to_datetime_object() + else: + self.carrier_date_time = datetime.fromisoformat(event["carrierTimestamp"]) self.status = event["status"] self.description = event["description"] if "description" in event else None @@ -141,7 +147,7 @@ def to_json(self): class TrackPackageResult: shipment: Optional[Shipment] package: Optional[Package] - events: Optional[List[TrackingEvent]] + events: Optional[List[TrackingEvent]] = list() def __init__(self, api_response: Dict[str, any], config: ShipEngineConfig) -> None: """This object is used as the return type for the `track_package` and `track` methods.""" diff --git a/shipengine_sdk/services/track_package.py b/shipengine_sdk/services/track_package.py index 8830080..99b30c6 100644 --- a/shipengine_sdk/services/track_package.py +++ b/shipengine_sdk/services/track_package.py @@ -16,7 +16,7 @@ def track( is_package_id_valid(tracking_data) api_response = rpc_request( - method=RPCMethods.LIST_CARRIERS.value, + method=RPCMethods.TRACK_PACKAGE.value, config=config, params={"packageID": tracking_data}, ) @@ -25,13 +25,13 @@ def track( if type(tracking_data) is TrackingQuery: api_response = rpc_request( - method=RPCMethods.LIST_CARRIERS.value, config=config, params=tracking_data.to_dict() + method=RPCMethods.TRACK_PACKAGE.value, config=config, params=tracking_data.to_dict() ) return TrackPackageResult(api_response, config) elif type(tracking_data) is dict: api_response = rpc_request( - method=RPCMethods.LIST_CARRIERS.value, config=config, params=tracking_data + method=RPCMethods.TRACK_PACKAGE.value, config=config, params=tracking_data ) return TrackPackageResult(api_response, config) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 4eb1104..63e1db9 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -31,7 +31,7 @@ def __init__(self, config: Union[str, Dict[str, any], ShipEngineConfig]) -> None self.config: ShipEngineConfig = ShipEngineConfig(config) def validate_address( - self, address: Address, config: Union[Dict[str, any], ShipEngineConfig] = None + self, address: Address, config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None ) -> AddressValidateResult: """ Validate an address in nearly any countryCode in the world. @@ -45,14 +45,16 @@ def validate_address( return validate(address=address, config=config) def normalize_address( - self, address: Address, config: Union[Dict[str, any], ShipEngineConfig] = None + self, address: Address, config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None ) -> Address: """Normalize a given address into a standardized format used by carriers.""" config: ShipEngineConfig = self.config.merge(new_config=config) return normalize(address=address, config=config) def get_carrier_accounts( - self, carrier_code: Optional[str] = None, config: Optional[Dict[str, any]] = None + self, + carrier_code: Optional[str] = None, + config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None, ) -> List[CarrierAccount]: """Fetch a list of the carrier accounts connected to your ShipEngine Account.""" config: ShipEngineConfig = self.config.merge(new_config=config) @@ -62,7 +64,7 @@ def get_carrier_accounts( def track_package( self, tracking_data: Union[str, Dict[str, any], TrackingQuery], - config: Union[Dict[str, any], ShipEngineConfig], + config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None, ) -> TrackPackageResult: """ Track a package by `tracking_number` and `carrier_code` via the **TrackingQuery** object, by using just the diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index cf42394..6745f50 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -19,11 +19,26 @@ def __init__(self, iso_string: str) -> None: def to_datetime_object(self) -> datetime: return datetime.strptime(self.iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") - def has_time(self) -> bool: - pattern = re.compile(r"[0-9]*T[0-9]*") - return True if pattern.match(self.iso_string) is not None else False - def has_timezone(self) -> bool: - pattern = re.compile(r"(?<=T).*[+-][0-9]|Z$") - if self.has_time() is True: - return True if pattern.match(self.iso_string) is not None else False + if self.is_valid_iso_string(self.iso_string): + return False if self.is_valid_iso_string_no_tz(self.iso_string) else True + + @staticmethod + def is_valid_iso_string_no_tz(iso_str: str): + pattern = re.compile( + r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?([+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa + ) + if pattern.match(iso_str): + return True + else: + return False + + @staticmethod + def is_valid_iso_string(iso_str: str): + pattern = re.compile( + r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa + ) + if pattern.match(iso_str): + return True + else: + return False diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py index a777018..9de9cce 100644 --- a/tests/services/test_track_package.py +++ b/tests/services/test_track_package.py @@ -1,5 +1,13 @@ """Testing the `track_package` method of the ShipEngine SDK.""" - -class TestTrackPackage: - pass +# class TestTrackPackage: +# def test_track_by_tracking_number_and_carrier_code(self) -> None: +# """DX-1084 - Test track by tracking number and carrier code.""" +# shipengine = configurable_stub_shipengine_instance(stub_config()) +# tracking_data = TrackingQuery( +# carrier_code="fedex", +# tracking_number="abcFedExDelivered" +# ) +# tracking_result = shipengine.track_package(tracking_data=tracking_data) +# +# assert tracking_data.carrier_code == tracking_result.shipment.carrier.code diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py index 0fcb725..dabdd66 100644 --- a/tests/util/test_helpers.py +++ b/tests/util/test_helpers.py @@ -2,22 +2,38 @@ from typing import Dict, Optional, Union from shipengine_sdk import ShipEngine -from shipengine_sdk.models import Address, AddressValidateResult, Endpoints +from shipengine_sdk.models import ( + Address, + AddressValidateResult, + Endpoints, + TrackingQuery, +) -def stub_config() -> Dict[str, any]: +def stub_config( + retries: int = 1, +) -> Dict[str, any]: """ Return a test configuration dictionary to be used when instantiating the ShipEngine object. """ return dict( - api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 + api_key="baz", + base_uri=Endpoints.TEST_RPC_URL.value, + page_size=50, + retries=retries, + timeout=15, ) +def configurable_stub_shipengine_instance(config: Dict[str, any]) -> ShipEngine: + """""" + return ShipEngine(config=config) + + def stub_shipengine_instance() -> ShipEngine: """Return a test instance of the ShipEngine object.""" - return ShipEngine(stub_config()) + return ShipEngine(config=stub_config()) def address_with_all_fields() -> Address: @@ -223,7 +239,12 @@ def normalize_an_address(address: Address) -> Address: it and calls the `normalize_address` method, providing it the `address` that is passed into this function. """ - return stub_shipengine_instance().normalize_address(address) + return stub_shipengine_instance().normalize_address(address=address) + + +def track_a_package(tracking_data: Union[str, Dict[str, any], TrackingQuery]): + """""" + return stub_shipengine_instance().track_package(tracking_data=tracking_data) def stub_get_carrier_accounts(carrier_code: Optional[str] = None): From 1a6c6bad7954fa56acba96a19ac1d915bbd9f6e9 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Sat, 12 Jun 2021 18:24:25 -0500 Subject: [PATCH 2/4] DX-1084 - Test track by tracking number and carrier code. - https://auctane.atlassian.net/browse/DX-1084 --- shipengine_sdk/models/__init__.py | 9 +- shipengine_sdk/models/enums/__init__.py | 1 + shipengine_sdk/models/enums/regex_patterns.py | 7 ++ .../models/track_pacakge/__init__.py | 96 ++++++++++++------- shipengine_sdk/util/iso_string.py | 16 ++-- tests/services/test_track_package.py | 25 ++--- 6 files changed, 100 insertions(+), 54 deletions(-) create mode 100644 shipengine_sdk/models/enums/regex_patterns.py diff --git a/shipengine_sdk/models/__init__.py b/shipengine_sdk/models/__init__.py index 5b6cb9a..3b5deb5 100644 --- a/shipengine_sdk/models/__init__.py +++ b/shipengine_sdk/models/__init__.py @@ -2,4 +2,11 @@ from .address import Address, AddressValidateResult from .carriers import Carrier, CarrierAccount from .enums import * # noqa -from .track_pacakge import Package, Shipment, TrackingQuery, TrackPackageResult +from .track_pacakge import ( + Location, + Package, + Shipment, + TrackingEvent, + TrackingQuery, + TrackPackageResult, +) diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine_sdk/models/enums/__init__.py index efdec97..9badc5a 100644 --- a/shipengine_sdk/models/enums/__init__.py +++ b/shipengine_sdk/models/enums/__init__.py @@ -6,6 +6,7 @@ from .error_code import ErrorCode from .error_source import ErrorSource from .error_type import ErrorType +from .regex_patterns import RegexPatterns class Endpoints(Enum): diff --git a/shipengine_sdk/models/enums/regex_patterns.py b/shipengine_sdk/models/enums/regex_patterns.py new file mode 100644 index 0000000..78d38c5 --- /dev/null +++ b/shipengine_sdk/models/enums/regex_patterns.py @@ -0,0 +1,7 @@ +"""Enumeration of Regular Expression patterns used in the ShipEngine SDK.""" +from enum import Enum + + +class RegexPatterns(Enum): + VALID_ISO_STRING = r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa + VALID_ISO_STRING_NO_TZ = r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?([+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa diff --git a/shipengine_sdk/models/track_pacakge/__init__.py b/shipengine_sdk/models/track_pacakge/__init__.py index 6e9b43a..1dbde4e 100644 --- a/shipengine_sdk/models/track_pacakge/__init__.py +++ b/shipengine_sdk/models/track_pacakge/__init__.py @@ -1,7 +1,6 @@ """Data objects to be used in the `track_package` and `track` methods.""" import json from dataclasses import dataclass -from datetime import datetime from typing import Dict, List, Optional from dataclasses_json import LetterCase, dataclass_json @@ -15,25 +14,25 @@ class Shipment: config: ShipEngineConfig - shipment_id: Optional[str] - account_id: Optional[str] - carrier_account: Optional[CarrierAccount] + shipment_id: Optional[str] = None + account_id: Optional[str] = None + carrier_account: Optional[CarrierAccount] = None carrier: Carrier - estimated_delivery_date: datetime - actual_delivery_date: datetime + estimated_delivery_date: str + actual_delivery_date: str def __init__( - self, shipment: Dict[str, any], actual_delivery_date: datetime, config: ShipEngineConfig + self, shipment: Dict[str, any], actual_delivery_date: IsoString, config: ShipEngineConfig ) -> None: """This object represents a given Shipment.""" self.config = config self.shipment_id = shipment["shipmentID"] if "shipmentID" in shipment else None self.account_id = shipment["carrierAccountID"] if "carrierAccountID" in shipment else None - self.carrier_account = ( - self._get_carrier_account(carrier=shipment["carrierCode"], account_id=self.account_id) - if "carrierCode" in shipment - else None - ) + + if self.account_id is not None: + self.carrier_account = self._get_carrier_account( + carrier=shipment["carrierCode"], account_id=self.account_id + ) if self.carrier_account is not None: self.carrier = self.carrier_account.carrier @@ -46,7 +45,7 @@ def __init__( self.estimated_delivery_date = IsoString( iso_string=shipment["estimatedDelivery"] - ).to_datetime_object() + ).to_string() self.actual_delivery_date = actual_delivery_date def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: @@ -57,24 +56,29 @@ def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: ) for account in carrier_accounts: - if account_id == account["account_id"]: + if account_id == account.account_id: target_carrier.append(account) return target_carrier[0] else: raise ShipEngineError( - message=f"accountID [{account_id}] doesn't amtch any of the accounts connected to your " - + "ShipEngine Account." + message=f"accountID [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa ) - def to_dict(self): + def to_dict(self) -> Dict[str, any]: + if hasattr(self, "config"): + del self.config + else: + pass # noqa return (lambda o: o.__dict__)(self) - def to_json(self): + def to_json(self) -> str: + if hasattr(self, "config"): + del self.config + else: + pass # noqa return json.dumps(self, default=lambda o: o.__dict__, indent=2) -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass class Package: """This object contains package information for a given shipment.""" @@ -84,6 +88,19 @@ class Package: tracking_number: Optional[str] tracking_url: Optional[str] + def __init__(self, package: Dict[str, any]) -> None: + self.package_id = package["packageID"] if "packageID" in package else None + self.weight = package["weight"] if "weight" in package else None + self.dimensions = package["dimensions"] if "dimensions" in package else None + self.tracking_number = package["trackingNumber"] if "trackingNumber" in package else None + self.tracking_url = package["trackingURL"] if "trackingURL" in package else None + + def to_dict(self) -> Dict[str, any]: + return (lambda o: o.__dict__)(self) + + def to_json(self) -> str: + return json.dumps(self, default=lambda o: o.__dict__, indent=2) + @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass @@ -97,17 +114,17 @@ class TrackingQuery: @dataclass_json(letter_case=LetterCase.CAMEL) @dataclass class Location: - city_locality: Optional[str] - state_province: Optional[str] - postal_code: Optional[str] - country_code: Optional[str] - latitude: Optional[float] - longitude: Optional[float] + city_locality: Optional[str] = None + state_province: Optional[str] = None + postal_code: Optional[str] = None + country_code: Optional[str] = None + latitude: Optional[float] = None + longitude: Optional[float] = None class TrackingEvent: - date_time: datetime - carrier_date_time: datetime + date_time: str + carrier_date_time: str status: str description: Optional[str] carrier_status_code: Optional[str] @@ -118,16 +135,14 @@ class TrackingEvent: def __init__(self, event: Dict[str, any]) -> None: """Tracking event object.""" if IsoString(event["timestamp"]).has_timezone(): - self.date_time = IsoString(iso_string=event["timestamp"]).to_datetime_object() + self.date_time = IsoString(iso_string=event["timestamp"]).to_string() else: - self.date_time = datetime.fromisoformat(event["timestamp"]) + self.date_time = IsoString(event["timestamp"]).to_string() if IsoString(iso_string=event["carrierTimestamp"]).has_timezone(): - self.carrier_date_time = IsoString( - iso_string=event["carrierTimestamp"] - ).to_datetime_object() + self.carrier_date_time = IsoString(iso_string=event["carrierTimestamp"]).to_string() else: - self.carrier_date_time = datetime.fromisoformat(event["carrierTimestamp"]) + self.carrier_date_time = IsoString(event["carrierTimestamp"]).to_string() self.status = event["status"] self.description = event["description"] if "description" in event else None @@ -151,6 +166,7 @@ class TrackPackageResult: def __init__(self, api_response: Dict[str, any], config: ShipEngineConfig) -> None: """This object is used as the return type for the `track_package` and `track` methods.""" + self.events = list() result = api_response["result"] for event in result["events"]: self.events.append(TrackingEvent(event=event)) @@ -161,10 +177,10 @@ def __init__(self, api_response: Dict[str, any], config: ShipEngineConfig) -> No actual_delivery_date=self.get_latest_event().date_time, config=config, ) - if "shipment" in api_response + if "shipment" in result else None ) - self.package = Package.from_dict(result["package"]) if "package" in result else None + self.package = Package(result["package"]) if "package" in result else None def get_errors(self) -> List[TrackingEvent]: # TODO: debug """Returns **only** the EXCEPTION events.""" @@ -184,7 +200,15 @@ def has_errors(self) -> bool: # TODO: debug return event.status == "EXCEPTION" def to_dict(self): + if hasattr(self.shipment, "config"): + del self.shipment.config + else: + pass # noqa return (lambda o: o.__dict__)(self) def to_json(self): + if hasattr(self.shipment, "config"): + del self.shipment.config + else: + pass # noqa return json.dumps(self, default=lambda o: o.__dict__, indent=2) diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index 6745f50..da5623d 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -2,6 +2,8 @@ import re from datetime import datetime +from shipengine_sdk.models import RegexPatterns + class IsoString: def __init__(self, iso_string: str) -> None: @@ -16,6 +18,12 @@ def __init__(self, iso_string: str) -> None: """ self.iso_string = iso_string + def __str__(self) -> str: + return f"{self.iso_string}" + + def to_string(self) -> str: + return self.iso_string + def to_datetime_object(self) -> datetime: return datetime.strptime(self.iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") @@ -25,9 +33,7 @@ def has_timezone(self) -> bool: @staticmethod def is_valid_iso_string_no_tz(iso_str: str): - pattern = re.compile( - r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?([+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa - ) + pattern = re.compile(RegexPatterns.VALID_ISO_STRING_NO_TZ.value) if pattern.match(iso_str): return True else: @@ -35,9 +41,7 @@ def is_valid_iso_string_no_tz(iso_str: str): @staticmethod def is_valid_iso_string(iso_str: str): - pattern = re.compile( - r"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$" # noqa - ) + pattern = re.compile(RegexPatterns.VALID_ISO_STRING.value) if pattern.match(iso_str): return True else: diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py index 9de9cce..7f9c886 100644 --- a/tests/services/test_track_package.py +++ b/tests/services/test_track_package.py @@ -1,13 +1,16 @@ """Testing the `track_package` method of the ShipEngine SDK.""" +from shipengine_sdk.models import TrackingQuery +from tests.util.test_helpers import configurable_stub_shipengine_instance, stub_config -# class TestTrackPackage: -# def test_track_by_tracking_number_and_carrier_code(self) -> None: -# """DX-1084 - Test track by tracking number and carrier code.""" -# shipengine = configurable_stub_shipengine_instance(stub_config()) -# tracking_data = TrackingQuery( -# carrier_code="fedex", -# tracking_number="abcFedExDelivered" -# ) -# tracking_result = shipengine.track_package(tracking_data=tracking_data) -# -# assert tracking_data.carrier_code == tracking_result.shipment.carrier.code + +class TestTrackPackage: + def test_track_by_tracking_number_and_carrier_code(self) -> None: + """DX-1084 - Test track by tracking number and carrier code.""" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="abcFedExDelivered") + tracking_result = shipengine.track_package(tracking_data=tracking_data) + + assert tracking_data.carrier_code == tracking_result.shipment.carrier.code + assert tracking_data.tracking_number == tracking_result.package.tracking_number + assert tracking_result.package.tracking_url is not None + assert type(tracking_result.package.tracking_url) is str From 20b43631ad7ffbe903e5e833eba690c78b1f6eec Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Mon, 14 Jun 2021 09:57:30 -0500 Subject: [PATCH 3/4] minor fixes to typing --- shipengine_sdk/http_client/client.py | 12 ++++----- shipengine_sdk/jsonrpc/__init__.py | 16 +++++++----- shipengine_sdk/jsonrpc/process_request.py | 12 ++++----- shipengine_sdk/models/carriers/__init__.py | 4 +-- .../models/track_pacakge/__init__.py | 24 ++++++++--------- shipengine_sdk/services/address_validation.py | 8 +++--- shipengine_sdk/services/track_package.py | 6 ++--- shipengine_sdk/shipengine.py | 26 +++++++++---------- shipengine_sdk/shipengine_config.py | 6 ++--- shipengine_sdk/util/sdk_assertions.py | 18 ++++++------- 10 files changed, 67 insertions(+), 65 deletions(-) diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 7cf5ecb..eea0bad 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -2,7 +2,7 @@ import json import os import platform -from typing import Dict, Optional +from typing import Any, Dict, Optional import requests from requests import PreparedRequest, Request, RequestException, Response, Session @@ -37,8 +37,8 @@ def __init__(self) -> None: self.session = requests.session() def send_rpc_request( - self, method: str, params: Optional[Dict[str, any]], retry: int, config: ShipEngineConfig - ) -> Dict[str, any]: + self, method: str, params: Optional[Dict[str, Any]], retry: int, config: ShipEngineConfig + ) -> Dict[str, Any]: """ Send a `JSON-RPC 2.0` request via HTTP Messages to ShipEngine API. If the response * is successful, the result is returned. Otherwise, an error is thrown. @@ -53,13 +53,13 @@ def send_rpc_request( else os.getenv("CLIENT_BASE_URI") ) - request_headers: Dict[str, any] = { + request_headers: Dict[str, Any] = { "User-Agent": self._derive_user_agent(), "Content-Type": "application/json", "Accept": "application/json", } - request_body: Dict[str, any] = wrap_request(method=method, params=params) + request_body: Dict[str, Any] = wrap_request(method=method, params=params) req: Request = Request( method="POST", @@ -80,7 +80,7 @@ def send_rpc_request( error_code=ErrorCode.UNSPECIFIED.value, ) - resp_body: Dict[str, any] = resp.json() + resp_body: Dict[str, Any] = resp.json() status_code: int = resp.status_code is_response_404(status_code=status_code, response_body=resp_body, config=config) diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index e0c3057..39dee0e 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -3,7 +3,7 @@ functionality for sending HTTP requests from the ShipEngine SDK. """ import time -from typing import Dict, Optional +from typing import Any, Dict, Optional from ..errors import RateLimitExceededError from ..http_client import ShipEngineClient @@ -12,8 +12,8 @@ def rpc_request( - method: str, config: ShipEngineConfig, params: Optional[Dict[str, any]] = None -) -> Dict[str, any]: + method: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None +) -> Dict[str, Any]: """ Create and send a `JSON-RPC 2.0` request over HTTP messages. TODO: add param and return docs @@ -21,13 +21,15 @@ def rpc_request( return rpc_request_loop(method, params, config) -def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> Dict[str, any]: +def rpc_request_loop( + method: str, params: Optional[Dict[str, Any]], config: ShipEngineConfig +) -> Dict[str, Any]: client: ShipEngineClient = ShipEngineClient() - api_response: Optional[Dict[str, any]] = None + api_response: Optional[Dict[str, Any]] = None retry: int = 0 while retry <= config.retries: try: - api_response: Dict[str, any] = client.send_rpc_request( + api_response = client.send_rpc_request( method=method, params=params, retry=retry, config=config ) except Exception as err: @@ -40,4 +42,4 @@ def rpc_request_loop(method: str, params: dict, config: ShipEngineConfig) -> Dic else: raise err retry += 1 - return api_response + return api_response diff --git a/shipengine_sdk/jsonrpc/process_request.py b/shipengine_sdk/jsonrpc/process_request.py index 244e7bc..a7d59df 100644 --- a/shipengine_sdk/jsonrpc/process_request.py +++ b/shipengine_sdk/jsonrpc/process_request.py @@ -1,5 +1,5 @@ """Functions that help with process requests and handle responses.""" -from typing import Dict, Optional +from typing import Any, Dict, Optional from uuid import uuid4 from ..errors import ( @@ -13,7 +13,7 @@ from ..models import ErrorType -def wrap_request(method: str, params: Optional[Dict[str, any]]) -> Dict[str, any]: +def wrap_request(method: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any]: """ Wrap request per `JSON-RPC 2.0` spec. @@ -21,7 +21,7 @@ def wrap_request(method: str, params: Optional[Dict[str, any]]) -> Dict[str, any invoke a specific remote procedure. :param params: The request data for the RPC request. This argument is optional and can either be a dictionary or None. - :type params: Optional[Dict[str, any]] + :type params: Optional[Dict[str, Any]] """ if params is None: return dict(id=f"req_{str(uuid4()).replace('-', '')}", jsonrpc="2.0", method=method) @@ -31,13 +31,13 @@ def wrap_request(method: str, params: Optional[Dict[str, any]]) -> Dict[str, any ) -def handle_response(response_body: Dict[str, any]) -> Dict[str, any]: +def handle_response(response_body: Dict[str, Any]) -> Dict[str, Any]: """Handles the response from ShipEngine API.""" if "result" in response_body: return response_body - error: Dict[str, any] = response_body["error"] - error_data: Dict[str, any] = error["data"] + error: Dict[str, Any] = response_body["error"] + error_data: Dict[str, Any] = error["data"] error_type: str = error_data["type"] if error_type is ErrorType.ACCOUNT_STATUS.value: raise AccountStatusError( diff --git a/shipengine_sdk/models/carriers/__init__.py b/shipengine_sdk/models/carriers/__init__.py index 43ef503..3b11f69 100644 --- a/shipengine_sdk/models/carriers/__init__.py +++ b/shipengine_sdk/models/carriers/__init__.py @@ -1,6 +1,6 @@ """CarrierAccount class object and immutable carrier object.""" import json -from typing import Dict +from typing import Any, Dict from ...errors import InvalidFieldValueError, ShipEngineError from ..enums import Carriers, does_member_value_exist, get_carrier_name_value @@ -25,7 +25,7 @@ def to_json(self): class CarrierAccount: carrier: Carrier - def __init__(self, account_information: Dict[str, any]) -> None: + def __init__(self, account_information: Dict[str, Any]) -> None: """This class represents a given account with a Carrier provider e.g. `FedEx`, `UPS`, `USPS`.""" self._set_carrier(account_information["carrierCode"]) self.account_id = account_information["accountID"] diff --git a/shipengine_sdk/models/track_pacakge/__init__.py b/shipengine_sdk/models/track_pacakge/__init__.py index 1dbde4e..db95781 100644 --- a/shipengine_sdk/models/track_pacakge/__init__.py +++ b/shipengine_sdk/models/track_pacakge/__init__.py @@ -1,9 +1,9 @@ """Data objects to be used in the `track_package` and `track` methods.""" import json from dataclasses import dataclass -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional -from dataclasses_json import LetterCase, dataclass_json +from dataclasses_json import LetterCase, dataclass_json # type: ignore from ...errors import ShipEngineError from ...services.get_carrier_accounts import GetCarrierAccounts @@ -22,7 +22,7 @@ class Shipment: actual_delivery_date: str def __init__( - self, shipment: Dict[str, any], actual_delivery_date: IsoString, config: ShipEngineConfig + self, shipment: Dict[str, Any], actual_delivery_date: str, config: ShipEngineConfig ) -> None: """This object represents a given Shipment.""" self.config = config @@ -58,13 +58,13 @@ def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: for account in carrier_accounts: if account_id == account.account_id: target_carrier.append(account) - return target_carrier[0] else: raise ShipEngineError( message=f"accountID [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa ) + return target_carrier[0] - def to_dict(self) -> Dict[str, any]: + def to_dict(self) -> Dict[str, Any]: if hasattr(self, "config"): del self.config else: @@ -83,19 +83,19 @@ class Package: """This object contains package information for a given shipment.""" package_id: Optional[str] - weight: Optional[Dict[str, any]] - dimensions: Optional[Dict[str, any]] + weight: Optional[Dict[str, Any]] + dimensions: Optional[Dict[str, Any]] tracking_number: Optional[str] tracking_url: Optional[str] - def __init__(self, package: Dict[str, any]) -> None: + def __init__(self, package: Dict[str, Any]) -> None: self.package_id = package["packageID"] if "packageID" in package else None self.weight = package["weight"] if "weight" in package else None self.dimensions = package["dimensions"] if "dimensions" in package else None self.tracking_number = package["trackingNumber"] if "trackingNumber" in package else None self.tracking_url = package["trackingURL"] if "trackingURL" in package else None - def to_dict(self) -> Dict[str, any]: + def to_dict(self) -> Dict[str, Any]: return (lambda o: o.__dict__)(self) def to_json(self) -> str: @@ -132,7 +132,7 @@ class TrackingEvent: signer: Optional[str] location: Optional[Location] - def __init__(self, event: Dict[str, any]) -> None: + def __init__(self, event: Dict[str, Any]) -> None: """Tracking event object.""" if IsoString(event["timestamp"]).has_timezone(): self.date_time = IsoString(iso_string=event["timestamp"]).to_string() @@ -150,7 +150,7 @@ def __init__(self, event: Dict[str, any]) -> None: event["carrierStatusCode"] if "carrierStatusCode" in event else None ) self.signer = event["signer"] if "signer" in event else None - self.location = Location.from_dict(event["location"]) if "location" in event else None + self.location = Location.from_dict(event["location"]) if "location" in event else None # type: ignore def to_dict(self): return (lambda o: o.__dict__)(self) @@ -164,7 +164,7 @@ class TrackPackageResult: package: Optional[Package] events: Optional[List[TrackingEvent]] = list() - def __init__(self, api_response: Dict[str, any], config: ShipEngineConfig) -> None: + def __init__(self, api_response: Dict[str, Any], config: ShipEngineConfig) -> None: """This object is used as the return type for the `track_package` and `track` methods.""" self.events = list() result = api_response["result"] diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index a7fc851..f38318d 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -1,5 +1,5 @@ """Validate a single address or multiple addresses.""" -from typing import Dict +from typing import Any, Dict from ..jsonrpc import rpc_request from ..models.address import Address, AddressValidateResult @@ -17,12 +17,12 @@ def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResul :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the validated and normalized address. """ - api_response: Dict[str, any] = rpc_request( + api_response: Dict[str, Any] = rpc_request( method=RPCMethods.ADDRESS_VALIDATE.value, config=config, - params={"address": address.to_dict()}, + params={"address": address.to_dict()}, # type: ignore ) - result: Dict[str, any] = api_response["result"] + result: Dict[str, Any] = api_response["result"] return AddressValidateResult( is_valid=result["isValid"], request_id=api_response["id"], diff --git a/shipengine_sdk/services/track_package.py b/shipengine_sdk/services/track_package.py index 99b30c6..2ae9edf 100644 --- a/shipengine_sdk/services/track_package.py +++ b/shipengine_sdk/services/track_package.py @@ -1,5 +1,5 @@ """Track a given package to obtain status updates on it's progression through the fulfillment cycle.""" -from typing import Dict, Union +from typing import Any, Dict, Union from ..errors import ShipEngineError from ..jsonrpc import rpc_request @@ -10,7 +10,7 @@ def track( - tracking_data: Union[str, Dict[str, any], TrackingQuery], config: ShipEngineConfig + tracking_data: Union[str, Dict[str, Any], TrackingQuery], config: ShipEngineConfig ) -> TrackPackageResult: if type(tracking_data) is str: is_package_id_valid(tracking_data) @@ -25,7 +25,7 @@ def track( if type(tracking_data) is TrackingQuery: api_response = rpc_request( - method=RPCMethods.TRACK_PACKAGE.value, config=config, params=tracking_data.to_dict() + method=RPCMethods.TRACK_PACKAGE.value, config=config, params=tracking_data.to_dict() # type: ignore ) return TrackPackageResult(api_response, config) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 63e1db9..8d3071b 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -1,5 +1,5 @@ """The entrypoint to the ShipEngine API SDK.""" -from typing import Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from .models import CarrierAccount, TrackingQuery, TrackPackageResult from .models.address import Address, AddressValidateResult @@ -17,7 +17,7 @@ class ShipEngine: unless specifically overridden when calling a method. """ - def __init__(self, config: Union[str, Dict[str, any], ShipEngineConfig]) -> None: + def __init__(self, config: Union[str, Dict[str, Any], ShipEngineConfig]) -> None: """ Exposes the functionality of the ShipEngine API. @@ -26,12 +26,12 @@ def __init__(self, config: Union[str, Dict[str, any], ShipEngineConfig]) -> None """ if type(config) is str: - self.config: ShipEngineConfig = ShipEngineConfig({"api_key": config}) + self.config = ShipEngineConfig({"api_key": config}) elif type(config) is dict: - self.config: ShipEngineConfig = ShipEngineConfig(config) + self.config = ShipEngineConfig(config) def validate_address( - self, address: Address, config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None + self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None ) -> AddressValidateResult: """ Validate an address in nearly any countryCode in the world. @@ -41,34 +41,34 @@ def validate_address( :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the validated and normalized address. """ - config: ShipEngineConfig = self.config.merge(new_config=config) + config = self.config.merge(new_config=config) return validate(address=address, config=config) def normalize_address( - self, address: Address, config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None + self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None ) -> Address: """Normalize a given address into a standardized format used by carriers.""" - config: ShipEngineConfig = self.config.merge(new_config=config) + config = self.config.merge(new_config=config) return normalize(address=address, config=config) def get_carrier_accounts( self, carrier_code: Optional[str] = None, - config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None, + config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None, ) -> List[CarrierAccount]: """Fetch a list of the carrier accounts connected to your ShipEngine Account.""" - config: ShipEngineConfig = self.config.merge(new_config=config) + config = self.config.merge(new_config=config) get_accounts = GetCarrierAccounts() return get_accounts.fetch_carrier_accounts(config=config, carrier_code=carrier_code) def track_package( self, - tracking_data: Union[str, Dict[str, any], TrackingQuery], - config: Optional[Union[Dict[str, any], ShipEngineConfig]] = None, + tracking_data: Union[str, Dict[str, Any], TrackingQuery], + config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None, ) -> TrackPackageResult: """ Track a package by `tracking_number` and `carrier_code` via the **TrackingQuery** object, by using just the **package_id**. """ - config: ShipEngineConfig = self.config.merge(new_config=config) + config = self.config.merge(new_config=config) return track(tracking_data=tracking_data, config=config) diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index 02dfcbd..5649b1b 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -1,6 +1,6 @@ """The global configuration object for the ShipEngine SDK.""" import json -from typing import Dict, Optional +from typing import Any, Dict, Optional from .models import Endpoints from .util import is_api_key_valid, is_retries_valid, is_timeout_valid @@ -19,7 +19,7 @@ class ShipEngineConfig: DEFAULT_TIMEOUT: int = 5 """Default timeout for the ShipEngineClient in seconds.""" - def __init__(self, config: Dict[str, any]) -> None: + def __init__(self, config: Dict[str, Any]) -> None: """ This is the configuration object for the ShipEngine object and it"s properties are used throughout this SDK. @@ -50,7 +50,7 @@ def __init__(self, config: Dict[str, any]) -> None: self.retries: int = self.DEFAULT_RETRIES # TODO: add event listener to config object once it"s implemented. - def merge(self, new_config: Optional[Dict[str, any]] = None): + def merge(self, new_config: Optional[Dict[str, Any]] = None): """ The method allows the merging of a method-level configuration adjustment into the current configuration. diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index bb296bc..c5f25de 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -1,6 +1,6 @@ """Assertion helper functions.""" import re -from typing import Dict, List +from typing import Any, Dict, List from ..errors import ( ClientSystemError, @@ -16,7 +16,7 @@ def is_street_valid(street: List[str]) -> None: - """Checks that street is not empty and that it is not too many address lines.""" + """Checks that street is not empty and that it is not too mAny address lines.""" if len(street) == 0: raise ValidationError( message="Invalid address. At least one address line is required.", @@ -89,7 +89,7 @@ def is_country_code_valid(country: str) -> None: ) -def is_api_key_valid(config: Dict[str, any]) -> None: +def is_api_key_valid(config: Dict[str, Any]) -> None: """ Check if API Key is set and is not empty or whitespace. @@ -115,7 +115,7 @@ def is_api_key_valid(config: Dict[str, any]) -> None: ) -def is_retries_valid(config: Dict[str, any]) -> None: +def is_retries_valid(config: Dict[str, Any]) -> None: """ Checks that config.retries is a valid value. @@ -132,7 +132,7 @@ def is_retries_valid(config: Dict[str, any]) -> None: ) -def is_timeout_valid(config: Dict[str, any]) -> None: +def is_timeout_valid(config: Dict[str, Any]) -> None: """ Checks that config.timeout is valid value. @@ -174,7 +174,7 @@ def timeout_validation_error_assertions(error) -> None: assert error.source is ErrorSource.SHIPENGINE.value -def is_response_404(status_code: int, response_body: Dict[str, any], config) -> None: +def is_response_404(status_code: int, response_body: Dict[str, Any], config) -> None: """Check if status_code is 404 and raises an error if so.""" if "error" in response_body and status_code == 404: error = response_body["error"] @@ -195,7 +195,7 @@ def is_response_404(status_code: int, response_body: Dict[str, any], config) -> ) -def is_response_429(status_code: int, response_body: Dict[str, any], config) -> None: +def is_response_429(status_code: int, response_body: Dict[str, Any], config) -> None: """Check if status_code is 429 and raises an error if so.""" if "error" in response_body and status_code == 429: error = response_body["error"] @@ -214,7 +214,7 @@ def is_response_429(status_code: int, response_body: Dict[str, any], config) -> ) -def is_response_500(status_code: int, response_body: Dict[str, any]) -> None: +def is_response_500(status_code: int, response_body: Dict[str, Any]) -> None: """Check if the status code is 500 and raises an error if so.""" if status_code == 500: error = response_body["error"] @@ -230,7 +230,7 @@ def is_response_500(status_code: int, response_body: Dict[str, any]) -> None: def does_normalized_address_have_errors(result) -> None: """ - Assertions to check if the returned normalized address has any errors. If errors + Assertions to check if the returned normalized address has Any errors. If errors are present an exception is thrown. :param AddressValidateResult result: The address validation response from ShipEngine API. From 74d889558ad30dcfe1e1414258eed2ad3a2a0fb8 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Mon, 14 Jun 2021 17:04:00 -0500 Subject: [PATCH 4/4] DX-1086-1096 track package tests --- shipengine_sdk/jsonrpc/__init__.py | 2 +- .../models/track_pacakge/__init__.py | 30 ++- shipengine_sdk/util/iso_string.py | 5 +- tests/services/test_track_package.py | 185 +++++++++++++++++- 4 files changed, 200 insertions(+), 22 deletions(-) diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py index 39dee0e..902dd94 100644 --- a/shipengine_sdk/jsonrpc/__init__.py +++ b/shipengine_sdk/jsonrpc/__init__.py @@ -42,4 +42,4 @@ def rpc_request_loop( else: raise err retry += 1 - return api_response + return api_response diff --git a/shipengine_sdk/models/track_pacakge/__init__.py b/shipengine_sdk/models/track_pacakge/__init__.py index db95781..9aab008 100644 --- a/shipengine_sdk/models/track_pacakge/__init__.py +++ b/shipengine_sdk/models/track_pacakge/__init__.py @@ -1,7 +1,7 @@ """Data objects to be used in the `track_package` and `track` methods.""" import json from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from dataclasses_json import LetterCase, dataclass_json # type: ignore @@ -18,11 +18,11 @@ class Shipment: account_id: Optional[str] = None carrier_account: Optional[CarrierAccount] = None carrier: Carrier - estimated_delivery_date: str - actual_delivery_date: str + estimated_delivery_date: Union[IsoString, str] + actual_delivery_date: Union[IsoString, str] def __init__( - self, shipment: Dict[str, Any], actual_delivery_date: str, config: ShipEngineConfig + self, shipment: Dict[str, Any], actual_delivery_date: IsoString, config: ShipEngineConfig ) -> None: """This object represents a given Shipment.""" self.config = config @@ -43,9 +43,7 @@ def __init__( else ShipEngineError("The carrierCode field was null from api response.") ) - self.estimated_delivery_date = IsoString( - iso_string=shipment["estimatedDelivery"] - ).to_string() + self.estimated_delivery_date = IsoString(iso_string=shipment["estimatedDelivery"]) self.actual_delivery_date = actual_delivery_date def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: @@ -58,11 +56,11 @@ def _get_carrier_account(self, carrier: str, account_id: str) -> CarrierAccount: for account in carrier_accounts: if account_id == account.account_id: target_carrier.append(account) + return target_carrier[0] else: raise ShipEngineError( message=f"accountID [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa ) - return target_carrier[0] def to_dict(self) -> Dict[str, Any]: if hasattr(self, "config"): @@ -123,8 +121,8 @@ class Location: class TrackingEvent: - date_time: str - carrier_date_time: str + date_time: Union[IsoString, str] + carrier_date_time: Union[IsoString, str] status: str description: Optional[str] carrier_status_code: Optional[str] @@ -134,15 +132,9 @@ class TrackingEvent: def __init__(self, event: Dict[str, Any]) -> None: """Tracking event object.""" - if IsoString(event["timestamp"]).has_timezone(): - self.date_time = IsoString(iso_string=event["timestamp"]).to_string() - else: - self.date_time = IsoString(event["timestamp"]).to_string() + self.date_time = IsoString(iso_string=event["timestamp"]) - if IsoString(iso_string=event["carrierTimestamp"]).has_timezone(): - self.carrier_date_time = IsoString(iso_string=event["carrierTimestamp"]).to_string() - else: - self.carrier_date_time = IsoString(event["carrierTimestamp"]).to_string() + self.carrier_date_time = IsoString(iso_string=event["carrierTimestamp"]) self.status = event["status"] self.description = event["description"] if "description" in event else None @@ -150,7 +142,7 @@ def __init__(self, event: Dict[str, Any]) -> None: event["carrierStatusCode"] if "carrierStatusCode" in event else None ) self.signer = event["signer"] if "signer" in event else None - self.location = Location.from_dict(event["location"]) if "location" in event else None # type: ignore + self.location = Location.from_dict(event["location"]) if "location" in event else None def to_dict(self): return (lambda o: o.__dict__)(self) diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index da5623d..4966d20 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -25,7 +25,10 @@ def to_string(self) -> str: return self.iso_string def to_datetime_object(self) -> datetime: - return datetime.strptime(self.iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") + if self.has_timezone(): + return datetime.strptime(self.iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") + elif self.is_valid_iso_string_no_tz(self.iso_string): + return datetime.fromisoformat(self.iso_string) def has_timezone(self) -> bool: if self.is_valid_iso_string(self.iso_string): diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py index 7f9c886..32b13fe 100644 --- a/tests/services/test_track_package.py +++ b/tests/services/test_track_package.py @@ -1,9 +1,60 @@ """Testing the `track_package` method of the ShipEngine SDK.""" -from shipengine_sdk.models import TrackingQuery +from typing import List + +from shipengine_sdk.models import TrackingQuery, TrackPackageResult from tests.util.test_helpers import configurable_stub_shipengine_instance, stub_config +def assertions_on_delivered_after_exception_or_multiple_attempts( + tracking_result: TrackPackageResult, +) -> None: + track_package_assertions(tracking_result=tracking_result) + does_delivery_date_match(tracking_result) + assert_events_in_order(tracking_result.events) + assert len(tracking_result.events) == 8 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + assert tracking_result.events[2].status == "unknown" + assert tracking_result.events[3].status == "in_transit" + assert tracking_result.events[4].status == "exception" + assert tracking_result.events[5].status == "exception" + assert tracking_result.events[6].status == "in_transit" + assert tracking_result.events[7].status == "delivered" + assert tracking_result.events[-1].status == "delivered" + + +def does_delivery_date_match(tracking_result: TrackPackageResult) -> None: + """Check that the delivery dates for a given tracking response match.""" + assert ( + tracking_result.shipment.actual_delivery_date.to_datetime_object() + == tracking_result.events[-1].date_time.to_datetime_object() + ) + + +def assert_events_in_order(events: List) -> None: + """""" + previous_date_time = events[0].date_time + for event in events: + assert event.date_time.to_datetime_object() >= previous_date_time.to_datetime_object() + previous_date_time = event.date_time + + +def track_package_assertions(tracking_result: TrackPackageResult) -> None: + """""" + carrier_account_carrier_code = tracking_result.shipment.carrier_account.carrier["code"] + carrier_code = tracking_result.shipment.carrier["code"] + estimated_delivery = tracking_result.shipment.estimated_delivery_date + + assert carrier_account_carrier_code is not None + assert type(carrier_account_carrier_code) is str + assert carrier_code is not None + assert type(carrier_code) is str + assert estimated_delivery.has_timezone() is True + + class TestTrackPackage: + _PACKAGE_ID_FEDEX_ACCEPTED: str = "pkg_1FedExAccepted" + def test_track_by_tracking_number_and_carrier_code(self) -> None: """DX-1084 - Test track by tracking number and carrier code.""" shipengine = configurable_stub_shipengine_instance(stub_config()) @@ -14,3 +65,135 @@ def test_track_by_tracking_number_and_carrier_code(self) -> None: assert tracking_data.tracking_number == tracking_result.package.tracking_number assert tracking_result.package.tracking_url is not None assert type(tracking_result.package.tracking_url) is str + + def test_track_by_package_id(self) -> None: + """DX-1086 - Test track by package ID.""" + package_id = self._PACKAGE_ID_FEDEX_ACCEPTED + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + assert tracking_result.package.package_id == package_id + assert tracking_result.package.tracking_number is not None + assert tracking_result.package.tracking_url is not None + assert tracking_result.shipment.shipment_id is not None + assert tracking_result.shipment.account_id is not None + + def test_initial_scan_tracking_event(self) -> None: + """DX-1088 - Test initial scan tracking event.""" + package_id = self._PACKAGE_ID_FEDEX_ACCEPTED + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert len(tracking_result.events) == 1 + assert tracking_result.events[0].status == "accepted" + + def test_out_for_delivery_tracking_event(self) -> None: + """DX-1089 - Test out for delivery tracking event.""" + package_id = "pkg_1FedExAttempted" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert len(tracking_result.events) == 5 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + + def test_multiple_delivery_attempts(self) -> None: + """DX-1090 - Test multiple delivery attempt events.""" + package_id = "pkg_1FedexDeLiveredAttempted" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert len(tracking_result.events) == 9 + assert_events_in_order(tracking_result.events) + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + assert tracking_result.events[2].status == "unknown" + assert tracking_result.events[3].status == "in_transit" + assert tracking_result.events[4].status == "attempted_delivery" + assert tracking_result.events[5].status == "in_transit" + assert tracking_result.events[6].status == "attempted_delivery" + assert tracking_result.events[7].status == "in_transit" + assert tracking_result.events[8].status == "delivered" + + def test_delivered_on_first_try(self) -> None: + """DX-1091 - Test delivered on first try tracking event.""" + package_id = "pkg_1FedExDeLivered" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert ( + tracking_result.shipment.actual_delivery_date.to_datetime_object() + == tracking_result.events[4].date_time.to_datetime_object() + ) + does_delivery_date_match(tracking_result) + assert_events_in_order(tracking_result.events) + assert len(tracking_result.events) == 5 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + assert tracking_result.events[4].status == "delivered" + assert tracking_result.events[-1].status == "delivered" + + def test_delivered_with_signature(self) -> None: + """DX-1092 - Test track delivered with signature event.""" + package_id = "pkg_1FedExDeLivered" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + does_delivery_date_match(tracking_result) + assert_events_in_order(tracking_result.events) + assert len(tracking_result.events) == 5 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + assert tracking_result.events[2].status == "unknown" + assert tracking_result.events[3].status == "in_transit" + assert tracking_result.events[4].status == "delivered" + assert tracking_result.events[-1].status == "delivered" + assert tracking_result.events[-1].signer is not None + assert type(tracking_result.events[-1].signer) is str + + def test_delivered_after_multiple_attempts(self) -> None: + """DX-1093 - Test delivered after multiple attempts tracking event.""" + package_id = "pkg_1FedexDeLiveredException" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + assertions_on_delivered_after_exception_or_multiple_attempts(tracking_result) + + def test_delivered_after_exception(self) -> None: + """DX-1094 - Test delivered after exception tracking event.""" + package_id = "pkg_1FedexDeLiveredException" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + assertions_on_delivered_after_exception_or_multiple_attempts(tracking_result) + + def test_single_exception_tracking_event(self) -> None: + """DX-1095 - Test single exception tracking event.""" + package_id = "pkg_1FedexException" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert_events_in_order(tracking_result.events) + assert len(tracking_result.events) == 3 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[1].status == "in_transit" + assert tracking_result.events[2].status == "exception" + + def test_track_with_multiple_exceptions(self) -> None: + """DX-1096 - Test track with multiple exceptions.""" + package_id = "pkg_1FedexDeLiveredException" + shipengine = configurable_stub_shipengine_instance(stub_config()) + tracking_result = shipengine.track_package(tracking_data=package_id) + + track_package_assertions(tracking_result=tracking_result) + assert_events_in_order(tracking_result.events) + assert len(tracking_result.events) == 8 + assert tracking_result.events[0].status == "accepted" + assert tracking_result.events[4].status == "exception" + assert tracking_result.events[5].status == "exception" + assert tracking_result.events[7].status == "delivered" + assert tracking_result.events[-1].status == "delivered"