Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
per-file-ignores =
# imported but unused
__init__.py: F401
max-line-length = 110
max-line-length = 120
ignore =
# line break before binary operator
W503
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python package
name: ShipEngine SDK

on:
push:
Expand Down
3 changes: 0 additions & 3 deletions shipengine_sdk/http_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ def send_rpc_request(
"""
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.

TODO: add param and return docs
"""
# TODO: debug the below base_uri variable to verify ternary logic works as intended.
client: Session = self._request_retry_session(retries=config.retries)
base_uri: Optional[str] = (
config.base_uri
Expand Down
5 changes: 1 addition & 4 deletions shipengine_sdk/jsonrpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
def rpc_request(
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
"""
"""Create and send a `JSON-RPC 2.0` request over HTTP messages."""
return rpc_request_loop(method, params, config)


Expand Down
38 changes: 28 additions & 10 deletions shipengine_sdk/models/package/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,34 @@ class TrackingQuery:
tracking_number: str


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Location:
city_locality: Optional[str] = None
state_province: Optional[str] = None
postal_code: Optional[str] = None
country_code: Optional[str] = None
city_locality: Optional[str]
state_province: Optional[str]
postal_code: Optional[str]
country_code: Optional[str]
latitude: Optional[float] = None
longitude: Optional[float] = None

def __init__(self, location_data: Dict[str, Any]) -> None:
self.city_locality = (
location_data["cityLocality"] if "cityLocality" in location_data else None
)
self.state_province = (
location_data["stateProvince"] if "stateProvince" in location_data else None
)
self.postal_code = location_data["postalCode"] if "postalCode" in location_data else None
self.country_code = location_data["countryCode"] if "countryCode" in location_data else None

if "coordinates" in location_data:
self.latitude = location_data["coordinates"]["latitude"]
self.longitude = location_data["coordinates"]["longitude"]

def to_dict(self):
return (lambda o: o.__dict__)(self)

def to_json(self):
return json.dumps(self, default=lambda o: o.__dict__, indent=2)


class TrackingEvent:
date_time: Union[IsoString, str]
Expand All @@ -142,7 +160,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(event["location"]) if "location" in event else None

def to_dict(self):
return (lambda o: o.__dict__)(self)
Expand Down Expand Up @@ -174,19 +192,19 @@ def __init__(self, api_response: Dict[str, Any], config: ShipEngineConfig) -> No
)
self.package = Package(result["package"]) if "package" in result else None

def get_errors(self) -> List[TrackingEvent]: # TODO: debug
def get_errors(self) -> List[TrackingEvent]:
"""Returns **only** the exception events."""
errors: List[TrackingEvent] = list()
for event in self.events:
if event.status == "exception":
errors.append(event)
return errors

def get_latest_event(self) -> TrackingEvent: # TODO: debug
def get_latest_event(self) -> TrackingEvent:
"""Returns the latest event to have occurred in the `events` list."""
return self.events[-1]

def has_errors(self) -> bool: # TODO: debug
def has_errors(self) -> bool:
"""Returns `true` if there are any exception events."""
for event in self.events:
if event.status == "exception":
Expand Down
15 changes: 14 additions & 1 deletion shipengine_sdk/util/sdk_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,5 +271,18 @@ def is_package_id_valid(package_id: str) -> None:
"""Checks that package_id is valid."""
pattern = re.compile(r"^pkg_[1-9a-zA-Z]+$")

if not package_id.startswith("pkg_"):
raise ValidationError(
message=f"[{package_id[0:4]}] is not a valid package ID prefix.",
source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.INVALID_IDENTIFIER.value,
)

if not pattern.match(package_id):
raise ValidationError(message=f"[{package_id}] is not a valid package ID.")
raise ValidationError(
message=f"[{package_id}] is not a valid package ID.",
source=ErrorSource.SHIPENGINE.value,
error_type=ErrorType.VALIDATION.value,
error_code=ErrorCode.INVALID_IDENTIFIER.value,
)
121 changes: 120 additions & 1 deletion tests/services/test_track_package.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
"""Testing the `track_package` method of the ShipEngine SDK."""
from typing import List

from shipengine_sdk.models import TrackingQuery, TrackPackageResult
from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError
from shipengine_sdk.models import (
ErrorCode,
ErrorSource,
ErrorType,
TrackingEvent,
TrackingQuery,
TrackPackageResult,
)
from tests.util.test_helpers import configurable_stub_shipengine_instance, stub_config


Expand Down Expand Up @@ -55,6 +63,14 @@ def track_package_assertions(tracking_result: TrackPackageResult) -> None:
assert estimated_delivery.has_timezone() is True


def date_time_assertions(event: TrackingEvent) -> None:
"""Check that date_time has a timezone."""
assert event.date_time is not None
assert event.carrier_date_time is not None
assert event.date_time.has_timezone() is True
assert event.carrier_date_time.has_timezone() is False


class TestTrackPackage:
_PACKAGE_ID_FEDEX_ACCEPTED: str = "pkg_1FedExAccepted"
_PACKAGE_ID_FEDEX_DELIVERED: str = "pkg_1FedExDeLivered"
Expand Down Expand Up @@ -202,3 +218,106 @@ def test_track_with_multiple_exceptions(self) -> None:
assert tracking_result.events[5].status == "exception"
assert tracking_result.events[7].status == "delivered"
assert tracking_result.events[-1].status == "delivered"

def test_multiple_locations_in_tracking_event(self) -> None:
"""DX-1097 - Test track package with multiple locations in tracking event."""
package_id = "pkg_Attempted"
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 tracking_result.events[0].location is None
assert tracking_result.events[1].location is None
assert type(tracking_result.events[2].location.latitude) is float
assert type(tracking_result.events[2].location.longitude) is float
assert tracking_result.events[4].location.latitude is None
assert tracking_result.events[4].location.longitude is None

def test_carrier_date_time_without_timezone(self) -> None:
"""DX-1098 - Test track package where carrierDateTime has no timezone."""
package_id = "pkg_Attempted"
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) == 5
for event in tracking_result.events:
date_time_assertions(event=event)

def test_invalid_tracking_number(self) -> None:
"""DX-1099 - Test track package with an invalid tracking number."""
tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="abc123")
shipengine = configurable_stub_shipengine_instance(stub_config())
try:
shipengine.track_package(tracking_data=tracking_data)
except ShipEngineError as err:
assert type(err) is ClientSystemError
assert err.request_id is not None
assert err.request_id.startswith("req_")
assert err.source == ErrorSource.CARRIER.value
assert err.error_type == ErrorType.BUSINESS_RULES.value
assert err.error_code == ErrorCode.INVALID_IDENTIFIER.value
assert (
err.message
== f"{tracking_data.tracking_number} is not a valid fedex tracking number."
)

def test_invalid_package_id_prefix(self) -> None:
"""DX-1100 - Test track package with invalid package_id prefix."""
package_id = "car_1FedExAccepted"
shipengine = configurable_stub_shipengine_instance(stub_config())
try:
shipengine.track_package(tracking_data=package_id)
except ShipEngineError as err:
assert type(err) is ValidationError
assert err.request_id is None
assert err.source is ErrorSource.SHIPENGINE.value
assert err.error_type is ErrorType.VALIDATION.value
assert err.error_code is ErrorCode.INVALID_IDENTIFIER.value
assert err.message == f"[{package_id[0:4]}] is not a valid package ID prefix."

def test_invalid_package_id(self) -> None:
"""DX-1101 - Test track package with invalid package_id."""
package_id = "pkg_12!@3a s567"
shipengine = configurable_stub_shipengine_instance(stub_config())
try:
shipengine.track_package(tracking_data=package_id)
except ShipEngineError as err:
assert type(err) is ValidationError
assert err.request_id is None
assert err.source is ErrorSource.SHIPENGINE.value
assert err.error_type is ErrorType.VALIDATION.value
assert err.error_code is ErrorCode.INVALID_IDENTIFIER.value
assert err.message == f"[{package_id}] is not a valid package ID."

def test_package_id_not_found(self) -> None:
"""DX-1102 - Test track package where package ID cannot be found."""
package_id = "pkg_123"
shipengine = configurable_stub_shipengine_instance(stub_config())
try:
shipengine.track_package(tracking_data=package_id)
except ShipEngineError as err:
assert type(err) is ClientSystemError
assert err.request_id is not None
assert err.request_id.startswith("req_")
assert err.source == ErrorSource.SHIPENGINE.value
assert err.error_type == ErrorType.VALIDATION.value
assert err.error_code == ErrorCode.INVALID_IDENTIFIER.value
assert err.message == f"Package ID {package_id} does not exist."

def test_server_side_error(self) -> None:
"""DX-1103 - Test track package server-side error."""
tracking_data = TrackingQuery(carrier_code="fedex", tracking_number="500 Server Error")
shipengine = configurable_stub_shipengine_instance(stub_config())
try:
shipengine.track_package(tracking_data=tracking_data)
except ShipEngineError as err:
assert type(err) is ClientSystemError
assert err.request_id is not None
assert err.request_id.startswith("req_")
assert err.source == ErrorSource.SHIPENGINE.value
assert err.error_type == ErrorType.SYSTEM.value
assert err.error_code == ErrorCode.UNSPECIFIED.value
assert err.message == "Unable to connect to the database"