From b413104f1e1e2945e0a5e5ad3eec74650d6c0192 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Mon, 2 Aug 2021 18:19:57 -0500 Subject: [PATCH 01/13] work in progress - refactoring client and removing models and jsonrpc tools --- shipengine_sdk/async_http_client/__init__.py | 1 - shipengine_sdk/{models => }/enums/__init__.py | 38 +- shipengine_sdk/{models => }/enums/country.py | 0 .../{models => }/enums/error_code.py | 0 .../{models => }/enums/error_source.py | 0 .../{models => }/enums/error_type.py | 0 .../{models => }/enums/regex_patterns.py | 0 shipengine_sdk/errors/__init__.py | 7 +- shipengine_sdk/events/__init__.py | 203 --------- shipengine_sdk/http_client/client.py | 164 +++---- shipengine_sdk/jsonrpc/__init__.py | 42 -- shipengine_sdk/jsonrpc/process_request.py | 88 ---- shipengine_sdk/models/__init__.py | 24 - shipengine_sdk/models/address/__init__.py | 83 ---- shipengine_sdk/models/carriers/__init__.py | 57 --- .../models/enums/carriers/__init__.py | 3 - .../models/enums/carriers/carrier_names.py | 63 --- .../models/enums/carriers/carriers.py | 63 --- shipengine_sdk/models/package/__init__.py | 264 ----------- shipengine_sdk/services/address_validation.py | 3 +- shipengine_sdk/shipengine.py | 102 +++-- shipengine_sdk/util/iso_string.py | 2 +- shipengine_sdk/util/sdk_assertions.py | 3 +- tests/errors/test_errors.py | 169 ------- tests/events/__init__.py | 1 - tests/events/test_emitted_events.py | 238 ---------- tests/http_client/test_http_client.py | 97 ---- tests/jsonrpc/test_process_request.py | 90 ---- tests/models/__init__.py | 1 - tests/models/address/test_address.py | 39 -- tests/models/carriers/test_carrier.py | 15 - tests/models/carriers/test_carrier_account.py | 39 -- tests/models/track_package/__init__.py | 1 - tests/models/track_package/test_package.py | 29 -- tests/models/track_package/test_shipment.py | 69 --- .../test_track_package_result.py | 148 ------ .../track_package/test_tracking_event.py | 30 -- tests/services/__init__.py | 1 - tests/services/test_address_validation.py | 263 ----------- tests/services/test_get_carrier_accounts.py | 72 --- tests/services/test_normalize_address.py | 190 -------- tests/services/test_track_package.py | 318 ------------- tests/test_shipengine.py | 54 +-- tests/test_shipengine_config.py | 2 +- ...init__.py => test_snake_case_converter.py} | 0 tests/util/__init__.py | 2 - tests/util/test_helpers.py | 421 ------------------ tests/util/test_iso_string.py | 22 - 48 files changed, 172 insertions(+), 3349 deletions(-) delete mode 100644 shipengine_sdk/async_http_client/__init__.py rename shipengine_sdk/{models => }/enums/__init__.py (55%) rename shipengine_sdk/{models => }/enums/country.py (100%) rename shipengine_sdk/{models => }/enums/error_code.py (100%) rename shipengine_sdk/{models => }/enums/error_source.py (100%) rename shipengine_sdk/{models => }/enums/error_type.py (100%) rename shipengine_sdk/{models => }/enums/regex_patterns.py (100%) delete mode 100644 shipengine_sdk/events/__init__.py delete mode 100644 shipengine_sdk/jsonrpc/__init__.py delete mode 100644 shipengine_sdk/jsonrpc/process_request.py delete mode 100644 shipengine_sdk/models/__init__.py delete mode 100644 shipengine_sdk/models/address/__init__.py delete mode 100644 shipengine_sdk/models/carriers/__init__.py delete mode 100644 shipengine_sdk/models/enums/carriers/__init__.py delete mode 100644 shipengine_sdk/models/enums/carriers/carrier_names.py delete mode 100644 shipengine_sdk/models/enums/carriers/carriers.py delete mode 100644 shipengine_sdk/models/package/__init__.py delete mode 100644 tests/errors/test_errors.py delete mode 100644 tests/events/__init__.py delete mode 100644 tests/events/test_emitted_events.py delete mode 100644 tests/http_client/test_http_client.py delete mode 100644 tests/jsonrpc/test_process_request.py delete mode 100644 tests/models/__init__.py delete mode 100644 tests/models/address/test_address.py delete mode 100644 tests/models/carriers/test_carrier.py delete mode 100644 tests/models/carriers/test_carrier_account.py delete mode 100644 tests/models/track_package/__init__.py delete mode 100644 tests/models/track_package/test_package.py delete mode 100644 tests/models/track_package/test_shipment.py delete mode 100644 tests/models/track_package/test_track_package_result.py delete mode 100644 tests/models/track_package/test_tracking_event.py delete mode 100644 tests/services/__init__.py delete mode 100644 tests/services/test_address_validation.py delete mode 100644 tests/services/test_get_carrier_accounts.py delete mode 100644 tests/services/test_normalize_address.py delete mode 100644 tests/services/test_track_package.py rename tests/{test___init__.py => test_snake_case_converter.py} (100%) delete mode 100644 tests/util/__init__.py delete mode 100644 tests/util/test_helpers.py delete mode 100644 tests/util/test_iso_string.py diff --git a/shipengine_sdk/async_http_client/__init__.py b/shipengine_sdk/async_http_client/__init__.py deleted file mode 100644 index 0cb716b..0000000 --- a/shipengine_sdk/async_http_client/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Initial Docstring.""" diff --git a/shipengine_sdk/models/enums/__init__.py b/shipengine_sdk/enums/__init__.py similarity index 55% rename from shipengine_sdk/models/enums/__init__.py rename to shipengine_sdk/enums/__init__.py index be38bf1..d81201e 100644 --- a/shipengine_sdk/models/enums/__init__.py +++ b/shipengine_sdk/enums/__init__.py @@ -1,7 +1,6 @@ """ShipEngine SDK Enumerations""" from enum import Enum -from .carriers import CarrierNames, Carriers from .country import Country from .error_code import ErrorCode from .error_source import ErrorSource @@ -9,33 +8,32 @@ from .regex_patterns import RegexPatterns -class Constants(Enum): - """Test API Key for use with Simengine.""" +class BaseURL(Enum): + """API Endpoint URI's used throughout the ShipEngine SDK.""" - STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs" - CARRIER_ACCOUNT_ID_STUB = "car_41GrQHn5uouiPZc2TNE6PU29tZU9ud" + SHIPENGINE_RPC_URL = "https://api.shipengine.com/" -class Endpoints(Enum): - """API Endpoint URI's used throughout the ShipEngine SDK.""" +class Constants(Enum): + """Test API Key for use with Simengine.""" - SHIPENGINE_RPC_URL = "https://api.shipengine.com/jsonrpc" + STUB_API_KEY = "TEST_vMiVbICUjBz4BZjq0TRBLC/9MrxY4+yjvb1G1RMxlJs" -class Events(Enum): - """ShipEngine Events emitted by the SDK when a request is sent or when a response is received.""" +class HTTPVerbs(Enum): + """A collection of HTTP verbs used in requests to ShipEngine API.""" - ON_REQUEST_SENT = "on_request_sent" - ON_RESPONSE_RECEIVED = "on_response_received" + GET = "GET" + DELETE = "DELETE" + POST = "POST" + PUT = "PUT" -class RPCMethods(Enum): +class Endpoints(Enum): """A collection of RPC Methods used throughout the ShipEngine SDK.""" - ADDRESS_VALIDATE = "address.validate.v1" - CREATE_TAG = "create.tag.v1" - LIST_CARRIERS = "carrier.listAccounts.v1" - TRACK_PACKAGE = "package.track.v1" + ADDRESSES_VALIDATE = "v1/addresses/validate" + LIST_CARRIERS = "v1/carriers" def does_member_value_exist(m: str, enum_to_search) -> bool: @@ -46,9 +44,3 @@ def does_member_value_exist(m: str, enum_to_search) -> bool: :param enum_to_search: The enumeration to check the member value against. """ return False if m not in (member.value for member in enum_to_search) else True - - -def get_carrier_name_value(upper_carrier_code: str): - for k in CarrierNames: - if upper_carrier_code == k.name: - return k.value diff --git a/shipengine_sdk/models/enums/country.py b/shipengine_sdk/enums/country.py similarity index 100% rename from shipengine_sdk/models/enums/country.py rename to shipengine_sdk/enums/country.py diff --git a/shipengine_sdk/models/enums/error_code.py b/shipengine_sdk/enums/error_code.py similarity index 100% rename from shipengine_sdk/models/enums/error_code.py rename to shipengine_sdk/enums/error_code.py diff --git a/shipengine_sdk/models/enums/error_source.py b/shipengine_sdk/enums/error_source.py similarity index 100% rename from shipengine_sdk/models/enums/error_source.py rename to shipengine_sdk/enums/error_source.py diff --git a/shipengine_sdk/models/enums/error_type.py b/shipengine_sdk/enums/error_type.py similarity index 100% rename from shipengine_sdk/models/enums/error_type.py rename to shipengine_sdk/enums/error_type.py diff --git a/shipengine_sdk/models/enums/regex_patterns.py b/shipengine_sdk/enums/regex_patterns.py similarity index 100% rename from shipengine_sdk/models/enums/regex_patterns.py rename to shipengine_sdk/enums/regex_patterns.py diff --git a/shipengine_sdk/errors/__init__.py b/shipengine_sdk/errors/__init__.py index d771528..08d4809 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine_sdk/errors/__init__.py @@ -2,7 +2,12 @@ import json from typing import Optional -from ..models.enums import ErrorCode, ErrorSource, ErrorType, does_member_value_exist +from shipengine_sdk.enums import ( + ErrorCode, + ErrorSource, + ErrorType, + does_member_value_exist, +) class ShipEngineError(Exception): diff --git a/shipengine_sdk/events/__init__.py b/shipengine_sdk/events/__init__.py deleted file mode 100644 index d0a5cde..0000000 --- a/shipengine_sdk/events/__init__.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -ShipEngine event emission via Observer Pattern. The ShipEngine SDK emits when an -HTTP request is sent and when an HTTP response is received for said request. -""" -import json -from dataclasses import dataclass -from datetime import datetime -from typing import Any, Callable, Dict, List, Optional, Union - -from dataclasses_json import dataclass_json - -from ..errors import ShipEngineError -from ..models.enums import Events - - -class ShipEngineEvent: - timestamp: datetime - - def __init__(self, event_type: str, message: str) -> None: - self.timestamp = datetime.now() - self.type = event_type - self.message = message - - 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) - - @staticmethod - def new_event_message(method: str, base_uri: str, message_type: str) -> str: - """A method to dynamically create an event message based on the $messageType being passed in.""" - if message_type == "base_message": - return f"Calling the ShipEngine {method} API at {base_uri}" - elif message_type == "retry_message": - return f"Retrying the ShipEngine {method} API at {base_uri}" - else: - raise ShipEngineError(f"Message type [{message_type}] is not a valid type of message.") - - -class RequestSentEvent(ShipEngineEvent): - REQUEST_SENT = "request_sent" - - def __init__( - self, - request_id: str, - message: str, - base_uri: str, - headers: List[str], - body: Dict[str, Any], - retry: int, - timeout: int, - ) -> None: - super().__init__(event_type=self.REQUEST_SENT, message=message) - self.request_id = request_id - self.base_uri = base_uri - self.headers = headers - self.body = body - self.retry = retry - self.timeout = timeout - - -class ResponseReceivedEvent(ShipEngineEvent): - RESPONSE_RECEIVED = "response_received" - - def __init__( - self, - message: str, - request_id: str, - base_uri: str, - status_code: int, - headers: List[str], - body: Dict[str, Any], - retry: int, - elapsed: float, - ) -> None: - super().__init__(event_type=self.RESPONSE_RECEIVED, message=message) - self.request_id = request_id - self.base_uri = base_uri - self.status_code = status_code - self.headers = headers - self.body = body - self.retry = retry - self.elapsed = elapsed - - -class Dispatcher: - def __init__(self, events: Optional[List[str]] = None) -> None: - events_list = [Events.ON_REQUEST_SENT.value, Events.ON_RESPONSE_RECEIVED.value] - if events: - for i in events: - events_list.append(i) - - self.events = {event: dict() for event in events_list} - - def get_subscribers(self, event: Optional[str] = None): - return self.events[event] - - def register(self, event, subscriber, callback: Union[Callable, str] = None): - if event is Events.ON_REQUEST_SENT.value and callback is None: - callback = getattr(subscriber, "catch_request_sent_event") - elif event is Events.ON_RESPONSE_RECEIVED.value and callback is None: - callback = getattr(subscriber, "catch_response_received_event") - self.get_subscribers(event)[subscriber] = callback - - def unregister(self, event, subscriber): - del self.get_subscribers(event)[subscriber] - - def dispatch(self, event, event_name: str = None): - for subscriber, callback in self.get_subscribers(event_name).items(): - callback(event) - - 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 Subscriber: - def __init__(self, name=None) -> None: - if name is not None: - self.name = name - else: - self.name = "Event Subscriber" - - @staticmethod - def update(event: Union[RequestSentEvent, ResponseReceivedEvent]): - return event - - @staticmethod - def catch_request_sent_event(event: RequestSentEvent): - return event - - @staticmethod - def catch_response_received_event(event: ResponseReceivedEvent): - return event - - 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 ShipEngineEventListener(Subscriber): - def __init__(self, name=None) -> None: - super().__init__(name=name) - - # You can add your own event consumption logic by adding/overriding the parent `update()` method below. - @staticmethod - def update(event: Union[RequestSentEvent, ResponseReceivedEvent]): - # print(event.to_dict()) - return event - - -def emit_event(emitted_event_type: str, event_data, dispatcher: Dispatcher): - if emitted_event_type == RequestSentEvent.REQUEST_SENT: - request_sent_event = RequestSentEvent( - message=event_data.message, - request_id=event_data.id, - base_uri=event_data.base_uri, - headers=event_data.request_headers, - body=event_data.body, - retry=event_data.retry, - timeout=event_data.timeout, - ) - dispatcher.dispatch(event=request_sent_event, event_name=Events.ON_REQUEST_SENT.value) - return request_sent_event - elif emitted_event_type == ResponseReceivedEvent.RESPONSE_RECEIVED: - response_received_event = ResponseReceivedEvent( - message=event_data.message, - request_id=event_data.id, - base_uri=event_data.base_uri, - status_code=event_data.status_code, - headers=event_data.response_headers, - body=event_data.body, - retry=event_data.retry, - elapsed=event_data.elapsed, - ) - dispatcher.dispatch( - event=response_received_event, event_name=Events.ON_RESPONSE_RECEIVED.value - ) - return response_received_event - else: - raise ShipEngineError(f"Event type [{emitted_event_type}] is not a valid type of event.") - - -@dataclass_json -@dataclass -class EventOptions: - """To be used as the main argument in the **emitEvent()** function.""" - - message: Optional[str] - id: Optional[str] - base_uri: Optional[str] - body: Optional[Dict[str, Any]] - retry: Optional[int] - status_code: Optional[int] = None - request_headers: Optional[Dict[str, Any]] = None - response_headers: Any = None - timeout: Optional[int] = None - elapsed: Optional[float] = None diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 7bc08ad..2f13c39 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 datetime import datetime +import time from typing import Any, Dict, Optional import requests @@ -13,49 +13,16 @@ from shipengine_sdk import __version__ -from ..errors import ShipEngineError -from ..events import ( - Dispatcher, - EventOptions, - RequestSentEvent, - ResponseReceivedEvent, - ShipEngineEvent, - emit_event, -) -from ..jsonrpc.process_request import handle_response, wrap_request +from ..enums import HTTPVerbs +from ..errors import RateLimitExceededError, ShipEngineError from ..models import ErrorCode, ErrorSource, ErrorType -from ..models.enums import Events from ..shipengine_config import ShipEngineConfig -from ..util.sdk_assertions import check_response_for_errors def base_url(config) -> str: return config.base_uri if os.getenv("CLIENT_BASE_URI") is None else os.getenv("CLIENT_BASE_URI") -def generate_event_message( - retry: int, - method: str, - base_uri: str, - status_code: Optional[int] = None, - message_type: Optional[str] = None, -) -> str: - if message_type == "received": - if retry > 0: - f"Retrying the ShipEngine {method} API at {base_uri}" - else: - return f"Received an HTTP {status_code} response from the ShipEngine {method} API" - - if retry == 0: - return ShipEngineEvent.new_event_message( - method=method, base_uri=base_uri, message_type="base_message" - ) - else: - return ShipEngineEvent.new_event_message( - method=method, base_uri=base_uri, message_type="retry_message" - ) - - def request_headers(user_agent: str, api_key: str) -> Dict[str, Any]: return { "User-Agent": user_agent, @@ -76,20 +43,64 @@ def __call__(self, request: Request, *args, **kwargs) -> Request: class ShipEngineClient: - _DISPATCHER: Dispatcher = Dispatcher() - - def __init__(self, config: ShipEngineConfig) -> None: + def __init__(self) -> None: """A `JSON-RPC 2.0` HTTP client used to send all HTTP requests from the SDK.""" - self._DISPATCHER.register( - event=Events.ON_REQUEST_SENT.value, subscriber=config.event_listener + self.session = requests.session() + + def get(self, endpoint: str, config: ShipEngineConfig): + """Send an HTTP GET request.""" + return self._request_loop( + http_method=HTTPVerbs.GET.value, endpoint=endpoint, params=None, config=config ) - self._DISPATCHER.register( - event=Events.ON_RESPONSE_RECEIVED.value, subscriber=config.event_listener + + def post( + self, endpoint: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None + ): + """Send an HTTP POST request.""" + return self._request_loop( + http_method=HTTPVerbs.POST.value, endpoint=endpoint, params=params, config=config ) - self.session = requests.session() - def send_rpc_request( - self, method: str, params: Optional[Dict[str, Any]], retry: int, config: ShipEngineConfig + def delete(self, endpoint: str, config: ShipEngineConfig): + """Send an HTTP DELETE request.""" + return self._request_loop( + http_method=HTTPVerbs.DELETE.value, endpoint=endpoint, params=None, config=config + ) + + def put(self, endpoint: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None): + """Send an HTTP PUT request.""" + return self._request_loop( + http_method=HTTPVerbs.PUT.value, endpoint=endpoint, params=params, config=config + ) + + def _request_loop( + self, + http_method: str, + endpoint: str, + params: Optional[Dict[str, Any]], + config: ShipEngineConfig, + ) -> Dict[str, Any]: + retry: int = 0 + while retry <= config.retries: + try: + api_response = self._send_request( + http_method=http_method, body=params, retry=retry, config=config + ) + except Exception as err: + if ( + retry < config.retries + and type(err) is RateLimitExceededError + and err.retry_after < config.timeout + ): + time.sleep(err.retry_after) + retry += 1 + continue + else: + raise err + return api_response + + def _send_request( + self, http_method: str, body: 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 @@ -98,76 +109,31 @@ def send_rpc_request( client: Session = self._request_retry_session(retries=config.retries) base_uri = base_url(config=config) - request_body: Dict[str, Any] = wrap_request(method=method, params=params) req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key) req: Request = Request( - method="POST", + method=http_method, url=base_uri, - data=json.dumps(request_body), + data=json.dumps(body), headers=req_headers, auth=ShipEngineAuth(config.api_key), ) prepared_req: PreparedRequest = req.prepare() - request_event_message = generate_event_message( - retry=retry, method=method, base_uri=base_uri - ) - - request_event_data = EventOptions( - message=request_event_message, - id=request_body["id"], - base_uri=base_uri, - request_headers=req_headers, - body=request_body, - retry=retry, - timeout=config.timeout, - ) - request_sent_event = emit_event( - emitted_event_type=RequestSentEvent.REQUEST_SENT, - event_data=request_event_data, - dispatcher=self._DISPATCHER, - ) - try: resp: Response = client.send(request=prepared_req, timeout=config.timeout) except RequestException as err: raise ShipEngineError( - message=f"An unknown error occurred while calling the ShipEngine {method} API:\n {err.response}", + message=f"An unknown error occurred while calling the ShipEngine {http_method} API:\n {err.response}", source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.SYSTEM.value, error_code=ErrorCode.UNSPECIFIED.value, ) resp_body: Dict[str, Any] = resp.json() - status_code: int = resp.status_code - - response_received_message = generate_event_message( - retry=retry, - method=method, - base_uri=base_uri, - status_code=status_code, - message_type="received", - ) - response_event_data = EventOptions( - message=response_received_message, - id=request_body["id"], - base_uri=base_uri, - status_code=status_code, - response_headers=resp.headers, - body=request_body, - retry=retry, - elapsed=(request_sent_event.timestamp - datetime.now()).total_seconds(), - ) - - # Emit `ResponseReceivedEvent` to registered Subscribers. - emit_event( - emitted_event_type=ResponseReceivedEvent.RESPONSE_RECEIVED, - event_data=response_event_data, - dispatcher=self._DISPATCHER, - ) + # status_code: int = resp.status_code - check_response_for_errors(status_code=status_code, response_body=resp_body, config=config) - return handle_response(resp.json()) + # check_response_for_errors(status_code=status_code, response_body=resp_body, config=config) + return resp_body def _request_retry_session( self, retries: int = 1, backoff_factor=1, status_force_list=(429, 500, 502, 503, 504) @@ -195,7 +161,9 @@ def _derive_user_agent() -> str: :rtype: str """ sdk_version: str = f"shipengine-python/{__version__}" + platform_os = platform.system() + os_version = platform.release() python_version: str = platform.python_version() python_implementation: str = platform.python_implementation() - return f"{sdk_version} {python_implementation}-v{python_version}" + return f"shipengine-python/{sdk_version} {platform_os}/{os_version} {python_implementation}/{python_version}" diff --git a/shipengine_sdk/jsonrpc/__init__.py b/shipengine_sdk/jsonrpc/__init__.py deleted file mode 100644 index 7502077..0000000 --- a/shipengine_sdk/jsonrpc/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -A collection of methods that provide `JSON-RPC 2.0` HTTP client -functionality for sending HTTP requests from the ShipEngine SDK. -""" -import time -from typing import Any, Dict, Optional - -from ..errors import RateLimitExceededError -from ..http_client import ShipEngineClient -from ..shipengine_config import ShipEngineConfig -from .process_request import handle_response, wrap_request - - -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.""" - return rpc_request_loop(method, params, config) - - -def rpc_request_loop( - method: str, params: Optional[Dict[str, Any]], config: ShipEngineConfig -) -> Dict[str, Any]: - client: ShipEngineClient = ShipEngineClient(config=config) - retry: int = 0 - while retry <= config.retries: - try: - api_response = client.send_rpc_request( - method=method, params=params, retry=retry, config=config - ) - except Exception as err: - if ( - retry < config.retries - and type(err) is RateLimitExceededError - and err.retry_after < config.timeout - ): - time.sleep(err.retry_after) - retry += 1 - continue - else: - raise err - return api_response diff --git a/shipengine_sdk/jsonrpc/process_request.py b/shipengine_sdk/jsonrpc/process_request.py deleted file mode 100644 index 05fa692..0000000 --- a/shipengine_sdk/jsonrpc/process_request.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Functions that help with process requests and handle responses.""" -from typing import Any, Dict, Optional - -from fuuid import b58_fuuid - -from ..errors import ( - AccountStatusError, - BusinessRuleError, - ClientSecurityError, - ClientSystemError, - ShipEngineError, - ValidationError, -) -from ..models import ErrorType - - -def wrap_request(method: str, params: Optional[Dict[str, Any]]) -> Dict[str, Any]: - """ - Wrap request per `JSON-RPC 2.0` spec. - - :param str method: The RPC Method to be sent to the RPC Server to - 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]] - """ - if params is None: - return dict(id=f"req_{b58_fuuid()}", jsonrpc="2.0", method=method) - else: - return dict(id=f"req_{b58_fuuid()}", jsonrpc="2.0", method=method, params=params) - - -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_type: str = error_data["type"] - if error_type is ErrorType.ACCOUNT_STATUS.value: - raise AccountStatusError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.SECURITY.value: - raise ClientSecurityError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.VALIDATION.value: - raise ValidationError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.BUSINESS_RULES.value: - raise BusinessRuleError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - elif error_type is ErrorType.SYSTEM.value: - raise ClientSystemError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) - else: - raise ShipEngineError( - message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], - ) diff --git a/shipengine_sdk/models/__init__.py b/shipengine_sdk/models/__init__.py deleted file mode 100644 index a1ebfd0..0000000 --- a/shipengine_sdk/models/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""ShipEngine SDK Models & Enumerations""" -from .address import Address, AddressValidateResult -from .carriers import Carrier, CarrierAccount -from .enums import ( - CarrierNames, - Carriers, - Country, - Endpoints, - ErrorCode, - ErrorSource, - ErrorType, - RegexPatterns, - RPCMethods, - does_member_value_exist, - get_carrier_name_value, -) -from .package import ( - Location, - Package, - Shipment, - TrackingEvent, - TrackingQuery, - TrackPackageResult, -) diff --git a/shipengine_sdk/models/address/__init__.py b/shipengine_sdk/models/address/__init__.py deleted file mode 100644 index a47a399..0000000 --- a/shipengine_sdk/models/address/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Models to be used throughout the ShipEngine SDK.""" -import json -from dataclasses import dataclass -from typing import List, Optional - -from dataclasses_json import LetterCase, dataclass_json - -from ...util import ( - is_city_valid, - is_country_code_valid, - is_postal_code_valid, - is_state_valid, - is_street_valid, -) - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass -class Address: - street: List[str] - city_locality: str - state_province: str - postal_code: str - country_code: str - is_residential: Optional[bool] = False - name: Optional[str] = "" - phone: Optional[str] = "" - company: Optional[str] = "" - - def __post_init__(self) -> None: - is_street_valid(self.street) - is_city_valid(self.city_locality) - is_state_valid(self.state_province) - is_postal_code_valid(self.postal_code) - is_country_code_valid(self.country_code) - - -class AddressValidateResult: - is_valid: Optional[bool] - request_id: str - normalized_address: Optional[Address] - info: Optional[List] - warnings: Optional[List] - errors: Optional[List] - - def __init__( - self, - is_valid: Optional[bool], - request_id: str, - normalized_address: Optional[Address], - messages: List, - info: Optional[List] = None, - warnings: Optional[List] = None, - errors: Optional[List] = None, - ) -> None: - self.is_valid = is_valid - self.request_id = request_id - self.normalized_address = normalized_address - self.info = list() if info is None else info - self.warnings = list() if warnings is None else warnings - self.errors = list() if errors is None else errors - self.__extract_messages(messages) - - def __extract_messages(self, messages): - for message in messages: - if message["type"] == "error": - del message["type"] - self.errors.append(message) - elif message["type"] == "info": - del message["type"] - self.info.append(message) - elif message["type"] == "warning": - del message["type"] - self.warnings.append(message) - - 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) - - def __repr__(self): - return f"AddressValidateResult({self.is_valid}, {self.request_id}, {self.normalized_address}, {self.info}, {self.warnings}, {self.errors})" # noqa diff --git a/shipengine_sdk/models/carriers/__init__.py b/shipengine_sdk/models/carriers/__init__.py deleted file mode 100644 index a7c04fd..0000000 --- a/shipengine_sdk/models/carriers/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -"""CarrierAccount class object and immutable carrier object.""" -import json -from typing import Any, Dict - -from ...errors import InvalidFieldValueError, ShipEngineError -from ..enums import Carriers, does_member_value_exist, get_carrier_name_value - - -class Carrier: - def __init__(self, code: str) -> None: - """This class represents a given account with a Carrier provider e.g. `FedEx`, `UPS`, `USPS`.""" - if not does_member_value_exist(code, Carriers): - raise ShipEngineError(f"Carrier [{code}] not currently supported.") - else: - self.name = get_carrier_name_value(code.upper()) - self.code = code - - 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) - - def __repr__(self): - return f"Carrier({self.code}, {self.name})" - - -class CarrierAccount: - carrier: Carrier - - 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"] - self.account_number = account_information["accountNumber"] - - def _set_carrier(self, carrier: str) -> None: - if does_member_value_exist(carrier, Carriers): - self.carrier = Carrier(code=carrier).to_dict() - self.name = self.carrier["name"] - else: - InvalidFieldValueError( - field_name="carrier", - reason=f"Carrier [{carrier}] is currently not supported.", - field_value=carrier, - ) - - 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) - - def __repr__(self): - return ( - f"CarrierAccount({self.account_id}, {self.account_number}, {self.carrier}, {self.name})" - ) diff --git a/shipengine_sdk/models/enums/carriers/__init__.py b/shipengine_sdk/models/enums/carriers/__init__.py deleted file mode 100644 index ec6f94d..0000000 --- a/shipengine_sdk/models/enums/carriers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Carrier enumerations used throughout the ShipEngine SDK.""" -from .carrier_names import CarrierNames -from .carriers import Carriers diff --git a/shipengine_sdk/models/enums/carriers/carrier_names.py b/shipengine_sdk/models/enums/carriers/carrier_names.py deleted file mode 100644 index fd8c8fd..0000000 --- a/shipengine_sdk/models/enums/carriers/carrier_names.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Enumeration of valid carrier names.""" -from enum import Enum - - -class CarrierNames(Enum): - """An enumeration valid carrier names.""" - - FEDEX = "FedEx" - """FedEx - Federal Express""" - - UPS = "United Parcel Service" - """UPS - United Parcel Service""" - - USPS = "U.S. Postal Service" - """USPS - United State Postal Service""" - - STAMPS_COM = "Stamps.com" - """USPS services via Stamps.com""" - - DHL_EXPRESS = "DHL Express" - """DHL Express""" - - DHL_GLOBAL_MAIL = "DHL ECommerce" - """DHL ECommerce""" - - CANADA_POST = "Canada Post" - """Canada Post""" - - AUSTRALIA_POST = "Australia Post" - """Australia Post""" - - FIRSTMILE = "First Mile" - """First Mile""" - - ASENDIA = "Asendia" - """Asendia""" - - ONTRAC = "OnTrac" - """OnTrac""" - - APC = "APC" - """APC""" - - NEWGISTICS = "Newgistics" - """Newgistics""" - - GLOBEGISTICS = "Globegistics" - """Globegistics""" - - RR_DONNELLEY = "RR Donnelley" - """RR Donnell""" - - IMEX = "IMEX" - """IMEX""" - - ACCESS_WORLDWIDE = "Access Worldwide" - """Access Worldwide""" - - PUROLATOR_CA = "Purolator Canada" - """Purolator Canada""" - - SENDLE = "Sendle" - """Sendle""" diff --git a/shipengine_sdk/models/enums/carriers/carriers.py b/shipengine_sdk/models/enums/carriers/carriers.py deleted file mode 100644 index a79342c..0000000 --- a/shipengine_sdk/models/enums/carriers/carriers.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Enumeration of valid carrier providers.""" -from enum import Enum - - -class Carriers(Enum): - """An enumeration of valid carrier providers.""" - - FEDEX = "fedex" - """FedEx - Federal Express""" - - UPS = "ups" - """UPS - United Parcel Service""" - - USPS = "usps" - """USPS - United State Postal Service""" - - STAMPS_COM = "stamps_com" - """USPS services via Stamps.com""" - - DHL_EXPRESS = "dhl_express" - """DHL Express""" - - DHL_GLOBAL_MAIL = "dhl_global_mail" - """DHL ECommerce""" - - CANADA_POST = "canada_post" - """Canada Post""" - - AUSTRALIA_POST = "australia_post" - """Australia Post""" - - FIRSTMILE = "firstmile" - """First Mile""" - - ASENDIA = "asendia" - """Asendia""" - - ONTRAC = "ontrac" - """OnTrac""" - - APC = "apc" - """APC""" - - NEWGISTICS = "newgistics" - """Newgistics""" - - GLOBEGISTICS = "globegistics" - """Globegistics""" - - RR_DONNELLEY = "rr_donnelley" - """RR Donnell""" - - IMEX = "imex" - """IMEX""" - - ACCESS_WORLDWIDE = "access_worldwide" - """Access Worldwide""" - - PUROLATOR_CA = "purolator_ca" - """Purolator Canada""" - - SENDLE = "sendle" - """Sendle""" diff --git a/shipengine_sdk/models/package/__init__.py b/shipengine_sdk/models/package/__init__.py deleted file mode 100644 index daeeff0..0000000 --- a/shipengine_sdk/models/package/__init__.py +++ /dev/null @@ -1,264 +0,0 @@ -"""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, Union - -from dataclasses_json import LetterCase, dataclass_json - -from ...errors import ShipEngineError -from ...services.get_carrier_accounts import GetCarrierAccounts -from ...shipengine_config import ShipEngineConfig -from ...util.iso_string import IsoString -from .. import Carrier, CarrierAccount - - -class Shipment: - config: ShipEngineConfig - shipment_id: Optional[str] = None - account_id: Optional[str] = None - carrier_account: Optional[CarrierAccount] = None - carrier: Optional[Carrier] = None - estimated_delivery_date: Union[IsoString, str] - actual_delivery_date: Union[IsoString, str] - - def __init__( - 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 - - 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 - else: - self.carrier = ( - Carrier(shipment["carrierCode"]) - if "carrierCode" in shipment - else ShipEngineError("The carrierCode field was null from api response.") - ) - - 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: - get_accounts: GetCarrierAccounts = GetCarrierAccounts() - target_carrier: List[CarrierAccount] = list() - carrier_accounts: List[CarrierAccount] = get_accounts.fetch_cached_carrier_accounts( - carrier_code=carrier, config=self.config - ) - - for account in carrier_accounts: - if account_id == account.account_id: - target_carrier.append(account) - return target_carrier[0] - - raise ShipEngineError( - message=f"accountId [{account_id}] doesn't match any of the accounts connected to your ShipEngine Account." # noqa - ) - - 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) -> str: - if hasattr(self, "config"): - del self.config - else: - pass # noqa - return json.dumps(self, default=lambda o: o.__dict__, indent=2) - - def __repr__(self): - return f"Shipment({self.shipment_id}, {self.account_id})" - - -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]] - 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) - - def __repr__(self): - return f"Package({self.package_id}, {self.weight}, {self.dimensions}, {self.tracking_number}, {self.tracking_url})" # noqa - - -@dataclass_json(letter_case=LetterCase.CAMEL) -@dataclass -class TrackingQuery: - """This object is used as an argument in the `track_package` and `track` methods.""" - - carrier_code: str - tracking_number: str - - -class Location: - 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 and location_data is not None - else None - ) - self.state_province = ( - location_data["stateProvince"] - if "stateProvince" in location_data and location_data is not None - else None - ) - self.postal_code = ( - location_data["postalCode"] - if "postalCode" in location_data and location_data is not None - else None - ) - self.country_code = ( - location_data["countryCode"] - if "countryCode" in location_data and location_data is not None - else None - ) - - if ( - "coordinates" in location_data - and location_data is not None - and location_data["coordinates"] is not None - ): - 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) - - def __repr__(self): - return f"Location({self.city_locality}, {self.state_province}, {self.postal_code}, {self.country_code}, {self.latitude}, {self.longitude})" # noqa - - -class TrackingEvent: - date_time: Union[IsoString, str] - carrier_date_time: Union[IsoString, str] - status: str - description: Optional[str] - carrier_status_code: Optional[str] - carrier_detail_code: Optional[str] - signer: Optional[str] - location: Optional[Location] - - def __init__(self, event: Dict[str, Any]) -> None: - """Tracking event object.""" - self.date_time = IsoString(iso_string=event["timestamp"]) - - self.carrier_date_time = IsoString(iso_string=event["carrierTimestamp"]) - - self.status = event["status"] - self.description = event["description"] if "description" in event else None - self.carrier_status_code = ( - event["carrierStatusCode"] if "carrierStatusCode" in event else None - ) - self.carrier_detail_code = ( - event["carrierDetailCode"] if "carrierDetailCode" in event else None - ) - self.signer = event["signer"] if "signer" in event else None - self.location = ( - Location(event["location"]) - if "location" in event and event["location"] is not None - else None - ) - - 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) - - def __repr__(self): - return f"TrackingEvent({self.date_time.to_string()}, {self.date_time.to_string()}, {self.status}, {self.description}, {self.carrier_status_code}, {self.carrier_detail_code}, {self.signer}, {self.location})" # noqa - - -class TrackPackageResult: - shipment: Optional[Shipment] - package: Optional[Package] - 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.""" - self.events = list() - result = api_response["result"] - for event in result["events"]: - self.events.append(TrackingEvent(event=event)) - - self.shipment = ( - Shipment( - shipment=result["shipment"], - actual_delivery_date=self.get_latest_event().date_time, - config=config, - ) - if "shipment" in result - else None - ) - self.package = Package(result["package"]) if "package" in result else None - - 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: - """Returns the latest event to have occurred in the `events` list.""" - return self.events[-1] - - def has_errors(self) -> bool: - """Returns `true` if there are any exception events.""" - for event in self.events: - if event.status == "exception": - return True - return False - - 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) - - def __repr__(self): - return f"TrackPackageResult({self.shipment}, {self.package}, {self.events})" diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index 31b4552..c3ca304 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -1,9 +1,10 @@ """Validate a single address or multiple addresses.""" from typing import Any, Dict +from shipengine_sdk.enums import RPCMethods + from ..jsonrpc import rpc_request from ..models.address import Address, AddressValidateResult -from ..models.enums import RPCMethods from ..shipengine_config import ShipEngineConfig from ..util import does_normalized_address_have_errors diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 0f76639..367f941 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -1,11 +1,14 @@ """The entrypoint to the ShipEngine API SDK.""" -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, Union -from .models import CarrierAccount, TrackingQuery, TrackPackageResult -from .models.address import Address, AddressValidateResult -from .services.address_validation import normalize, validate -from .services.get_carrier_accounts import GetCarrierAccounts -from .services.track_package import track +from shipengine_sdk.enums import Endpoints + +# from .models import CarrierAccount, TrackingQuery, TrackPackageResult +# from .models.address import Address, AddressValidateResult +# from .services.address_validation import normalize, validate +# from .services.get_carrier_accounts import GetCarrierAccounts +# from .services.track_package import track +from .http_client import ShipEngineClient from .shipengine_config import ShipEngineConfig @@ -24,51 +27,68 @@ def __init__(self, config: Union[str, Dict[str, Any], ShipEngineConfig]) -> None The `api_key` you pass in can be either a ShipEngine sandbox or production API Key. (sandbox keys start with "TEST_") """ + self.client = ShipEngineClient() if type(config) is str: self.config = ShipEngineConfig({"api_key": config}) elif type(config) is dict: self.config = ShipEngineConfig(config) - def validate_address( - self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None - ) -> AddressValidateResult: + def validate_addresses( + self, params: Dict[str, Any], config: Union[str, Dict[str, Any], ShipEngineConfig] = None + ) -> Dict[str, Any]: """ Validate an address in nearly any countryCode in the world. - :param Address address: The address to be validate. - :param ShipEngineConfig config: The global ShipEngine configuration object. - :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the + :param Dict[str, Any] params: The address to be validate. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: The global ShipEngine configuration object. + :returns: Dict[str, Any]: The response from ShipEngine API including the validated and normalized address. """ 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 - ) -> Address: - """Normalize a given address into a standardized format used by carriers.""" - 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, - ) -> List[CarrierAccount]: - """Fetch a list of the carrier accounts connected to your ShipEngine Account.""" - config = self.config.merge(new_config=config) - get_accounts = GetCarrierAccounts() - return get_accounts.fetch_carrier_accounts(config=config, carrier_code=carrier_code) + return self.client.post( + endpoint=Endpoints.ADDRESSES_VALIDATE.value, params=params, config=config + ) - def track_package( - self, - tracking_data: Union[str, 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 = self.config.merge(new_config=config) - return track(tracking_data=tracking_data, config=config) + # def validate_address( + # self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None + # ) -> AddressValidateResult: + # """ + # Validate an address in nearly any countryCode in the world. + # + # :param Address address: The address to be validate. + # :param ShipEngineConfig config: The global ShipEngine configuration object. + # :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the + # validated and normalized address. + # """ + # 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 + # ) -> Address: + # """Normalize a given address into a standardized format used by carriers.""" + # 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, + # ) -> List[CarrierAccount]: + # """Fetch a list of the carrier accounts connected to your ShipEngine Account.""" + # 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, 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 = self.config.merge(new_config=config) + # return track(tracking_data=tracking_data, config=config) diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py index cdd02e3..657fb4b 100644 --- a/shipengine_sdk/util/iso_string.py +++ b/shipengine_sdk/util/iso_string.py @@ -2,7 +2,7 @@ import re from datetime import datetime -from ..models.enums import RegexPatterns +from shipengine_sdk.enums import RegexPatterns class IsoString: diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index b45c2a0..0dd39e5 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -2,6 +2,8 @@ import re from typing import Any, Dict, List +from shipengine_sdk.enums import Country, ErrorCode, ErrorSource, ErrorType + from ..errors import ( ClientSystemError, ClientTimeoutError, @@ -10,7 +12,6 @@ ShipEngineError, ValidationError, ) -from ..models.enums import Country, ErrorCode, ErrorSource, ErrorType validation_message = "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py deleted file mode 100644 index e748dec..0000000 --- a/tests/errors/test_errors.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Tests for the ShipEngine SDK Errors""" -import pytest - -from shipengine_sdk.errors import ( - AccountStatusError, - BusinessRuleError, - ClientSecurityError, - ClientTimeoutError, - InvalidFieldValueError, - RateLimitExceededError, - ShipEngineError, - ValidationError, -) - - -def shipengine_error_defaults() -> ShipEngineError: - """Return a ShipEngineError that only has a message string passed in.""" - raise ShipEngineError(message="Testing the defaults for this error.") - - -def shipengine_error(): - """Return a ShipEngineError that have all fields populated by valid values.""" - raise ShipEngineError( - request_id="req_a523b1b19bd54054b7eb953f000e7f15", - message="The is a test exception", - source="shipengine", - error_type="validation", - error_code="invalid_address", - url="https://google.com", - ) - - -def shipengine_error_with_no_error_type() -> ShipEngineError: - """Return a ShipEngineError that only has the error_type set to None.""" - raise ShipEngineError( - request_id="req_a523b1b19bd54054b7eb953f000e7f15", - message="The is a test exception", - source="shipengine", - error_type=None, - error_code="invalid_address", - ) - - -def shipengine_error_with_bad_error_type() -> ShipEngineError: - """Return a ShipEngineError that has an invalid error_type.""" - raise ShipEngineError( - request_id="req_a523b1b19bd54054b7eb953f000e7f15", - message="The is a test exception", - source="shipengine", - error_type="tracking", - error_code="invalid_address", - ) - - -def shipengine_error_with_bad_error_source() -> ShipEngineError: - """Return a ShipEngineError that has an invalid error_source.""" - raise ShipEngineError( - request_id="req_a523b1b19bd54054b7eb953f000e7f15", - message="The is a test exception", - source="wayne_enterprises", - error_type="validation", - error_code="invalid_address", - ) - - -def shipengine_error_with_bad_error_code() -> ShipEngineError: - """Return a ShipEngineError that has an invalid error_code.""" - raise ShipEngineError( - request_id="req_a523b1b19bd54054b7eb953f000e7f15", - message="The is a test exception", - source="shipengine", - error_type="validation", - error_code="failure", - ) - - -def account_status() -> AccountStatusError: - raise AccountStatusError("There was an issue with your ShipEngine account.") - - -def business_rule_error() -> BusinessRuleError: - raise BusinessRuleError("Invalid postal code.") - - -def security_error() -> ClientSecurityError: - raise ClientSecurityError("Unauthorized - you API key is invalid.") - - -def validation_error() -> ValidationError: - raise ValidationError("The value provided must be an integer - object provided.") - - -def client_timeout_error() -> ClientTimeoutError: - raise ClientTimeoutError(300, "shipengine", "req_a523b1b19bd54054b7eb953f000e7f15") - - -def invalid_filed_value_error() -> InvalidFieldValueError: - raise InvalidFieldValueError("is_residential", "Value should be int but got str.", 1) - - -def rate_limit_exceeded_error() -> RateLimitExceededError: - raise RateLimitExceededError(300, "shipengine", "req_a523b1b19bd54054b7eb953f000e7f15") - - -class TestShipEngineErrors: - def test_shipengine_error(self) -> None: - with pytest.raises(ShipEngineError): - shipengine_error() - - def test_shipengine_error_with_bad_error_type(self) -> None: - with pytest.raises(ValueError): - shipengine_error_with_bad_error_type() - - def test_shipengine_error_with_bad_error_source(self) -> None: - with pytest.raises(ValueError): - shipengine_error_with_bad_error_source() - - def test_shipengine_error_with_bad_error_code(self) -> None: - with pytest.raises(ValueError): - shipengine_error_with_bad_error_code() - - def test_account_status(self) -> None: - with pytest.raises(AccountStatusError): - account_status() - - def test_business_rule_error(self) -> None: - with pytest.raises(BusinessRuleError): - business_rule_error() - - def test_security_error(self) -> None: - with pytest.raises(ClientSecurityError): - security_error() - - def test_validation_error(self) -> None: - with pytest.raises(ValidationError): - validation_error() - - def test_timeout_error(self) -> None: - with pytest.raises(ClientTimeoutError): - client_timeout_error() - - def test_invalid_filed_value_error(self) -> None: - with pytest.raises(InvalidFieldValueError): - invalid_filed_value_error() - - def test_rate_limit_exceeded_error(self) -> None: - with pytest.raises(RateLimitExceededError): - rate_limit_exceeded_error() - - def test_error_defaults(self) -> None: - """Test the error class default values.""" - with pytest.raises(ShipEngineError): - shipengine_error_defaults() - - def test_to_dict_method(self) -> None: - """Test the to_dict convenience method.""" - try: - shipengine_error() - except ShipEngineError as err: - d = err.to_dict() - assert type(d) is dict - - def test_to_json_method(self) -> None: - """Test the to_json convenience method.""" - try: - shipengine_error() - except ShipEngineError as err: - j = err.to_json() - assert type(j) is str diff --git a/tests/events/__init__.py b/tests/events/__init__.py deleted file mode 100644 index 39fe0c3..0000000 --- a/tests/events/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests around events emitted from ShipEngine SDK.""" diff --git a/tests/events/test_emitted_events.py b/tests/events/test_emitted_events.py deleted file mode 100644 index 790ea0c..0000000 --- a/tests/events/test_emitted_events.py +++ /dev/null @@ -1,238 +0,0 @@ -"""Test that `RequestSentEvents` are emitted from the SDK properly.""" -from datetime import datetime - -from pytest_mock import MockerFixture - -from shipengine_sdk import __version__ -from shipengine_sdk.errors import ( - ClientTimeoutError, - RateLimitExceededError, - ShipEngineError, -) -from shipengine_sdk.events import ( - RequestSentEvent, - ResponseReceivedEvent, - ShipEngineEventListener, -) -from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType -from shipengine_sdk.models.enums import Constants - -from ..util import ( - assert_on_429_exception, - configurable_stub_shipengine_instance, - valid_residential_address, -) - - -class TestEmittedEvents: - def test_user_agent_includes_correct_sdk_version(self, mocker: MockerFixture) -> None: - """DX-1517 - Test user agent includes correct SDK version.""" - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - config = { - "api_key": Constants.STUB_API_KEY.value, - "retries": 1, - "timeout": 10, - } - shipengine = configurable_stub_shipengine_instance(config=config) - shipengine.validate_address(address=valid_residential_address()) - request_sent_return = request_sent_spy.spy_return - assert request_sent_return.headers["User-Agent"].split(" ")[0].split("/")[1] == __version__ - - def test_request_sent_event_on_retries(self, mocker: MockerFixture) -> None: - """DX-1521 - Test that a RequestSentEvent is emitted on retries.""" - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - config = { - "api_key": Constants.STUB_API_KEY.value, - "retries": 1, - "timeout": 10, - } - shipengine = configurable_stub_shipengine_instance(config=config) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert_on_429_exception(err=err, error_class=RateLimitExceededError) - - request_sent_return = request_sent_spy.spy_return - assert request_sent_spy.call_count == 2 - assert type(request_sent_return) == RequestSentEvent - assert request_sent_return.retry == 1 - assert type(request_sent_return.timestamp) == datetime - assert request_sent_return.timeout == config["timeout"] - assert request_sent_return.body["method"] == "carrier.listAccounts.v1" - assert request_sent_return.base_uri == "https://api.shipengine.com/jsonrpc" - assert request_sent_return.headers["Api-Key"] == Constants.STUB_API_KEY.value - assert request_sent_return.headers["Content-Type"] == "application/json" - assert ( - request_sent_return.message == "Retrying the ShipEngine carrier.listAccounts.v1 API" - " at https://api.shipengine.com/jsonrpc" - ) - - def test_response_received_event_success(self, mocker: MockerFixture) -> None: - """DX-1522 Test response received event success.""" - test_start_time = datetime.now() - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - config = { - "api_key": Constants.STUB_API_KEY.value, - "retries": 2, - "timeout": 10, - } - shipengine = configurable_stub_shipengine_instance(config=config) - shipengine.validate_address(address=valid_residential_address()) - - response_recd_return = response_received_spy.spy_return - assert response_received_spy.call_count == 1 - assert type(response_recd_return) == ResponseReceivedEvent - assert ( - response_recd_return.message - == "Received an HTTP 200 response from the ShipEngine address.validate.v1 API" - ) - assert response_recd_return.status_code == 200 - assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" - assert response_recd_return.body["method"] == "address.validate.v1" - assert response_recd_return.retry == 0 - assert response_recd_return.elapsed < test_start_time.second - assert response_recd_return.headers["Content-Type"].split(";")[0] == "application/json" - - def test_response_received_on_error(self, mocker: MockerFixture) -> None: - """DX-1523 - Test response received event on error.""" - test_start_time = datetime.now() - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - config = { - "api_key": Constants.STUB_API_KEY.value, - "retries": 1, - "timeout": 10, - } - shipengine = configurable_stub_shipengine_instance(config=config) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert_on_429_exception(err=err, error_class=RateLimitExceededError) - response_recd_return = response_received_spy.spy_return - assert type(response_recd_return) == ResponseReceivedEvent - assert ( - response_recd_return.message - == "Retrying the ShipEngine carrier.listAccounts.v1 API at https://api.shipengine.com/jsonrpc" - ) - assert response_recd_return.status_code == 429 - assert response_recd_return.base_uri == "https://api.shipengine.com/jsonrpc" - assert response_recd_return.body["method"] == "carrier.listAccounts.v1" - assert response_recd_return.retry == 1 - assert response_recd_return.elapsed < test_start_time.second - assert (response_recd_return.timestamp - test_start_time).total_seconds() > 1 - - def test_config_with_retries_disabled(self, mocker: MockerFixture) -> None: - """DX-1527 - Tests that the SDK does not automatically retry if retries in config is set to 0.""" - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - shipengine = configurable_stub_shipengine_instance( - { - "api_key": Constants.STUB_API_KEY.value, - "retries": 0, - "timeout": 10, - } - ) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert_on_429_exception(err=err, error_class=RateLimitExceededError) - request_sent_return = request_sent_spy.spy_return - assert request_sent_spy.call_count == 1 - assert type(request_sent_return) == RequestSentEvent - assert request_sent_return.retry == 0 - - response_recd_return = response_received_spy.spy_return - assert request_sent_spy.call_count == 1 - assert type(response_recd_return) == ResponseReceivedEvent - assert response_recd_return.retry == 0 - - def test_config_with_custom_retries(self, mocker: MockerFixture) -> None: - """DX-1528 - Test config with custom retries.""" - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - shipengine = configurable_stub_shipengine_instance( - { - "api_key": Constants.STUB_API_KEY.value, - "retries": 3, - "timeout": 21, - } - ) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert_on_429_exception(err=err, error_class=RateLimitExceededError) - request_sent_return = request_sent_spy.spy_return - assert request_sent_spy.call_count == 4 - assert type(request_sent_return) == RequestSentEvent - assert request_sent_return.retry == 3 - - response_recd_return = response_received_spy.spy_return - assert request_sent_spy.call_count == 4 - assert type(response_recd_return) == ResponseReceivedEvent - assert response_recd_return.retry == 3 - - def test_timeout_err_when_retry_greater_than_timeout(self, mocker: MockerFixture) -> None: - """DX-1529 - Test timeout error when retry_after is greater than timeout.""" - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - config = { - "api_key": Constants.STUB_API_KEY.value, - "retries": 3, - "timeout": 1, - } - shipengine = configurable_stub_shipengine_instance(config=config) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert type(err) == ClientTimeoutError - assert err.request_id is not None - assert err.request_id.startswith("req_") - assert ( - err.message - == f"The request took longer than the {config['timeout']} seconds allowed." - ) - assert err.source is ErrorSource.SHIPENGINE.value - assert err.error_type is ErrorType.SYSTEM.value - assert err.error_code is ErrorCode.TIMEOUT.value - assert err.url == "https://www.shipengine.com/docs/rate-limits" - - request_sent_return = request_sent_spy.spy_return - assert request_sent_spy.call_count == 1 - assert type(request_sent_return) == RequestSentEvent - assert request_sent_return.retry == 0 - assert request_sent_return.timeout == 1 - - response_recd_return = response_received_spy.spy_return - assert request_sent_spy.call_count == 1 - assert type(response_recd_return) == ResponseReceivedEvent - assert response_recd_return.retry == 0 - - def test_retry_waits_correct_amount_of_time(self, mocker: MockerFixture) -> None: - """DX-1530 - retry waits the correct amount of time.""" - test_start_time = datetime.now() - request_sent_spy = mocker.spy(ShipEngineEventListener, "catch_request_sent_event") - response_received_spy = mocker.spy(ShipEngineEventListener, "catch_response_received_event") - shipengine = configurable_stub_shipengine_instance( - { - "api_key": Constants.STUB_API_KEY.value, - "retries": 2, - "timeout": 10, - } - ) - try: - shipengine.get_carrier_accounts(carrier_code="amazon_buy_shipping") - except ShipEngineError as err: - assert_on_429_exception(err=err, error_class=RateLimitExceededError) - - request_sent_return = request_sent_spy.spy_return - assert request_sent_spy.call_count == 3 - assert type(request_sent_return) == RequestSentEvent - assert request_sent_return.retry == 2 - assert request_sent_return.timeout == 10 - - response_recd_return = response_received_spy.spy_return - assert request_sent_spy.call_count == 3 - assert type(response_recd_return) == ResponseReceivedEvent - assert response_recd_return.retry == 2 - assert ( - int(str(round((test_start_time - datetime.now()).total_seconds())).strip("-")) <= 6 - ) diff --git a/tests/http_client/test_http_client.py b/tests/http_client/test_http_client.py deleted file mode 100644 index b41776e..0000000 --- a/tests/http_client/test_http_client.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Testing basic ShipEngineClient functionality.""" -import pytest -import responses - -from shipengine_sdk import ShipEngine -from shipengine_sdk.errors import ClientSystemError -from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType -from shipengine_sdk.models.address import Address -from shipengine_sdk.models.enums import Endpoints - - -def validate_address(address): - shipengine = ShipEngine( - dict( - api_key="baz", - page_size=50, - retries=2, - timeout=10, - ) - ) - return shipengine.validate_address(address) - - -def valid_residential_address() -> Address: - return Address( - street=["4 Jersey St"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def get_500_server_error() -> Address: - return Address( - street=["500 Server Error"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -class TestShipEngineClient: - @responses.activate - def test_500_server_response(self): - responses.add( - responses.POST, - Endpoints.SHIPENGINE_RPC_URL.value, - json={ - "jsonrpc": "2.0", - "id": "req_DezVNUvRkAP819f3JeqiuS", - "error": { - "code": "-32603", - "message": "Unable to connect to the database", - "data": {"source": "shipengine", "type": "system", "code": "unspecified"}, - }, - }, - status=500, - ) - try: - validate_address(get_500_server_error()) - except ClientSystemError as e: - assert e.message == "Unable to connect to the database" - assert e.request_id is not None - assert e.source == ErrorSource.SHIPENGINE.value - assert e.error_type == ErrorType.SYSTEM.value - assert e.error_code == ErrorCode.UNSPECIFIED.value - with pytest.raises(ClientSystemError): - validate_address(get_500_server_error()) - - @responses.activate - def test_404_server_response(self): - responses.add( - responses.POST, - Endpoints.SHIPENGINE_RPC_URL.value, - json={ - "jsonrpc": "2.0", - "id": "req_DezVNUvRkAP819f3JeqiuS", - "error": { - "code": "-32603", - "message": "Content not found.", - "data": {"source": "shipengine", "type": "system", "code": "not_found"}, - }, - }, - status=404, - ) - try: - validate_address(valid_residential_address()) - except ClientSystemError as e: - assert e.message == "Content not found." - assert e.request_id is not None - assert e.source == ErrorSource.SHIPENGINE.value - assert e.error_type == ErrorType.SYSTEM.value - assert e.error_code == ErrorCode.NOT_FOUND.value - with pytest.raises(ClientSystemError): - validate_address(valid_residential_address()) diff --git a/tests/jsonrpc/test_process_request.py b/tests/jsonrpc/test_process_request.py deleted file mode 100644 index 5d8dce8..0000000 --- a/tests/jsonrpc/test_process_request.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Testing the process request and response functions.""" -import pytest - -from shipengine_sdk.errors import ( - AccountStatusError, - BusinessRuleError, - ClientSecurityError, - ClientSystemError, - ShipEngineError, - ValidationError, -) -from shipengine_sdk.jsonrpc import handle_response, wrap_request -from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType, RPCMethods - - -def handle_response_errors(error_source: str, error_code: str, error_type: str): - return handle_response( - { - "jsonrpc": "2.0", - "id": "req_0938jf40398j4f09s8hfd", - "error": { - "code": 12345, - "message": "Test message from the test suite.", - "data": {"source": error_source, "type": error_type, "code": error_code}, - }, - } - ) - - -class TestProcessRequest: - """Test the handle request and response functionality.""" - - def test_wrap_request_with_no_params(self) -> None: - """Unit test for the `wrap_request` method used by the client.""" - request_body = wrap_request(method=RPCMethods.ADDRESS_VALIDATE.value, params=None) - assert "params" not in request_body - - def test_account_status_handling(self) -> None: - """Unit test for the `handle_response` method account status error case.""" - with pytest.raises(AccountStatusError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.ACCOUNT_STATUS.value, - error_code=ErrorCode.TERMS_NOT_ACCEPTED.value, - ) - - def test_security_error_case(self) -> None: - """Unit test for the `handle_response` method security error case.""" - with pytest.raises(ClientSecurityError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.SECURITY.value, - error_code=ErrorCode.UNAUTHORIZED.value, - ) - - def test_validation_error_case(self) -> None: - """Unit test for the `handle_response` method validation error case.""" - with pytest.raises(ValidationError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.VALIDATION.value, - error_code=ErrorCode.INVALID_FIELD_VALUE.value, - ) - - def test_business_rule_error_case(self) -> None: - """Unit test for the `handle_response` method business rule error case.""" - with pytest.raises(BusinessRuleError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.BUSINESS_RULES.value, - error_code=ErrorCode.TRACKING_NOT_SUPPORTED.value, - ) - - def test_system_error_case(self) -> None: - """Unit test for the `handle_response` method system error case.""" - with pytest.raises(ClientSystemError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.SYSTEM.value, - error_code=ErrorCode.UNSPECIFIED.value, - ) - - def test_shipengine_error_case(self) -> None: - """Unit test for the `handle_response` method shipengine error case.""" - with pytest.raises(ShipEngineError): - handle_response_errors( - error_source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.AUTHORIZATION.value, - error_code=ErrorCode.TERMS_NOT_ACCEPTED.value, - ) diff --git a/tests/models/__init__.py b/tests/models/__init__.py deleted file mode 100644 index 44cbe32..0000000 --- a/tests/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests on the models used throughout the ShipEngine SDK.""" diff --git a/tests/models/address/test_address.py b/tests/models/address/test_address.py deleted file mode 100644 index 150c6f7..0000000 --- a/tests/models/address/test_address.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Test the instantiation of the Address object and it's validations.""" -import pytest - -from shipengine_sdk.errors import ValidationError -from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType -from tests.util import address_with_too_many_lines, empty_address_lines - - -def address_line_assertions(err: ValidationError, variant: str) -> None: - """""" - 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 - - if variant == "empty_address_lines": - assert err.message == "Invalid address. At least one address line is required." - assert err.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value - elif variant == "too_many_address_lines": - assert err.message == "Invalid address. No more than 3 street lines are allowed." - assert err.error_code is ErrorCode.INVALID_FIELD_VALUE.value - - -class TestAddress: - def test_no_address_lines(self): - """DX-1033/DX-1051 - No address lines in the street list.""" - try: - empty_address_lines() - except ValidationError as err: - address_line_assertions(err, "empty_address_lines") - with pytest.raises(ValidationError): - empty_address_lines() - - def test_address_with_too_many_lines(self): - """DX-1034/DX-1052 - Too many address lines.""" - try: - address_with_too_many_lines() - except ValidationError as err: - address_line_assertions(err, "too_many_address_lines") diff --git a/tests/models/carriers/test_carrier.py b/tests/models/carriers/test_carrier.py deleted file mode 100644 index c920047..0000000 --- a/tests/models/carriers/test_carrier.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Test the Carrier class object.""" -from shipengine_sdk.errors import ShipEngineError -from shipengine_sdk.models import Carrier - - -class TestCarrier: - def test_invalid_carrier(self) -> None: - try: - Carrier(code="royal_mail") - except ShipEngineError as err: - assert err.message == "Carrier [royal_mail] not currently supported." - - def test_to_json(self) -> None: - carrier = Carrier(code="fedex") - assert type(carrier.to_json()) is str diff --git a/tests/models/carriers/test_carrier_account.py b/tests/models/carriers/test_carrier_account.py deleted file mode 100644 index 4fd8ab0..0000000 --- a/tests/models/carriers/test_carrier_account.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Test the CarrierAccount class object.""" -from typing import Any, Dict - -from shipengine_sdk.errors import InvalidFieldValueError -from shipengine_sdk.models import CarrierAccount - - -def stub_carrier_account_object() -> Dict[str, Any]: - """ - Return a dictionary that mimics the data this object would be passed - from the returned ShipEngine API response. - """ - return { - "accountId": "car_1knseddGBrseWTiw", - "accountNumber": "1169350", - "carrierCode": "royal_mail", - "name": "United Parcel Service", - } - - -class TestCarrierAccount: - def test_carrier_account_with_invalid_carrier(self) -> None: - k = stub_carrier_account_object() - try: - CarrierAccount(account_information=k) - except InvalidFieldValueError as err: - assert err.message == f"Carrier [{k['carrierCode']}] is currently not supported." - - def test_carrier_account_to_dict(self) -> None: - k = stub_carrier_account_object() - carrier_account = CarrierAccount(account_information=k) - - assert type(carrier_account.to_dict()) is dict - - def test_carrier_account_to_json(self) -> None: - k = stub_carrier_account_object() - carrier_account = CarrierAccount(account_information=k) - - assert type(carrier_account.to_json()) is str diff --git a/tests/models/track_package/__init__.py b/tests/models/track_package/__init__.py deleted file mode 100644 index 6491bdf..0000000 --- a/tests/models/track_package/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Test for the models used in the `track_package` method of the ShipEngine SDK.""" diff --git a/tests/models/track_package/test_package.py b/tests/models/track_package/test_package.py deleted file mode 100644 index b21d5db..0000000 --- a/tests/models/track_package/test_package.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Testing the Package class object.""" -from typing import Any, Dict - -from shipengine_sdk.models import Package - - -def stub_package_data() -> Dict[str, Any]: - return { - "packageId": "pkg_1FedExAccepted", - "trackingNumber": "5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ", - "trackingUrl": "https://www.fedex.com/track/5fSkgyuh3GkfUjTZSEAQ8gHeTU29tZ", - "weight": {"value": 76, "unit": "kilogram"}, - "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"}, - } - - -def stub_package() -> Package: - """Return a valid stub Package object.""" - return Package(stub_package_data()) - - -class TestPackage: - def test_package_to_dict(self) -> None: - package = stub_package() - assert type(package.to_dict()) is dict - - def test_package_to_json(self) -> None: - package = stub_package() - assert type(package.to_json()) is str diff --git a/tests/models/track_package/test_shipment.py b/tests/models/track_package/test_shipment.py deleted file mode 100644 index ba30ac7..0000000 --- a/tests/models/track_package/test_shipment.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Testing the Shipment class object.""" -from typing import Any, Dict - -import pytest - -from shipengine_sdk.errors import ShipEngineError -from shipengine_sdk.models import Shipment -from shipengine_sdk.models.enums import Constants -from shipengine_sdk.util.iso_string import IsoString - -from ...util import stub_shipengine_config - - -def stub_valid_shipment_data() -> Dict[str, Any]: - """ - Return a dictionary that mimics the Shipment data that would - be returned by ShipEngine API. - """ - return { - "carrierCode": "fedex", - "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, - "shipmentId": "shp_yuh3GkfUjTZSEAQ", - "estimatedDelivery": "2021-06-15T21:00:00.000Z", - } - - -def stub_invalid_shipment_data() -> Dict[str, Any]: - """ - Return a dictionary that mimics the Shipment data that would - be returned by ShipEngine API, where the `carrierAccountId` is invalid. - """ - return { - "carrierCode": "fedex", - "carrierAccountId": "car_kfUoSHIPENGINEQ8gHeT", - "shipmentId": "shp_yuh3GkfUjTZSEAQ", - "estimatedDelivery": "2021-06-15T21:00:00.000Z", - } - - -def stub_invalid_account_id_shipment_instantiation() -> Shipment: - """Return a test Shipment object that has an invalid `carrierAccountId`..""" - return Shipment( - shipment=stub_invalid_shipment_data(), - actual_delivery_date=IsoString("2021-06-10T21:00:00.000"), - config=stub_shipengine_config(), - ) - - -def stub_valid_shipment_instantiation() -> Shipment: - """Return a valid test Shipment object.""" - return Shipment( - shipment=stub_valid_shipment_data(), - actual_delivery_date=IsoString("2021-06-10T21:00:00.000"), - config=stub_shipengine_config(), - ) - - -class TestShipment: - def test_get_carrier_account_failure_via_invalid_account_id(self) -> None: - with pytest.raises(ShipEngineError): - stub_invalid_account_id_shipment_instantiation() - - def test_shipment_to_dict(self) -> None: - shipment = stub_valid_shipment_instantiation() - assert type(shipment.to_dict()) is dict - - def test_shipment_to_json(self) -> None: - shipment = stub_valid_shipment_instantiation() - assert type(shipment.to_json()) is str diff --git a/tests/models/track_package/test_track_package_result.py b/tests/models/track_package/test_track_package_result.py deleted file mode 100644 index f7541a0..0000000 --- a/tests/models/track_package/test_track_package_result.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Testing the TrackPackageResult class object.""" -from typing import Any, Dict - -from shipengine_sdk.models import TrackingEvent, TrackPackageResult -from shipengine_sdk.models.enums import Constants - -from ...util import stub_shipengine_config - - -def stub_track_package_data() -> Dict[str, Any]: - """ - Return a dictionary that mimics the track_package response - from ShipEngine API. - """ - return { - "jsonrpc": "2.0", - "id": "req_1de9ca85b8544c1c91cd17abc43cbb5e", - "result": { - "shipment": { - "carrierCode": "fedex", - "carrierAccountId": Constants.CARRIER_ACCOUNT_ID_STUB.value, - "shipmentId": "shp_tJUaQJz3Twz57iL", - "estimatedDelivery": "2021-06-15T21:00:00.000Z", - }, - "package": { - "packageId": "pkg_1FedexDeLiveredException", - "trackingNumber": "2A4g3tJUaQJz3Twz57iLWBciD7wZWH", - "trackingUrl": "https://www.fedex.com/track/2A4g3tJUaQJz3Twz57iLWBciD7wZWH", - "weight": {"value": 76, "unit": "kilogram"}, - "dimensions": {"length": 36, "width": 36, "height": 23, "unit": "inch"}, - }, - "events": [ - { - "timestamp": "2021-06-10T19:00:00.000Z", - "carrierTimestamp": "2021-06-11T01:00:00", - "status": "accepted", - "description": "Dropped-off at shipping center", - "carrierStatusCode": "ACPT-2", - "carrierDetailCode": "PU7W", - }, - { - "timestamp": "2021-06-11T01:00:00.000Z", - "carrierTimestamp": "2021-06-11T07:00:00", - "status": "in_transit", - "description": "En-route to distribution center hub", - "carrierStatusCode": "ER00P", - }, - { - "timestamp": "2021-06-11T20:00:00.000Z", - "carrierTimestamp": "2021-06-12T02:00:00", - "status": "unknown", - "description": "Mechanically sorted", - "carrierStatusCode": "MMSa", - "carrierDetailCode": "00004134918400045", - }, - { - "timestamp": "2021-06-12T10:00:00.000Z", - "carrierTimestamp": "2021-06-12T16:00:00", - "status": "in_transit", - "description": "On vehicle for delivery", - "carrierStatusCode": "OFD-22", - "carrierDetailCode": "91R-159E", - }, - { - "timestamp": "2021-06-12T17:00:00.000Z", - "carrierTimestamp": "2021-06-12T23:00:00", - "status": "exception", - "description": "Weather delay", - "carrierStatusCode": "EX026", - "carrierDetailCode": "XX00016", - "location": { - "cityLocality": "Pittsburgh", - "stateProvince": "PA", - "postalCode": "15218", - "countryCode": "US", - }, - }, - { - "timestamp": "2021-06-13T02:00:00.000Z", - "carrierTimestamp": "2021-06-13T08:00:00", - "status": "exception", - "description": "Equipment failure", - "carrierStatusCode": "EX038", - "carrierDetailCode": "XX00184", - "location": { - "cityLocality": "Pittsburgh", - "stateProvince": "PA", - "postalCode": "15218", - "countryCode": "US", - }, - }, - { - "timestamp": "2021-06-13T10:00:00.000Z", - "carrierTimestamp": "2021-06-13T16:00:00", - "status": "in_transit", - "description": "On vehicle for delivery", - "carrierStatusCode": "OFD-22", - "carrierDetailCode": "91R-159E", - }, - { - "timestamp": "2021-06-13T20:00:00.000Z", - "carrierTimestamp": "2021-06-14T02:00:00", - "status": "delivered", - "description": "Delivered", - "carrierStatusCode": "DV99-0004", - "signer": "John P. Doe", - "location": { - "cityLocality": "Pittsburgh", - "stateProvince": "PA", - "postalCode": "15218", - "countryCode": "US", - "coordinates": {"latitude": 40.4504687, "longitude": -79.9352761}, - }, - }, - ], - }, - } - - -def stub_track_package_result() -> TrackPackageResult: - """Return a valid stub TrackPackageResult object.""" - return TrackPackageResult( - api_response=stub_track_package_data(), config=stub_shipengine_config() - ) - - -class TestTrackPackageResult: - def test_get_errors(self) -> None: - result = stub_track_package_result() - err = result.get_errors() - assert type(err) is list - assert len(err) == 2 - - def test_has_errors(self) -> None: - result = stub_track_package_result() - assert result.has_errors() is True - - def test_get_latest_event(self) -> None: - result = stub_track_package_result() - assert type(result.get_latest_event()) is TrackingEvent - - def test_track_package_result_to_dict(self) -> None: - result = stub_track_package_result() - assert type(result.to_dict()) is dict - - def test_track_package_result_to_json(self) -> None: - result = stub_track_package_result() - assert type(result.to_json()) is str diff --git a/tests/models/track_package/test_tracking_event.py b/tests/models/track_package/test_tracking_event.py deleted file mode 100644 index 4e37682..0000000 --- a/tests/models/track_package/test_tracking_event.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Testing the TrackingEvent class object.""" -from typing import Any, Dict - -from shipengine_sdk.models import TrackingEvent - - -def stub_event_data() -> Dict[str, Any]: - """Return a dictionary that mimics a tracking event object in a response from ShipEngine API.""" - return { - "timestamp": "2021-06-13T13:00:00.000Z", - "carrierTimestamp": "2021-06-13T19:00:00", - "status": "accepted", - "description": "Dropped-off at shipping center", - "carrierStatusCode": "ACPT-2", - } - - -def stub_tracking_event() -> TrackingEvent: - """Return a valid stub TrackingEvent object.""" - return TrackingEvent(stub_event_data()) - - -class TestTrackingEvent: - def test_tracking_event_to_dict(self) -> None: - tracking_event = stub_tracking_event() - assert type(tracking_event.to_dict()) is dict - - def test_tracking_event_to_json(self) -> None: - tracking_event = stub_tracking_event() - assert type(tracking_event.to_json()) is str diff --git a/tests/services/__init__.py b/tests/services/__init__.py deleted file mode 100644 index b5bcf10..0000000 --- a/tests/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Initial Docstring""" diff --git a/tests/services/test_address_validation.py b/tests/services/test_address_validation.py deleted file mode 100644 index bab1cfb..0000000 --- a/tests/services/test_address_validation.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Test the validate address method of the ShipEngine SDK.""" -import re - -from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError -from shipengine_sdk.models import ( - Address, - AddressValidateResult, - ErrorCode, - ErrorSource, - ErrorType, -) - -from ..util.test_helpers import ( - address_missing_required_fields, - address_with_all_fields, - address_with_errors, - address_with_invalid_country, - address_with_invalid_postal_code, - address_with_invalid_state, - address_with_warnings, - canada_valid_avs_assertions, - get_server_side_error, - multi_line_address, - non_latin_address, - unknown_address, - valid_address_assertions, - valid_canadian_address, - valid_commercial_address, - valid_residential_address, - validate_an_address, -) - - -class TestValidateAddress: - TEST_METHOD: str = "validate" - - def test_valid_residential_address(self) -> None: - """DX-1024 - Valid residential address.""" - residential_address = valid_residential_address() - validated_address = validate_an_address(residential_address) - address = validated_address.normalized_address - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=residential_address, - returned_address=validated_address, - expected_residential_indicator=True, - ) - assert ( - address.street[0] - == (residential_address.street[0] + " " + residential_address.street[1]) - .replace(".", "") - .upper() - ) - - def test_valid_commercial_address(self) -> None: - """DX-1025 - Valid commercial address.""" - commercial_address = valid_commercial_address() - validated_address = validate_an_address(commercial_address) - address = validated_address.normalized_address - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=commercial_address, - returned_address=validated_address, - expected_residential_indicator=False, - ) - assert ( - address.street[0] - == (commercial_address.street[0] + " " + commercial_address.street[1]) - .replace(".", "") - .upper() - ) - - def test_multi_line_address(self) -> None: - """DX-1027 - Validate multiline address.""" - valid_multi_line_address = multi_line_address() - validated_address = validate_an_address(valid_multi_line_address) - address = validated_address.normalized_address - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=valid_multi_line_address, - returned_address=validated_address, - expected_residential_indicator=False, - ) - assert ( - address.street[0] - == (valid_multi_line_address.street[0] + " " + valid_multi_line_address.street[1]) - .replace(".", "") - .upper() - ) - assert address.street[1] == valid_multi_line_address.street[2].upper() - - def test_numeric_postal_code(self) -> None: - """DX-1028 - Validate numeric postal code.""" - residential_address = valid_residential_address() - validated_address = validate_an_address(residential_address) - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=residential_address, - returned_address=validated_address, - expected_residential_indicator=True, - ) - assert re.match(r"\d", validated_address.normalized_address.postal_code) - - def test_alpha_postal_code(self) -> None: - """DX-1029 - Alpha postal code.""" - canadian_address = valid_canadian_address() - validated_address = validate_an_address(canadian_address) - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="international", - original_address=canadian_address, - returned_address=validated_address, - expected_residential_indicator=False, - ) - - def test_unknown_address(self) -> None: - """DX-1026 - Validate address of unknown address.""" - address = unknown_address() - validated_address = validate_an_address(address) - canada_valid_avs_assertions( - original_address=address, - validated_address=validated_address, - expected_residential_indicator=None, - ) - - def test_address_with_non_latin_chars(self) -> None: - """DX-1030 - non-latin characters.""" - non_latin = non_latin_address() - validated_address = validate_an_address(non_latin) - address = validated_address.normalized_address - - assert validated_address.is_valid is True - assert address is not None - assert type(address) is Address - assert address.street[0] == "68 Kamitobatsunodacho" - assert address.city_locality == "Kyoto-Shi Minami-Ku" - assert address.state_province == "Kyoto" - assert address.postal_code == non_latin.postal_code - assert address.country_code == non_latin.country_code - assert address.is_residential is False - assert len(address.street) == 1 - - def test_address_with_warnings(self) -> None: - """DX-1031 - validate with warnings.""" - warnings_address = address_with_warnings() - validated_address = validate_an_address(warnings_address) - address = validated_address.normalized_address - - assert type(validated_address) is AddressValidateResult - assert validated_address.is_valid is True - assert type(address) is Address - assert len(validated_address.info) == 0 - assert len(validated_address.warnings) != 0 - assert ( - validated_address.warnings[0]["code"] - == ErrorCode.PARTIALLY_VERIFIED_TO_PREMISE_LEVEL.value - ) - assert ( - validated_address.warnings[0]["message"] - == "This address has been verified down to the house/building level (highest possible accuracy with the provided data)" # noqa - ) - assert len(validated_address.errors) == 0 - assert address.city_locality == warnings_address.city_locality - assert address.state_province == warnings_address.state_province.title() - assert address.postal_code == "M6K 3C3" - assert address.country_code == warnings_address.country_code.upper() - assert address.is_residential is True - - def test_address_with_errors(self) -> None: - """DX-1032 - Validate with error messages.""" - error_address = address_with_errors() - validated_address = validate_an_address(error_address) - address = validated_address.normalized_address - - assert type(validated_address) is AddressValidateResult - assert validated_address.is_valid is False - assert address is None - assert len(validated_address.info) == 0 - assert len(validated_address.warnings) != 0 - assert validated_address.warnings[0]["message"] == "Address not found" - assert len(validated_address.errors) != 0 - assert validated_address.errors[0]["code"] == ErrorCode.ADDRESS_NOT_FOUND.value - assert validated_address.errors[0]["message"] == "Invalid City, State, or Zip" - assert validated_address.errors[1]["code"] == ErrorCode.ADDRESS_NOT_FOUND.value - assert validated_address.errors[1]["message"] == "Insufficient or Incorrect Address Data" - - def test_missing_city_state_and_postal_code(self) -> None: - """DX-1035 & DX-1036 - Missing city, state, and postal code.""" - try: - address_missing_required_fields() - except ValidationError as err: - 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.FIELD_VALUE_REQUIRED.value - assert ( - err.message - == "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa - ) - - def test_invalid_country_code(self) -> None: - """DX-1037 - Invalid country code.""" - try: - address_with_invalid_country() - except ValidationError as err: - 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.FIELD_VALUE_REQUIRED.value - assert err.message == "Invalid address: [RZ] is not a valid country code." - - def test_server_side_error(self) -> None: - """DX-1038 - Server-side error.""" - try: - get_server_side_error() - except ClientSystemError as err: - assert err.request_id is not None - assert err.request_id.startswith("req_") is True - assert err.source is ErrorSource.SHIPENGINE.value - assert err.error_type is ErrorType.SYSTEM.value - assert err.error_code is ErrorCode.UNSPECIFIED.value - - def test_address_with_name_company_phone(self) -> None: - """DX-1393 - Validate address with name, company, and phone.""" - address = address_with_all_fields() - validated_address = validate_an_address(address=address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=address, - returned_address=validated_address, - expected_residential_indicator=True, - ) - - def test_address_with_invalid_state(self) -> None: - """Test validate_address when an invalid state is passed in.""" - try: - address_with_invalid_state() - except ShipEngineError as err: - assert type(err) is ValidationError - assert ( - err.message - == "Invalid address. Either the postal code or the city/locality and state/province must be specified." - ) # noqa - - def test_address_with_invalid_postal_code(self) -> None: - """Test validate_address when an invalid postal code is passed in.""" - try: - address_with_invalid_postal_code() - except ShipEngineError as err: - assert type(err) is ValidationError - assert ( - err.message - == "Invalid address. Either the postal code or the city/locality and state/province must be specified." - ) # noqa diff --git a/tests/services/test_get_carrier_accounts.py b/tests/services/test_get_carrier_accounts.py deleted file mode 100644 index 1a1eabe..0000000 --- a/tests/services/test_get_carrier_accounts.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Tests for the GetCarrierAccounts service in the ShipEngine SDK.""" -from shipengine_sdk.errors import ClientSystemError -from shipengine_sdk.models import Carriers, ErrorCode, ErrorSource, ErrorType -from shipengine_sdk.services.get_carrier_accounts import GetCarrierAccounts - -from ..util.test_helpers import stub_get_carrier_accounts - - -class TestGetCarrierAccounts: - def test_no_accounts_setup(self) -> None: - """DX-1075 - No accounts setup yet.""" - accounts = stub_get_carrier_accounts(carrier_code="sendle") - - assert type(accounts) is list - assert len(accounts) == 0 - - def test_multiple_accounts(self) -> None: - """DX-1076 - Multiple carrier accounts.""" - accounts = stub_get_carrier_accounts() - - assert len(accounts) == 5 - assert accounts[0].carrier["code"] == Carriers.UPS.value - assert accounts[1].carrier["code"] == Carriers.FEDEX.value - assert accounts[2].carrier["code"] == Carriers.FEDEX.value - assert accounts[3].carrier["code"] == Carriers.USPS.value - assert accounts[4].carrier["code"] == Carriers.STAMPS_COM.value - - for account in accounts: - assert account.account_id.startswith("car_") - assert account.name is not None - assert account.account_number is not None - assert account.carrier is not None - assert account.carrier["code"] is not None - assert type(account.carrier["code"]) == str - assert account.carrier["name"] is not None - assert type(account.carrier["name"]) == str - - def test_multiple_accounts_of_same_carrier(self): - """DX-1077 - Multiple accounts of the same carrier.""" - accounts = stub_get_carrier_accounts() - - assert len(accounts) == 5 - assert accounts[0].carrier["code"] == Carriers.UPS.value - assert accounts[0].account_id != accounts[1].account_id - assert accounts[1].carrier["code"] == Carriers.FEDEX.value - assert accounts[2].carrier["code"] == Carriers.FEDEX.value - assert accounts[3].carrier["code"] == Carriers.USPS.value - assert accounts[4].carrier["code"] == Carriers.STAMPS_COM.value - - for account in accounts: - assert account.account_id.startswith("car_") - assert account.name is not None - - def test_server_side_error(self) -> None: - """DX-1078 - Get carrier accounts server-side error.""" - try: - stub_get_carrier_accounts("access_worldwide") - except ClientSystemError as err: - assert err.request_id is not None - assert err.request_id.startswith("req_") is True - assert err.source == ErrorSource.SHIPENGINE.value - assert err.error_type == ErrorType.SYSTEM.value - assert err.error_code == ErrorCode.UNSPECIFIED.value - - def test_get_cached_accounts_by_carrier(self) -> None: - get_accounts = GetCarrierAccounts() - stub_get_carrier_accounts() # fill the cache - accounts = get_accounts.get_cached_accounts_by_carrier_code(carrier_code="fedex") - - assert len(accounts) == 2 - assert accounts[0].carrier["code"] == "fedex" - assert accounts[1].carrier["code"] == "fedex" diff --git a/tests/services/test_normalize_address.py b/tests/services/test_normalize_address.py deleted file mode 100644 index 7c2454a..0000000 --- a/tests/services/test_normalize_address.py +++ /dev/null @@ -1,190 +0,0 @@ -"""Test the normalize address method of the ShipEngine SDK.""" -import re - -from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError -from shipengine_sdk.models import Address, ErrorCode, ErrorSource, ErrorType - -from ..util.test_helpers import ( - address_missing_required_fields, - address_with_errors, - address_with_single_error, - address_with_warnings, - get_server_side_error, - multi_line_address, - non_latin_address, - normalize_an_address, - unknown_address, - valid_address_assertions, - valid_canadian_address, - valid_commercial_address, - valid_residential_address, -) - - -class TestNormalizeAddress: - TEST_METHOD: str = "normalize" - - def test_normalize_valid_residential_address(self) -> None: - """DX-1041 - Normalize valid residential address.""" - residential_address = valid_residential_address() - normalized = normalize_an_address(residential_address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=residential_address, - returned_address=normalized, - expected_residential_indicator=True, - ) - - def test_normalize_valid_commercial_address(self) -> None: - """DX-1042 - Normalize valid commercial address.""" - commercial_address = valid_commercial_address() - normalized = normalize_an_address(commercial_address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=commercial_address, - returned_address=normalized, - expected_residential_indicator=False, - ) - - def test_normalize_unknown_address(self) -> None: - """DX-1043 - Normalize unknown address.""" - address = unknown_address() - normalized = normalize_an_address(address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="international", - original_address=address, - returned_address=normalized, - expected_residential_indicator=None, - ) - - def test_normalize_multi_line_address(self) -> None: - """DX-1044 - Normalize multi-line address.""" - multi_line = multi_line_address() - normalized = normalize_an_address(multi_line) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=multi_line, - returned_address=normalized, - expected_residential_indicator=False, - ) - assert ( - normalized.street[0] - == (multi_line.street[0] + " " + multi_line.street[1]).replace(".", "").upper() - ) - assert normalized.street[1] == multi_line.street[2].upper() - - def test_normalize_numeric_postal_code(self) -> None: - """DX-1045 - Normalize address with numeric postal code.""" - address = valid_residential_address() - normalized = normalize_an_address(address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="domestic", - original_address=address, - returned_address=normalized, - expected_residential_indicator=True, - ) - assert re.match(r"\d", normalized.postal_code) - - def test_normalize_alpha_postal_code(self) -> None: - """DX-1046 - Normalize address with alpha-numeric postal code.""" - address = valid_canadian_address() - normalized = normalize_an_address(address) - - valid_address_assertions( - test_method=self.TEST_METHOD, - locale="international", - original_address=address, - returned_address=normalized, - expected_residential_indicator=False, - ) - - def test_normalize_non_latin_chars(self) -> None: - """DX-1047 - Normalize address with non-latin characters.""" - non_latin = non_latin_address() - normalized = normalize_an_address(non_latin) - - assert type(normalized) is Address - assert normalized.street[0] == "68 Kamitobatsunodacho" - assert normalized.city_locality == "Kyoto-Shi Minami-Ku" - assert normalized.state_province == "Kyoto" - assert normalized.postal_code == non_latin.postal_code - assert normalized.country_code == non_latin.country_code - assert normalized.is_residential is False - assert len(normalized.street) == 1 - - def test_normalize_with_warnings(self) -> None: - """DX-1048 - Normalize address with warnings.""" - warning_address = address_with_warnings() - normalized = normalize_an_address(warning_address) - - assert type(normalized) is Address - assert normalized is not None - assert normalized.city_locality == warning_address.city_locality - assert normalized.state_province == warning_address.state_province.title() - assert normalized.postal_code == "M6K 3C3" - assert normalized.country_code == warning_address.country_code.upper() - assert normalized.is_residential is True - - def test_normalize_with_single_error_message(self) -> None: - """DX-1049 - Normalize address with single error message.""" - single_error = address_with_single_error() - try: - normalize_an_address(single_error) - except ShipEngineError as err: - assert err.request_id is not None - assert err.request_id.startswith("req_") is True - assert err.source is ErrorSource.SHIPENGINE.value - assert err.error_type is ErrorType.ERROR.value - assert err.error_code == ErrorCode.MINIMUM_POSTAL_CODE_VERIFICATION_FAILED.value - assert err.message == "Invalid address. Insufficient or inaccurate postal code" - - def test_normalize_with_multiple_errors(self) -> None: - """DX-1050 - Normalize address with multiple error messages.""" - errors_address = address_with_errors() - try: - normalize_an_address(errors_address) - except ShipEngineError as err: - assert err.request_id is not None - assert err.request_id.startswith("req_") is True - assert err.source is ErrorSource.SHIPENGINE.value - assert err.error_type is ErrorType.ERROR.value - assert err.error_code is ErrorCode.INVALID_ADDRESS.value - assert ( - err.message - == "Invalid address.\nInvalid City, State, or Zip\nInsufficient or Incorrect Address Data" - ) - - def test_normalize_missing_city_state_and_postal_code(self) -> None: - """DX-1053 & DX-1054 - Missing city, state, and postal code.""" - try: - address_missing_required_fields() - except ValidationError as err: - 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.FIELD_VALUE_REQUIRED.value - assert ( - err.message - == "Invalid address. Either the postal code or the city/locality and state/province must be specified." # noqa - ) - - def test_normalize_server_side_error(self) -> None: - """DX-1055 - Server-side error.""" - try: - get_server_side_error() - except ClientSystemError as err: - assert err.request_id is not None - assert err.request_id.startswith("req_") is True - assert err.source is ErrorSource.SHIPENGINE.value - assert err.error_type is ErrorType.SYSTEM.value - assert err.error_code is ErrorCode.UNSPECIFIED.value diff --git a/tests/services/test_track_package.py b/tests/services/test_track_package.py deleted file mode 100644 index 49fd566..0000000 --- a/tests/services/test_track_package.py +++ /dev/null @@ -1,318 +0,0 @@ -"""Testing the `track_package` method of the ShipEngine SDK.""" -from typing import List - -from shipengine_sdk.errors import ClientSystemError, ShipEngineError, ValidationError -from shipengine_sdk.models import ( - ErrorCode, - ErrorSource, - ErrorType, - TrackingEvent, - TrackingQuery, - TrackPackageResult, -) - -from ..util 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 == "in_transit" - assert tracking_result.events[3].status == "unknown" - assert tracking_result.events[4].status == "exception" - assert tracking_result.events[5].status == "exception" - assert tracking_result.events[6].status == "attempted_delivery" - 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: - """ - Checks that the order of events is correct in that they should be ordered with - the newest event at the bottom of the list. - """ - 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: - """Common `track_package` assertions.""" - 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 - - -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" - _PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION: str = "pkg_1FedexDeLiveredException" - - 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 - - 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[-1].status == "delivered" - - def test_delivered_on_first_try(self) -> None: - """DX-1091 - Test delivered on first try tracking event.""" - package_id = self._PACKAGE_ID_FEDEX_DELIVERED - 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 = self._PACKAGE_ID_FEDEX_DELIVERED - 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[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 = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION - 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 = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION - 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 = self._PACKAGE_ID_FEDEX_DELIVERED_EXCEPTION - 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" - - 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.latitude is None - assert tracking_result.events[1].location.longitude is None - assert type(tracking_result.events[2].location.latitude) is float - assert type(tracking_result.events[2].location.longitude) is float - - 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 process this request. A downstream API error occurred." diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index 67f6b2a..25eb1e5 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -1,24 +1,6 @@ """Testing the ShipEngine object.""" -import pytest -from shipengine_sdk import ShipEngine, __version__ -from shipengine_sdk.errors import ValidationError -from shipengine_sdk.util.sdk_assertions import api_key_validation_error_assertions - - -def shipengine_no_api_key() -> ShipEngine: - """Return an error from no API Key.""" - return ShipEngine(dict(retries=2)) - - -def shipengine_empty_api_key() -> ShipEngine: - """Return an error from empty API Key.""" - return ShipEngine(config="") - - -def shipengine_whitespace_in_api_key() -> ShipEngine: - """Return an error from whitespace in API Key.""" - return ShipEngine(config=" ") +from shipengine_sdk import __version__ class TestShipEngine: @@ -26,20 +8,20 @@ def test_version(self) -> None: """Test the package version of the ShipEngine SDK.""" assert __version__ == "0.0.1" - def test_no_api_key_provided(self) -> None: - """DX-1440 - No API Key at instantiation.""" - try: - shipengine_no_api_key() - except Exception as e: - api_key_validation_error_assertions(e) - with pytest.raises(ValidationError): - shipengine_no_api_key() - - def test_empty_api_key_provided(self) -> None: - """DX-1441 - Empty API Key at instantiation.""" - try: - shipengine_empty_api_key() - except Exception as e: - api_key_validation_error_assertions(e) - with pytest.raises(ValidationError): - shipengine_empty_api_key() + # def test_no_api_key_provided(self) -> None: + # """DX-1440 - No API Key at instantiation.""" + # try: + # shipengine_no_api_key() + # except Exception as e: + # api_key_validation_error_assertions(e) + # with pytest.raises(ValidationError): + # shipengine_no_api_key() + # + # def test_empty_api_key_provided(self) -> None: + # """DX-1441 - Empty API Key at instantiation.""" + # try: + # shipengine_empty_api_key() + # except Exception as e: + # api_key_validation_error_assertions(e) + # with pytest.raises(ValidationError): + # shipengine_empty_api_key() diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 81b392f..fadc1e9 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -2,9 +2,9 @@ import pytest from shipengine_sdk import ShipEngine, ShipEngineConfig +from shipengine_sdk.enums import Endpoints from shipengine_sdk.errors import InvalidFieldValueError, ValidationError from shipengine_sdk.models.address import Address -from shipengine_sdk.models.enums import Endpoints from shipengine_sdk.util import api_key_validation_error_assertions from shipengine_sdk.util.sdk_assertions import timeout_validation_error_assertions diff --git a/tests/test___init__.py b/tests/test_snake_case_converter.py similarity index 100% rename from tests/test___init__.py rename to tests/test_snake_case_converter.py diff --git a/tests/util/__init__.py b/tests/util/__init__.py deleted file mode 100644 index c8fef9e..0000000 --- a/tests/util/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Test helper functions and test data as functions used throughout the test suite.""" -from .test_helpers import * # noqa diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py deleted file mode 100644 index 706155a..0000000 --- a/tests/util/test_helpers.py +++ /dev/null @@ -1,421 +0,0 @@ -"""Test data as functions and common assertion helper functions.""" -from typing import Dict, Optional, Union - -from shipengine_sdk import ShipEngine, ShipEngineConfig -from shipengine_sdk.errors import ShipEngineError -from shipengine_sdk.models import ( - Address, - AddressValidateResult, - ErrorCode, - ErrorSource, - ErrorType, - TrackingQuery, -) -from shipengine_sdk.models.enums import Constants - - -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=Constants.STUB_API_KEY.value, - page_size=50, - retries=retries, - timeout=15, - ) - - -def stub_shipengine_config() -> ShipEngineConfig: - """Return a valid test ShipEngineConfig object.""" - return ShipEngineConfig(config=stub_config()) - - -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(config=stub_config()) - - -def address_with_all_fields() -> Address: - """Return an address with all fields populated.""" - return Address( - name="ShipEngine", - company="Auctane", - phone="123456789", - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def valid_residential_address() -> Address: - """ - Return a test Address object with valid residential - address information. - """ - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def valid_commercial_address() -> Address: - """ - Return a test Address object with valid commercial - address information. - """ - return Address( - street=["4 Jersey St", "ste 200"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def address_with_warnings() -> Address: - """Return a test Address object that will cause the server to return warning messages.""" - return Address( - street=["170 Warning Blvd", "Apartment 32-B"], - city_locality="Toronto", - state_province="On", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def address_with_single_error() -> Address: - """Return a test Address object that will cause the server to return a single error message.""" - return Address( - street=["170 Error Blvd"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def address_with_errors() -> Address: - """Return a test Address object that will cause the server to return an error message.""" - return Address( - street=["4 Invalid St"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def valid_canadian_address() -> Address: - """Return an Address object with a valid canadian address.""" - return Address( - street=["170 Princes Blvd", "Ste 200"], - city_locality="Toronto", - state_province="On", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def empty_address_lines() -> Address: - """Returns an invalid address with empty street list.""" - return Address( - street=list(), - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def address_with_too_many_lines() -> Address: - """Return an address with too many address lines in the street list.""" - return Address( - street=["4 Jersey St", "ste 200", "1st Floor", "Room B"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def multi_line_address() -> Address: - """Returns a valid multiline address.""" - return Address( - street=["4 Jersey St", "ste 200", "1st Floor"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def non_latin_address() -> Address: - """Return an address with non-latin characters.""" - return Address( - street=["上鳥羽角田町68"], - city_locality="南区", - state_province="京都", - postal_code="601-8104", - country_code="JP", - ) - - -def unknown_address() -> Address: - """ - Return an address that will make the server respond with an - address with an unknown residential flag. - """ - return Address( - street=["4 Unknown St"], - city_locality="Toronto", - state_province="On", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def address_missing_required_fields() -> Address: - """Return an address that is missing a state, city, and postal_code to return a ValidationError..""" - return Address( - street=["4 Jersey St"], - city_locality="", - state_province="", - postal_code="", - country_code="US", - ) - - -def address_missing_country() -> Address: - """Return an address that is only missing the country_code.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="", - ) - - -def address_with_invalid_country() -> Address: - """Return an address that has an invalid country_code specified.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="RZ", - ) - - -def address_with_invalid_state() -> Address: - """Return an address with an invalid state value.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="&$", - postal_code="02215", - country_code="US", - ) - - -def address_with_invalid_postal_code() -> Address: - """Return an address with an invalid postal code.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="2$1*5", - country_code="US", - ) - - -def get_429_address() -> Address: - """Return an address that fetches a 429 fixture from the server.""" - return Address( - street=["429 Rate Limit Error"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def get_server_side_error() -> Address: - """Return an address that will cause the server to return a 500 server error.""" - return Address( - street=["500 Server Error"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def validate_an_address(address: Address) -> AddressValidateResult: - """ - Helper function that passes a config dictionary into the ShipEngine object to instantiate - it and calls the `validate_address` method, providing it the `address` that is passed into - this function. - """ - return stub_shipengine_instance().validate_address(address=address) - - -def normalize_an_address(address: Address) -> Address: - """ - Helper function that passes a config dictionary into the ShipEngine object to instantiate - it and calls the `normalize_address` method, providing it the `address` that is passed into - this function. - """ - 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): - """Helper function that passes the `get_carrier_accounts` method a given carrier_code or None.""" - return stub_shipengine_instance().get_carrier_accounts(carrier_code=carrier_code) - - -# Assertion helper functions - - -def valid_address_assertions( - test_method: str, - locale: str, - original_address: Address, - returned_address: Union[Address, AddressValidateResult], - expected_residential_indicator, -) -> None: - """ - A set of common assertions that are regularly made on the commercial US address - used for testing the `validate_address` or `normalize_address` methods, based on - the `test_method` that is passed in. It also makes different sets of assertions - depending on what `locale` is passed in. - """ - address = ( - returned_address.normalized_address - if type(returned_address) is AddressValidateResult - else returned_address - ) - if locale == "domestic": - if test_method == "validate": - assert type(returned_address) is AddressValidateResult - assert returned_address.is_valid is True - assert type(address) is Address - assert len(returned_address.info) == 0 - assert len(returned_address.warnings) == 0 - assert len(returned_address.errors) == 0 - assert address is not None - assert address.city_locality == original_address.city_locality.upper() - assert address.state_province == original_address.state_province.upper() - assert address.postal_code == original_address.postal_code - assert address.country_code == original_address.country_code.upper() - assert address.is_residential is expected_residential_indicator - elif test_method == "normalize": - assert type(returned_address) is Address - assert returned_address.city_locality == original_address.city_locality.upper() - assert returned_address.state_province == original_address.state_province.upper() - assert returned_address.postal_code == original_address.postal_code - assert returned_address.country_code == original_address.country_code.upper() - assert returned_address.is_residential is expected_residential_indicator - elif locale == "international": - if test_method == "validate": - canada_valid_avs_assertions( - original_address=original_address, - validated_address=returned_address, - expected_residential_indicator=expected_residential_indicator, - ) - if test_method == "normalize": - canada_valid_normalize_assertions( - original_address=original_address, - normalized_address=returned_address, - expected_residential_indicator=expected_residential_indicator, - ) - - -def canada_valid_avs_assertions( - original_address: Address, - validated_address: AddressValidateResult, - expected_residential_indicator, -) -> None: - """ - A set of common assertions that are regularly made on the canadian_address - used for testing `validate_address`. - """ - address = validated_address.normalized_address - assert type(validated_address) is AddressValidateResult - assert validated_address.is_valid is True - assert type(address) is Address - assert len(validated_address.info) == 0 - assert len(validated_address.warnings) == 0 - assert len(validated_address.errors) == 0 - assert address is not None - assert address.city_locality == original_address.city_locality - assert address.state_province == original_address.state_province.title() - assert address.postal_code == "M6K 3C3" - assert address.country_code == original_address.country_code.upper() - assert address.is_residential is expected_residential_indicator - - -def us_valid_normalize_assertions( - original_address: Address, - normalized_address: Address, - expected_residential_indicator, -) -> None: - """ - A set of common assertions that are regularly made on the commercial US address - used for `normalized_address` testing. - """ - assert type(normalized_address) is Address - assert normalized_address.city_locality == original_address.city_locality.upper() - assert normalized_address.state_province == original_address.state_province.upper() - assert normalized_address.postal_code == original_address.postal_code - assert normalized_address.country_code == original_address.country_code.upper() - assert normalized_address.is_residential is expected_residential_indicator - - -def canada_valid_normalize_assertions( - original_address: Address, - normalized_address: Address, - expected_residential_indicator, -) -> None: - """ - A set of common assertions that are regularly made on the canadian_address - used for testing `validate_address`. - """ - assert type(normalized_address) is Address - assert normalized_address.city_locality == original_address.city_locality - assert normalized_address.state_province == original_address.state_province.title() - assert normalized_address.postal_code == "M6K 3C3" - assert normalized_address.country_code == original_address.country_code.upper() - assert normalized_address.is_residential is expected_residential_indicator - - -def assert_on_429_exception(err: ShipEngineError, error_class: object) -> None: - error = err.to_dict() - assert type(err) == error_class - assert error["request_id"] is not None - assert error["request_id"].startswith("req_") - assert error["source"] is ErrorSource.SHIPENGINE.value - assert error["error_type"] is ErrorType.SYSTEM.value - assert error["error_code"] is ErrorCode.RATE_LIMIT_EXCEEDED.value - assert error["message"] == "You have exceeded the rate limit." - assert error["url"] is not None - assert error["url"] == "https://www.shipengine.com/docs/rate-limits" diff --git a/tests/util/test_iso_string.py b/tests/util/test_iso_string.py deleted file mode 100644 index b204828..0000000 --- a/tests/util/test_iso_string.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Testing the IsoString class object.""" -import datetime - -from shipengine_sdk.util.iso_string import IsoString - - -class TestIsoString: - _test_iso_string_no_tz: str = "2021-06-10T21:00:00.000" - - def test_to_string(self) -> None: - iso_str = IsoString(self._test_iso_string_no_tz).to_string() - assert type(iso_str) is str - - def test_to_datetime_object(self) -> None: - iso_str = IsoString(self._test_iso_string_no_tz).to_datetime_object() - assert type(iso_str) is datetime.datetime - - def test_static_valid_iso_check(self) -> None: - assert IsoString.is_valid_iso_string_with_tz(self._test_iso_string_no_tz) is True - - def test_static_valid_iso_check_failure(self) -> None: - assert IsoString.is_valid_iso_string_with_tz("2021-06-10T21:00:00.000K") is False From 02c37615b59cab036e929923431e59607fbf0e17 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 3 Aug 2021 12:21:44 -0500 Subject: [PATCH 02/13] work in progress - implementing services --- shipengine_sdk/http_client/client.py | 31 +++-- shipengine_sdk/services/__init__.py | 1 - shipengine_sdk/services/address_validation.py | 47 ------- .../services/get_carrier_accounts.py | 61 --------- shipengine_sdk/services/track_package.py | 27 ---- shipengine_sdk/shipengine.py | 124 ++++++++++-------- shipengine_sdk/shipengine_config.py | 16 +-- shipengine_sdk/util/__init__.py | 1 - shipengine_sdk/util/iso_string.py | 60 --------- tests/test_shipengine_config.py | 99 ++++++-------- 10 files changed, 136 insertions(+), 331 deletions(-) delete mode 100644 shipengine_sdk/services/__init__.py delete mode 100644 shipengine_sdk/services/address_validation.py delete mode 100644 shipengine_sdk/services/get_carrier_accounts.py delete mode 100644 shipengine_sdk/services/track_package.py delete mode 100644 shipengine_sdk/util/iso_string.py diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 2f13c39..70a8a2d 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -13,9 +13,8 @@ from shipengine_sdk import __version__ -from ..enums import HTTPVerbs +from ..enums import ErrorCode, ErrorSource, ErrorType, HTTPVerbs from ..errors import RateLimitExceededError, ShipEngineError -from ..models import ErrorCode, ErrorSource, ErrorType from ..shipengine_config import ShipEngineConfig @@ -47,7 +46,7 @@ def __init__(self) -> None: """A `JSON-RPC 2.0` HTTP client used to send all HTTP requests from the SDK.""" self.session = requests.session() - def get(self, endpoint: str, config: ShipEngineConfig): + def get(self, endpoint: str, config: ShipEngineConfig) -> Dict[str, Any]: """Send an HTTP GET request.""" return self._request_loop( http_method=HTTPVerbs.GET.value, endpoint=endpoint, params=None, config=config @@ -55,7 +54,7 @@ def get(self, endpoint: str, config: ShipEngineConfig): def post( self, endpoint: str, config: ShipEngineConfig, params: Optional[Dict[str, Any]] = None - ): + ) -> Dict[str, Any]: """Send an HTTP POST request.""" return self._request_loop( http_method=HTTPVerbs.POST.value, endpoint=endpoint, params=params, config=config @@ -84,7 +83,11 @@ def _request_loop( while retry <= config.retries: try: api_response = self._send_request( - http_method=http_method, body=params, retry=retry, config=config + http_method=http_method, + endpoint=endpoint, + body=params, + retry=retry, + config=config, ) except Exception as err: if ( @@ -100,19 +103,24 @@ def _request_loop( return api_response def _send_request( - self, http_method: str, body: Optional[Dict[str, Any]], retry: int, config: ShipEngineConfig + self, + http_method: str, + endpoint: str, + body: 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. """ - client: Session = self._request_retry_session(retries=config.retries) base_uri = base_url(config=config) + client: Session = self._request_retry_session(retries=config.retries, url_base=base_uri) req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key) req: Request = Request( method=http_method, - url=base_uri, + url=f"{base_uri}{endpoint}", data=json.dumps(body), headers=req_headers, auth=ShipEngineAuth(config.api_key), @@ -136,7 +144,11 @@ def _send_request( return resp_body def _request_retry_session( - self, retries: int = 1, backoff_factor=1, status_force_list=(429, 500, 502, 503, 504) + self, + url_base: str, + retries: int = 1, + backoff_factor=1, + status_force_list=(429, 500, 502, 503, 504), ) -> Session: """A requests `Session()` that has retries enforced.""" retry: Retry = Retry( @@ -149,6 +161,7 @@ def _request_retry_session( adapter: HTTPAdapter = HTTPAdapter(max_retries=retry) self.session.mount("http://", adapter=adapter) self.session.mount("https://", adapter=adapter) + self.session.url_base = url_base return self.session @staticmethod diff --git a/shipengine_sdk/services/__init__.py b/shipengine_sdk/services/__init__.py deleted file mode 100644 index a677db9..0000000 --- a/shipengine_sdk/services/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""ShipEngine SDK service objects.""" diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py deleted file mode 100644 index c3ca304..0000000 --- a/shipengine_sdk/services/address_validation.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Validate a single address or multiple addresses.""" -from typing import Any, Dict - -from shipengine_sdk.enums import RPCMethods - -from ..jsonrpc import rpc_request -from ..models.address import Address, AddressValidateResult -from ..shipengine_config import ShipEngineConfig -from ..util import does_normalized_address_have_errors - - -def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResult: - """ - Validate a single address via the `address/validate` remote procedure. - - :param Address address: The address to be validate. - :param ShipEngineConfig config: The global ShipEngine configuration object. - :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the - validated and normalized address. - """ - api_response: Dict[str, Any] = rpc_request( - method=RPCMethods.ADDRESS_VALIDATE.value, - config=config, - params={"address": address.to_dict()}, - ) - result: Dict[str, Any] = api_response["result"] - return AddressValidateResult( - is_valid=result["isValid"], - request_id=api_response["id"], - normalized_address=Address.from_dict(result["normalizedAddress"]) - if "normalizedAddress" in result and result["normalizedAddress"] is not None - else None, - messages=result["messages"], - ) - - -def normalize(address: Address, config: ShipEngineConfig) -> Address: - """ - Normalize a given address into a standardized format. - - :param Address address: The address to be validate. - :param ShipEngineConfig config: The global ShipEngine configuration object. - :returns: :class:`Address`: The normalized address returned from ShipEngine API. - """ - validation_result: AddressValidateResult = validate(address=address, config=config) - does_normalized_address_have_errors(result=validation_result) - return validation_result.normalized_address diff --git a/shipengine_sdk/services/get_carrier_accounts.py b/shipengine_sdk/services/get_carrier_accounts.py deleted file mode 100644 index c5da08e..0000000 --- a/shipengine_sdk/services/get_carrier_accounts.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Fetch the carrier account connected to a given ShipEngine Account -based on the API Key passed into the ShipEngine SDK. -""" -from typing import List, Optional - -from ..jsonrpc import rpc_request -from ..models import CarrierAccount, RPCMethods -from ..shipengine_config import ShipEngineConfig - -cached_accounts: List = list() - - -class GetCarrierAccounts: - @staticmethod - def fetch_carrier_accounts( - config: ShipEngineConfig, carrier_code: Optional[str] = None - ) -> List[CarrierAccount]: - global cached_accounts - if carrier_code is not None: - api_response = rpc_request( - method=RPCMethods.LIST_CARRIERS.value, - config=config, - params={"carrierCode": carrier_code}, - ) - else: - api_response = rpc_request( - method=RPCMethods.LIST_CARRIERS.value, - config=config, - ) - - accounts = api_response["result"]["carrierAccounts"] - cached_accounts = list() - for account in accounts: - carrier_account = CarrierAccount(account) - cached_accounts.append(carrier_account) - - return cached_accounts - - def fetch_cached_carrier_accounts( - self, config: ShipEngineConfig, carrier_code: Optional[str] - ) -> List[CarrierAccount]: - global cached_accounts - accounts = cached_accounts - return ( - accounts - if len(cached_accounts) > 0 - else self.fetch_carrier_accounts(config=config, carrier_code=carrier_code) - ) - - @staticmethod - def get_cached_accounts_by_carrier_code(carrier_code: Optional[str]) -> List[CarrierAccount]: - global cached_accounts - accounts = list() - if carrier_code is None: - return cached_accounts - else: - for account in cached_accounts: - if account.carrier["code"] == carrier_code: - accounts.append(account) - return accounts diff --git a/shipengine_sdk/services/track_package.py b/shipengine_sdk/services/track_package.py deleted file mode 100644 index 764d6c2..0000000 --- a/shipengine_sdk/services/track_package.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Track a given package to obtain status updates on it's progression through the fulfillment cycle.""" -from typing import Union - -from ..jsonrpc import rpc_request -from ..models import RPCMethods, TrackingQuery, TrackPackageResult -from ..shipengine_config import ShipEngineConfig -from ..util import is_package_id_valid - - -def track(tracking_data: Union[str, TrackingQuery], config: ShipEngineConfig) -> TrackPackageResult: - if type(tracking_data) is str: - is_package_id_valid(tracking_data) - - api_response = rpc_request( - method=RPCMethods.TRACK_PACKAGE.value, - config=config, - params={"packageId": tracking_data}, - ) - - return TrackPackageResult(api_response, config) - - if type(tracking_data) is TrackingQuery: - api_response = rpc_request( - 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 367f941..e72c2b0 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -3,11 +3,6 @@ from shipengine_sdk.enums import Endpoints -# from .models import CarrierAccount, TrackingQuery, TrackPackageResult -# from .models.address import Address, AddressValidateResult -# from .services.address_validation import normalize, validate -# from .services.get_carrier_accounts import GetCarrierAccounts -# from .services.track_package import track from .http_client import ShipEngineClient from .shipengine_config import ShipEngineConfig @@ -34,61 +29,82 @@ def __init__(self, config: Union[str, Dict[str, Any], ShipEngineConfig]) -> None elif type(config) is dict: self.config = ShipEngineConfig(config) + def create_label_from_rate( + self, rate_id: str, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + When retrieving rates for shipments using the /rates endpoint, the returned information contains a rateId + property that can be used to generate a label without having to refill in the shipment information repeatedly. + See: https://shipengine.github.io/shipengine-openapi/#operation/create_label_from_rate + + :param str rate_id: The rate_id you wish to create a shipping label for. + :params Dict[str, Any] params: A list of label params that will dictate the label display and + level of verification. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: A label that corresponds the to shipment details for a rate_id you provided. + """ + config = self.config.merge(new_config=config) + return self.client.post(endpoint=f"v1/labels/rates/{rate_id}", params=params, config=config) + + def create_label_from_shipment( + self, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Purchase and print a shipping label for a given shipment. + See: https://shipengine.github.io/shipengine-openapi/#operation/create_label + # TODO: Add docstring type annotations. + """ + config = self.config.merge(new_config=config) + return self.client.post(endpoint="v1/labels", params=params, config=config) + + def get_rates_from_shipment( + self, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Given some shipment details and rate options, this endpoint returns a list of rate quotes. + See: https://shipengine.github.io/shipengine-openapi/#operation/calculate_rates + # TODO: Add docstring type annotations. + """ + config = self.config.merge(new_config=config) + return self.client.post(endpoint="v1/rates", params=params, config=config) + + def list_carriers(self, config: Dict[str, Any] = None) -> Dict[str, Any]: + """Fetch the carrier accounts connected to your ShipEngine Account.""" + config = self.config.merge(new_config=config) + return self.client.get(endpoint=Endpoints.LIST_CARRIERS.value, config=config) + def validate_addresses( - self, params: Dict[str, Any], config: Union[str, Dict[str, Any], ShipEngineConfig] = None + self, address: Dict[str, Any], config: Union[str, Dict[str, Any]] = None ) -> Dict[str, Any]: """ - Validate an address in nearly any countryCode in the world. + Address validation ensures accurate addresses and can lead to reduced shipping costs by preventing address + correction surcharges. ShipEngine cross references multiple databases to validate addresses and identify + potential deliverability issues. + See: https://shipengine.github.io/shipengine-openapi/#operation/validate_address - :param Dict[str, Any] params: The address to be validate. - :param Union[str, Dict[str, Any], ShipEngineConfig] config: The global ShipEngine configuration object. - :returns: Dict[str, Any]: The response from ShipEngine API including the - validated and normalized address. + :param Dict[str, Any] address: The address to be validate. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns: Dict[str, Any]: The response from ShipEngine API including the validated and normalized address. """ config = self.config.merge(new_config=config) return self.client.post( - endpoint=Endpoints.ADDRESSES_VALIDATE.value, params=params, config=config + endpoint=Endpoints.ADDRESSES_VALIDATE.value, params=address, config=config ) - # def validate_address( - # self, address: Address, config: Optional[Union[Dict[str, Any], ShipEngineConfig]] = None - # ) -> AddressValidateResult: - # """ - # Validate an address in nearly any countryCode in the world. - # - # :param Address address: The address to be validate. - # :param ShipEngineConfig config: The global ShipEngine configuration object. - # :returns: :class:`AddressValidateResult`: The response from ShipEngine API including the - # validated and normalized address. - # """ - # 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 - # ) -> Address: - # """Normalize a given address into a standardized format used by carriers.""" - # 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, - # ) -> List[CarrierAccount]: - # """Fetch a list of the carrier accounts connected to your ShipEngine Account.""" - # 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, 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 = self.config.merge(new_config=config) - # return track(tracking_data=tracking_data, config=config) + def void_label_by_label_id( + self, label_id: str, config: Union[str, Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Void label with a Label Id. + See: https://shipengine.github.io/shipengine-openapi/#operation/void_label + + :param str label_id: The label_id of the label you wish to void. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: The response from ShipEngine API confirming the label was successfully voided or + unable to be voided. + """ + config = self.config.merge(new_config=config) + return self.client.put(endpoint=f"v1/labels/{label_id}/void", config=config) diff --git a/shipengine_sdk/shipengine_config.py b/shipengine_sdk/shipengine_config.py index 596bbc1..3bd6617 100644 --- a/shipengine_sdk/shipengine_config.py +++ b/shipengine_sdk/shipengine_config.py @@ -2,13 +2,12 @@ import json from typing import Any, Dict, Optional -from .events import ShipEngineEventListener -from .models import Endpoints +from .enums import BaseURL from .util import is_api_key_valid, is_retries_valid, is_timeout_valid class ShipEngineConfig: - DEFAULT_BASE_URI: str = Endpoints.SHIPENGINE_RPC_URL.value + DEFAULT_BASE_URI: str = BaseURL.SHIPENGINE_RPC_URL.value """A ShipEngine API Key, sandbox API Keys start with `TEST_`.""" DEFAULT_PAGE_SIZE: int = 50 @@ -50,11 +49,6 @@ def __init__(self, config: Dict[str, Any]) -> None: else: self.retries: int = self.DEFAULT_RETRIES - if "event_listener" in config: - self.event_listener = config["event_listener"] - else: - self.event_listener = ShipEngineEventListener() - def merge(self, new_config: Optional[Dict[str, Any]] = None): """ The method allows the merging of a method-level configuration @@ -85,12 +79,6 @@ def merge(self, new_config: Optional[Dict[str, Any]] = None): {"timeout": new_config["timeout"]} ) if "timeout" in new_config else config.update({"timeout": self.timeout}) - config.update( - {"event_listener": new_config["event_listener"]} - ) if "event_listener" in new_config else config.update( - {"event_listener": self.event_listener} - ) - return ShipEngineConfig(config) def to_dict(self): diff --git a/shipengine_sdk/util/__init__.py b/shipengine_sdk/util/__init__.py index 638584c..bd4b611 100644 --- a/shipengine_sdk/util/__init__.py +++ b/shipengine_sdk/util/__init__.py @@ -1,5 +1,4 @@ """Testing a string manipulation helper function.""" -from .iso_string import IsoString from .sdk_assertions import * # noqa diff --git a/shipengine_sdk/util/iso_string.py b/shipengine_sdk/util/iso_string.py deleted file mode 100644 index 657fb4b..0000000 --- a/shipengine_sdk/util/iso_string.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Initial Docstring""" -import re -from datetime import datetime - -from shipengine_sdk.enums import RegexPatterns - - -class IsoString: - def __init__(self, iso_string: str) -> None: - """ - A string representing a Date, DateTime, or DateTime with Timezone. The object - also has a method to return a `datetime.datetime` object, which is the native - datetime object in python as of 3.7. - This class object takes in an **ISO-8601** string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 - :param str iso_string: An `ISO-8601` string. Learn more here: https://en.wikipedia.org/wiki/ISO_8601 - """ - 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: - iso_string = self._maybe_add_microseconds(self.iso_string) - if self.has_timezone(): - return datetime.strptime(iso_string, "%Y-%m-%dT%H:%M:%S.%fZ") - else: - return datetime.fromisoformat(iso_string) - - def has_timezone(self) -> bool: - if self.is_valid_iso_string_with_tz(self.iso_string): - return False if self.is_valid_iso_string_with_tz_no_tz(self.iso_string) else True - - @staticmethod - def is_valid_iso_string_with_tz(iso_str: str): - pattern = re.compile(RegexPatterns.VALID_ISO_STRING.value) - if pattern.match(iso_str): - return True - else: - return False - - @staticmethod - def is_valid_iso_string_with_tz_no_tz(iso_str: str): - pattern = re.compile(RegexPatterns.VALID_ISO_STRING_NO_TZ.value) - if pattern.match(iso_str): - return True - else: - return False - - @staticmethod - def _maybe_add_microseconds(iso_str: str): - if "." not in iso_str: - if "Z" not in iso_str: - return iso_str + ".0" - else: - return iso_str[:-1] + ".0Z" - else: - return iso_str diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index fadc1e9..363ef21 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -1,10 +1,9 @@ """Testing the ShipEngineConfig object.""" import pytest -from shipengine_sdk import ShipEngine, ShipEngineConfig -from shipengine_sdk.enums import Endpoints +from shipengine_sdk import ShipEngineConfig +from shipengine_sdk.enums import BaseURL from shipengine_sdk.errors import InvalidFieldValueError, ValidationError -from shipengine_sdk.models.address import Address from shipengine_sdk.util import api_key_validation_error_assertions from shipengine_sdk.util.sdk_assertions import timeout_validation_error_assertions @@ -17,20 +16,6 @@ def stub_config() -> dict: return dict(api_key="baz_sim", page_size=50, retries=2, timeout=15) -def valid_residential_address() -> Address: - """ - Return a test Address object with valid residential - address information. - """ - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - def config_with_no_api_key() -> ShipEngineConfig: """Return an error from no API Key.""" return ShipEngineConfig(dict(retries=2)) @@ -97,7 +82,7 @@ def test_valid_custom_config(self): """ valid_config: ShipEngineConfig = complete_valid_config() assert valid_config.api_key == "baz_sim" - assert valid_config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value + assert valid_config.base_uri is BaseURL.SHIPENGINE_RPC_URL.value assert valid_config.page_size == 50 assert valid_config.retries == 2 assert valid_config.timeout == 10 @@ -151,44 +136,44 @@ def test_invalid_timeout_provided(self): e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." ) - def test_invalid_timeout_in_method_call(self): - """DX-1447 - Invalid timeout in method call configuration.""" - timeout = -5 - try: - shipengine = ShipEngine(stub_config()) - shipengine.validate_address( - address=valid_residential_address(), config=dict(timeout=timeout) - ) - except InvalidFieldValueError as e: - timeout_validation_error_assertions(e) - assert ( - e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." - ) - - def test_invalid_retries_in_method_call(self): - """DX-1446 - Invalid retries in method call configuration.""" - retries = -5 - try: - shipengine = ShipEngine(stub_config()) - shipengine.validate_address( - address=valid_residential_address(), config=dict(retries=retries) - ) - except InvalidFieldValueError as e: - timeout_validation_error_assertions(e) - assert ( - e.message == f"retries - Retries must be zero or greater. {retries} was provided." - ) - - def test_invalid_api_key_in_method_call(self): - """DX-1445 - Invalid api_key in method call configuration.""" - api_key = " " - try: - shipengine = ShipEngine(stub_config()) - shipengine.validate_address( - address=valid_residential_address(), config=dict(api_key=api_key) - ) - except Exception as e: - api_key_validation_error_assertions(e) + # def test_invalid_timeout_in_method_call(self): + # """DX-1447 - Invalid timeout in method call configuration.""" + # timeout = -5 + # try: + # shipengine = ShipEngine(stub_config()) + # shipengine.validate_address( + # address=valid_residential_address(), config=dict(timeout=timeout) + # ) + # except InvalidFieldValueError as e: + # timeout_validation_error_assertions(e) + # assert ( + # e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." + # ) + + # def test_invalid_retries_in_method_call(self): + # """DX-1446 - Invalid retries in method call configuration.""" + # retries = -5 + # try: + # shipengine = ShipEngine(stub_config()) + # shipengine.validate_address( + # address=valid_residential_address(), config=dict(retries=retries) + # ) + # except InvalidFieldValueError as e: + # timeout_validation_error_assertions(e) + # assert ( + # e.message == f"retries - Retries must be zero or greater. {retries} was provided." + # ) + + # def test_invalid_api_key_in_method_call(self): + # """DX-1445 - Invalid api_key in method call configuration.""" + # api_key = " " + # try: + # shipengine = ShipEngine(stub_config()) + # shipengine.validate_address( + # address=valid_residential_address(), config=dict(api_key=api_key) + # ) + # except Exception as e: + # api_key_validation_error_assertions(e) def test_config_defaults(self) -> None: """Test default retries.""" @@ -197,7 +182,7 @@ def test_config_defaults(self) -> None: assert config.retries == 1 assert config.page_size == 50 assert config.timeout == 5 - assert config.base_uri is Endpoints.SHIPENGINE_RPC_URL.value + assert config.base_uri is BaseURL.SHIPENGINE_RPC_URL.value def test_to_dict_method(self) -> None: """Test the to_dict convenience method.""" From d5987f7ba66a8de5c8a38167b646342582ddcd5e Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 3 Aug 2021 15:19:19 -0500 Subject: [PATCH 03/13] Implementation of services complete - working on tests now --- shipengine_sdk/shipengine.py | 64 +++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index e72c2b0..4cceba9 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 Any, Dict, Union +from typing import Any, Dict, List, Union from shipengine_sdk.enums import Endpoints @@ -38,11 +38,11 @@ def create_label_from_rate( See: https://shipengine.github.io/shipengine-openapi/#operation/create_label_from_rate :param str rate_id: The rate_id you wish to create a shipping label for. - :params Dict[str, Any] params: A list of label params that will dictate the label display and + :param Dict[str, Any] params: A dictionary of label params that will dictate the label display and level of verification. :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values for properties of the global ShipEngineConfig object. - :returns Dict[str, Any]: A label that corresponds the to shipment details for a rate_id you provided. + :returns Dict[str, Any]: A label that corresponds the to shipment details for the rate_id provided. """ config = self.config.merge(new_config=config) return self.client.post(endpoint=f"v1/labels/rates/{rate_id}", params=params, config=config) @@ -53,7 +53,11 @@ def create_label_from_shipment( """ Purchase and print a shipping label for a given shipment. See: https://shipengine.github.io/shipengine-openapi/#operation/create_label - # TODO: Add docstring type annotations. + + :param Dict[str, Any] params: A dictionary of shipment details for the label creation. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: A label that corresponds the to shipment details provided. """ config = self.config.merge(new_config=config) return self.client.post(endpoint="v1/labels", params=params, config=config) @@ -64,18 +68,62 @@ def get_rates_from_shipment( """ Given some shipment details and rate options, this endpoint returns a list of rate quotes. See: https://shipengine.github.io/shipengine-openapi/#operation/calculate_rates - # TODO: Add docstring type annotations. + + :param Dict[str, Any] params: A dictionary of shipment details for the label creation. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: A label that corresponds the to shipment details provided. """ config = self.config.merge(new_config=config) return self.client.post(endpoint="v1/rates", params=params, config=config) def list_carriers(self, config: Dict[str, Any] = None) -> Dict[str, Any]: - """Fetch the carrier accounts connected to your ShipEngine Account.""" + """ + Fetch the carrier accounts connected to your ShipEngine Account. + + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: The carrier accounts associated with a given ShipEngine Account. + """ config = self.config.merge(new_config=config) return self.client.get(endpoint=Endpoints.LIST_CARRIERS.value, config=config) + def track_package_by_label_id( + self, label_id: str, config: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Retrieve a given shipping label's tracking information with a label_id. + See: https://shipengine.github.io/shipengine-openapi/#operation/get_tracking_log_from_label + + :param str label_id: The label_id for a shipment you wish to get tracking information for. + (Best option if you create labels via ShipEngine API) + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: Tracking information corresponding to the label_id provided. + """ + config = self.config.merge(new_config=config) + return self.client.get(endpoint=f"v1/labels/{label_id}/track", config=config) + + def track_package_by_carrier_code_and_tracking_number( + self, carrier_code: str, tracking_number: str, config: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Retrieve the label's tracking information with Carrier Code and Tracking Number. + See: https://shipengine.github.io/shipengine-openapi/#operation/get_tracking_log + + :param str carrier_code: The carrier_code for the carrier servicing the shipment. + :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values + for properties of the global ShipEngineConfig object. + :returns Dict[str, Any]: Tracking information corresponding to the carrier_code and tracking_number provided. + """ + config = self.config.merge(new_config=config) + return self.client.get( + endpoint=f"v1/tracking?carrier_code={carrier_code}&tracking_number={tracking_number}", + config=config, + ) + def validate_addresses( - self, address: Dict[str, Any], config: Union[str, Dict[str, Any]] = None + self, address: List[Dict[str, Any]], config: Union[str, Dict[str, Any]] = None ) -> Dict[str, Any]: """ Address validation ensures accurate addresses and can lead to reduced shipping costs by preventing address @@ -83,7 +131,7 @@ def validate_addresses( potential deliverability issues. See: https://shipengine.github.io/shipengine-openapi/#operation/validate_address - :param Dict[str, Any] address: The address to be validate. + :param List[Dict[str, Any]] address: A list containing the address(es) to be validated. :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values for properties of the global ShipEngineConfig object. :returns: Dict[str, Any]: The response from ShipEngine API including the validated and normalized address. From 47af63e9c1288d7fd53a8167ff717712466a9fbe Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Tue, 3 Aug 2021 17:12:23 -0500 Subject: [PATCH 04/13] work in progress - testing client --- shipengine_sdk/http_client/client.py | 5 +-- shipengine_sdk/util/sdk_assertions.py | 47 ++++++++++++--------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index 70a8a2d..efdf04f 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -16,6 +16,7 @@ from ..enums import ErrorCode, ErrorSource, ErrorType, HTTPVerbs from ..errors import RateLimitExceededError, ShipEngineError from ..shipengine_config import ShipEngineConfig +from ..util import check_response_for_errors def base_url(config) -> str: @@ -138,9 +139,9 @@ def _send_request( ) resp_body: Dict[str, Any] = resp.json() - # status_code: int = resp.status_code + status_code: int = resp.status_code - # check_response_for_errors(status_code=status_code, response_body=resp_body, config=config) + check_response_for_errors(status_code=status_code, response_body=resp_body, config=config) return resp_body def _request_retry_session( diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index 0dd39e5..f8042de 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -176,55 +176,50 @@ def timeout_validation_error_assertions(error) -> None: def check_response_for_errors(status_code: int, response_body: Dict[str, Any], config) -> None: - """Checks response and status_code for 404, 429, and 500 error cases and raises an approved exception.""" + """Checks response and status_code for 400, 404, 429, and 500 error cases and raises an approved exception.""" - # 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"] - error_data = error["data"] - raise ClientSystemError( + error = response_body["errors"][0] + + if status_code == 400: + raise ShipEngineError( message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], + source=ErrorSource.SHIPENGINE.value, + error_type=error["error_type"], + error_code=error["error_code"], ) - elif status_code == 404: + + if status_code == 404: raise ShipEngineError( - message=f"Resource not found, please check the base_uri you have set and try again. [{config.base_uri}] is currently set.", # noqa + message=error["message"], source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.SYSTEM.value, - error_code=ErrorCode.NOT_FOUND.value, + error_type=error["error_type"], + error_code=error["error_code"], ) # 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"] - error_data = error["data"] - retry_after = error_data["details"]["retryAfter"] + if "errors" in response_body and status_code == 429: + retry_after = error["details"]["retryAfter"] if retry_after > config.timeout: raise ClientTimeoutError( retry_after=config.timeout, source=ErrorSource.SHIPENGINE.value, - request_id=response_body["id"], + request_id=response_body["request_id"], ) else: raise RateLimitExceededError( retry_after=retry_after, source=ErrorSource.SHIPENGINE.value, - request_id=response_body["id"], + request_id=response_body["request_id"], ) # Check if the status code is 500 and raises an error if so. if status_code == 500: - error = response_body["error"] - error_data = error["data"] raise ClientSystemError( message=error["message"], - request_id=response_body["id"], - source=error_data["source"], - error_type=error_data["type"], - error_code=error_data["code"], + request_id=response_body["request_id"], + source=error["error_source"], + error_type=error["error_type"], + error_code=error["error_code"], ) From 2d144e283bc18d9aea61635d8a76ad4f1ca8efc9 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 14:37:06 -0500 Subject: [PATCH 05/13] wrap up - adding some tests for public functions with mocks to increase coverage --- poetry.lock | 211 +++-- pyproject.toml | 1 - shipengine_sdk/enums/__init__.py | 1 + shipengine_sdk/errors/__init__.py | 30 +- shipengine_sdk/http_client/client.py | 5 +- shipengine_sdk/shipengine.py | 8 +- shipengine_sdk/util/sdk_assertions.py | 141 ++- tests/errors/test_errors.py | 169 ++++ tests/services/__init__.py | 1 + tests/services/test_get_rate_from_shipment.py | 211 +++++ tests/services/test_list_carriers.py | 857 ++++++++++++++++++ tests/services/test_validate_addresses.py | 67 ++ tests/test_shipengine.py | 47 +- tests/test_shipengine_config.py | 107 ++- tests/test_snake_case_converter.py | 3 +- tests/util/__init__.py | 2 + tests/util/test_helpers.py | 52 ++ tox.ini | 1 - 18 files changed, 1654 insertions(+), 260 deletions(-) create mode 100644 tests/errors/test_errors.py create mode 100644 tests/services/__init__.py create mode 100644 tests/services/test_get_rate_from_shipment.py create mode 100644 tests/services/test_list_carriers.py create mode 100644 tests/services/test_validate_addresses.py create mode 100644 tests/util/__init__.py create mode 100644 tests/util/test_helpers.py diff --git a/poetry.lock b/poetry.lock index 4e38846..566626e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -74,6 +74,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] pytz = ">=2015.7" +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] + [[package]] name = "base58" version = "2.1.0" @@ -131,6 +146,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "charset-normalizer" +version = "2.0.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "click" version = "8.0.1" @@ -164,7 +190,7 @@ toml = ["toml"] [[package]] name = "coveralls" -version = "3.1.0" +version = "3.2.0" description = "Show coverage stats online via coveralls.io" category = "dev" optional = false @@ -265,7 +291,7 @@ base58 = ">=2.1.0,<3.0.0" [[package]] name = "identify" -version = "2.2.10" +version = "2.2.12" description = "File identification library for Python" category = "dev" optional = false @@ -276,11 +302,11 @@ license = ["editdistance-s"] [[package]] name = "idna" -version = "2.10" +version = "3.2" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.5" [[package]] name = "imagesize" @@ -292,7 +318,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.6.1" +version = "4.6.3" description = "Read metadata from Python packages" category = "dev" optional = false @@ -317,7 +343,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.1" +version = "5.9.3" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -353,16 +379,16 @@ python-versions = ">=3.6" [[package]] name = "marshmallow" -version = "3.12.1" +version = "3.13.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." category = "main" optional = false python-versions = ">=3.5" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==4.0.0)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.4)"] -lint = ["mypy (==0.812)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==4.1.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.6)"] +lint = ["mypy (==0.910)", "flake8 (==3.9.2)", "flake8-bugbear (==21.4.3)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -421,11 +447,23 @@ pyparsing = ">=2.0.2" [[package]] name = "pathspec" -version = "0.8.1" +version = "0.9.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.2.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -605,7 +643,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" -version = "2021.7.6" +version = "2021.8.3" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -613,21 +651,21 @@ python-versions = "*" [[package]] name = "requests" -version = "2.25.1" +version = "2.26.0" description = "Python HTTP for Humans." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<5" -idna = ">=2.5,<3" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} urllib3 = ">=1.21.1,<1.27" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] [[package]] name = "responses" @@ -781,7 +819,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tox" -version = "3.23.1" +version = "3.24.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -845,22 +883,23 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.4.7" +version = "20.7.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -appdirs = ">=1.4.3,<2" +"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.0.0,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] name = "watchdog" @@ -967,6 +1006,10 @@ babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, +] base58 = [ {file = "base58-2.1.0-py3-none-any.whl", hash = "sha256:8225891d501b68c843ffe30b86371f844a21c6ba00da76f52f9b998ba771fb48"}, {file = "base58-2.1.0.tar.gz", hash = "sha256:171a547b4a3c61e1ae3807224a6f7aec75e364c4395e7562649d7335768001a2"}, @@ -986,6 +1029,10 @@ chardet = [ {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] +charset-normalizer = [ + {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, + {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -1049,8 +1096,8 @@ coverage = [ {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] coveralls = [ - {file = "coveralls-3.1.0-py2.py3-none-any.whl", hash = "sha256:172fb79c5f61c6ede60554f2cac46deff6d64ee735991fb2124fb414e188bdb4"}, - {file = "coveralls-3.1.0.tar.gz", hash = "sha256:9b3236e086627340bf2c95f89f757d093cbed43d17179d3f4fb568c347e7d29a"}, + {file = "coveralls-3.2.0-py2.py3-none-any.whl", hash = "sha256:aedfcc5296b788ebaf8ace8029376e5f102f67c53d1373f2e821515c15b36527"}, + {file = "coveralls-3.2.0.tar.gz", hash = "sha256:15a987d9df877fff44cd81948c5806ffb6eafb757b3443f737888358e96156ee"}, ] dataclasses-json = [ {file = "dataclasses-json-0.5.4.tar.gz", hash = "sha256:6c3976816fd3cdd8db3be2b516b64fc083acd46ac22c680d3dc24cb1d6ae3367"}, @@ -1083,28 +1130,28 @@ fuuid = [ {file = "fuuid-0.1.0.tar.gz", hash = "sha256:ce8aec9ae81078941fa730dca0bf5ff7ca56675bea9327e938f1a04c8fad18b2"}, ] identify = [ - {file = "identify-2.2.10-py2.py3-none-any.whl", hash = "sha256:18d0c531ee3dbc112fa6181f34faa179de3f57ea57ae2899754f16a7e0ff6421"}, - {file = "identify-2.2.10.tar.gz", hash = "sha256:5b41f71471bc738e7b586308c3fca172f78940195cb3bf6734c1e66fdac49306"}, + {file = "identify-2.2.12-py2.py3-none-any.whl", hash = "sha256:a510cbe155f39665625c8a4c4b4f9360cbce539f51f23f47836ab7dd852db541"}, + {file = "identify-2.2.12.tar.gz", hash = "sha256:242332b3bdd45a8af1752d5d5a3afb12bee26f8e67c4be06e394f82d05ef1a4d"}, ] idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, + {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, + {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, ] imagesize = [ {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"}, - {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"}, + {file = "importlib_metadata-4.6.3-py3-none-any.whl", hash = "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b"}, + {file = "importlib_metadata-4.6.3.tar.gz", hash = "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, - {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, + {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, + {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] jinja2 = [ {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"}, @@ -1147,8 +1194,8 @@ markupsafe = [ {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, ] marshmallow = [ - {file = "marshmallow-3.12.1-py2.py3-none-any.whl", hash = "sha256:b45cde981d1835145257b4a3c5cb7b80786dcf5f50dd2990749a50c16cb48e01"}, - {file = "marshmallow-3.12.1.tar.gz", hash = "sha256:8050475b70470cc58f4441ee92375db611792ba39ca1ad41d39cad193ea9e040"}, + {file = "marshmallow-3.13.0-py2.py3-none-any.whl", hash = "sha256:dd4724335d3c2b870b641ffe4a2f8728a1380cd2e7e2312756715ffeaa82b842"}, + {file = "marshmallow-3.13.0.tar.gz", hash = "sha256:c67929438fd73a2be92128caa0325b1b5ed8b626d91a094d2f7f2771bf1f1c0e"}, ] marshmallow-enum = [ {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, @@ -1210,8 +1257,12 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] pathspec = [ - {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, - {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, + {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, @@ -1299,51 +1350,43 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ - {file = "regex-2021.7.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222"}, - {file = "regex-2021.7.6-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407"}, - {file = "regex-2021.7.6-cp36-cp36m-win32.whl", hash = "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b"}, - {file = "regex-2021.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb"}, - {file = "regex-2021.7.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad"}, - {file = "regex-2021.7.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895"}, - {file = "regex-2021.7.6-cp37-cp37m-win32.whl", hash = "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5"}, - {file = "regex-2021.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f"}, - {file = "regex-2021.7.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_i686.whl", hash = "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761"}, - {file = "regex-2021.7.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068"}, - {file = "regex-2021.7.6-cp38-cp38-win32.whl", hash = "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0"}, - {file = "regex-2021.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4"}, - {file = "regex-2021.7.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"}, - {file = "regex-2021.7.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3"}, - {file = "regex-2021.7.6-cp39-cp39-win32.whl", hash = "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035"}, - {file = "regex-2021.7.6-cp39-cp39-win_amd64.whl", hash = "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c"}, - {file = "regex-2021.7.6.tar.gz", hash = "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d"}, + {file = "regex-2021.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1"}, + {file = "regex-2021.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531"}, + {file = "regex-2021.8.3-cp36-cp36m-win32.whl", hash = "sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d"}, + {file = "regex-2021.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee"}, + {file = "regex-2021.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16"}, + {file = "regex-2021.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f"}, + {file = "regex-2021.8.3-cp37-cp37m-win32.whl", hash = "sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d"}, + {file = "regex-2021.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b"}, + {file = "regex-2021.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281"}, + {file = "regex-2021.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20"}, + {file = "regex-2021.8.3-cp38-cp38-win32.whl", hash = "sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a"}, + {file = "regex-2021.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6"}, + {file = "regex-2021.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b"}, + {file = "regex-2021.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b"}, + {file = "regex-2021.8.3-cp39-cp39-win32.whl", hash = "sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6"}, + {file = "regex-2021.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91"}, + {file = "regex-2021.8.3.tar.gz", hash = "sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a"}, ] requests = [ - {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, - {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] responses = [ {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, @@ -1393,8 +1436,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tox = [ - {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"}, - {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"}, + {file = "tox-3.24.1-py2.py3-none-any.whl", hash = "sha256:60eda26fa47b7130e6fc1145620b1fd897963af521093c3685c3f63d1c394029"}, + {file = "tox-3.24.1.tar.gz", hash = "sha256:9850daeb96d21b4abf049bc5f197426123039e383ebfed201764e9355fc5a880"}, ] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, @@ -1443,8 +1486,8 @@ urllib3 = [ {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, ] virtualenv = [ - {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, - {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, + {file = "virtualenv-20.7.0-py2.py3-none-any.whl", hash = "sha256:fdfdaaf0979ac03ae7f76d5224a05b58165f3c804f8aa633f3dd6f22fbd435d5"}, + {file = "virtualenv-20.7.0.tar.gz", hash = "sha256:97066a978431ec096d163e72771df5357c5c898ffdd587048f45e0aecc228094"}, ] watchdog = [ {file = "watchdog-2.1.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9628f3f85375a17614a2ab5eac7665f7f7be8b6b0a2a228e6f6a2e91dd4bfe26"}, diff --git a/pyproject.toml b/pyproject.toml index 1270093..fe3c149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ Sphinx = "^3.5.2" tox = "^3.23.0" coverage = "^5.5" isort = "^5.8.0" -responses = "^0.13.3" coveralls = "^3.1.0" pre-commit = "2.13.0" pytest-cache = "^1.0" diff --git a/shipengine_sdk/enums/__init__.py b/shipengine_sdk/enums/__init__.py index d81201e..6627690 100644 --- a/shipengine_sdk/enums/__init__.py +++ b/shipengine_sdk/enums/__init__.py @@ -33,6 +33,7 @@ class Endpoints(Enum): """A collection of RPC Methods used throughout the ShipEngine SDK.""" ADDRESSES_VALIDATE = "v1/addresses/validate" + GET_RATE_FROM_SHIPMENT = "v1/rates" LIST_CARRIERS = "v1/carriers" diff --git a/shipengine_sdk/errors/__init__.py b/shipengine_sdk/errors/__init__.py index 08d4809..b2ae22c 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine_sdk/errors/__init__.py @@ -15,7 +15,7 @@ def __init__( self, message: str, request_id: Optional[str] = None, - source: Optional[str] = None, + error_source: Optional[str] = None, error_type: Optional[str] = None, error_code: Optional[str] = None, url: Optional[str] = None, @@ -23,18 +23,18 @@ def __init__( """Base exception class that all other client errors will inherit from.""" self.message = message self.request_id = request_id - self.source = source + self.error_source = error_source self.error_code = error_code self.error_type = error_type self.url = url self._are_enums_valid() def _are_enums_valid(self): - if self.source is None: + if self.error_source is None: pass # noqa - elif not does_member_value_exist(self.source, ErrorSource): + elif not does_member_value_exist(self.error_source, ErrorSource): raise ValueError( - f"Error source must be a member of ErrorSource enum - [{self.source}] provided." + f"Error source must be a member of ErrorSource enum - [{self.error_source}] provided." ) if self.error_type is None: @@ -82,17 +82,17 @@ class ClientTimeoutError(ShipEngineError): def __init__( self, retry_after: int, - source: Optional[str] = None, + error_source: Optional[str] = None, request_id: Optional[str] = None, ) -> None: """An exception that indicates the configured timeout has been reached for a given request.""" self.retry_after = retry_after - self.source = source + self.error_source = error_source self.request_id = request_id super(ClientTimeoutError, self).__init__( message=f"The request took longer than the {retry_after} seconds allowed.", request_id=self.request_id, - source=self.source, + error_source=self.error_source, error_type=ErrorType.SYSTEM.value, error_code=ErrorCode.TIMEOUT.value, url="https://www.shipengine.com/docs/rate-limits", @@ -100,15 +100,15 @@ def __init__( class InvalidFieldValueError(ShipEngineError): - def __init__(self, field_name: str, reason: str, field_value, source: str = None) -> None: + def __init__(self, field_name: str, reason: str, field_value, error_source: str = None) -> None: """This error occurs when a field has been set to an invalid value.""" self.field_name = field_name self.field_value = field_value - self.source = source + self.error_source = error_source super(InvalidFieldValueError, self).__init__( request_id=None, message=f"{self.field_name} - {reason} {self.field_value} was provided.", - source=self.source, + error_source=self.error_source, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.INVALID_FIELD_VALUE.value, ) @@ -117,18 +117,18 @@ def __init__(self, field_name: str, reason: str, field_value, source: str = None class RateLimitExceededError(ShipEngineError): def __init__( self, - retry_after: int, - source: Optional[str] = None, + retry_after: Optional[int] = None, + error_source: Optional[str] = None, request_id: Optional[str] = None, ) -> None: """The amount of time (in SECONDS) to wait before retrying the request.""" self.retry_after = retry_after - self.source = source + self.error_source = error_source self.request_id = request_id super(RateLimitExceededError, self).__init__( message="You have exceeded the rate limit.", request_id=self.request_id, - source=self.source, + error_source=self.error_source, error_type=ErrorType.SYSTEM.value, error_code=ErrorCode.RATE_LIMIT_EXCEEDED.value, url="https://www.shipengine.com/docs/rate-limits", diff --git a/shipengine_sdk/http_client/client.py b/shipengine_sdk/http_client/client.py index efdf04f..b89bf17 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine_sdk/http_client/client.py @@ -4,6 +4,7 @@ import platform import time from typing import Any, Dict, Optional +from urllib.parse import urljoin import requests from requests import PreparedRequest, Request, RequestException, Response, Session @@ -121,7 +122,7 @@ def _send_request( req_headers = request_headers(user_agent=self._derive_user_agent(), api_key=config.api_key) req: Request = Request( method=http_method, - url=f"{base_uri}{endpoint}", + url=urljoin(base_uri, endpoint), data=json.dumps(body), headers=req_headers, auth=ShipEngineAuth(config.api_key), @@ -133,7 +134,7 @@ def _send_request( except RequestException as err: raise ShipEngineError( message=f"An unknown error occurred while calling the ShipEngine {http_method} API:\n {err.response}", - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.SYSTEM.value, error_code=ErrorCode.UNSPECIFIED.value, ) diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 4cceba9..73b2e5f 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -63,19 +63,21 @@ def create_label_from_shipment( return self.client.post(endpoint="v1/labels", params=params, config=config) def get_rates_from_shipment( - self, params: Dict[str, Any], config: Union[str, Dict[str, Any]] = None + self, shipment: Dict[str, Any], config: Union[str, Dict[str, Any]] = None ) -> Dict[str, Any]: """ Given some shipment details and rate options, this endpoint returns a list of rate quotes. See: https://shipengine.github.io/shipengine-openapi/#operation/calculate_rates - :param Dict[str, Any] params: A dictionary of shipment details for the label creation. + :param Dict[str, Any] shipment: A dictionary of shipment details for the label creation. :param Union[str, Dict[str, Any], ShipEngineConfig] config: Method level configuration to set new values for properties of the global ShipEngineConfig object. :returns Dict[str, Any]: A label that corresponds the to shipment details provided. """ config = self.config.merge(new_config=config) - return self.client.post(endpoint="v1/rates", params=params, config=config) + return self.client.post( + endpoint=Endpoints.GET_RATE_FROM_SHIPMENT.value, params=shipment, config=config + ) def list_carriers(self, config: Dict[str, Any] = None) -> Dict[str, Any]: """ diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index f8042de..d3d9e00 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -6,7 +6,6 @@ from ..errors import ( ClientSystemError, - ClientTimeoutError, InvalidFieldValueError, RateLimitExceededError, ShipEngineError, @@ -21,14 +20,14 @@ def is_street_valid(street: List[str]) -> None: if len(street) == 0: raise ValidationError( message="Invalid address. At least one address line is required.", - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) elif len(street) > 3: raise ValidationError( message="Invalid address. No more than 3 street lines are allowed.", - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.INVALID_FIELD_VALUE.value, ) @@ -44,7 +43,7 @@ def is_city_valid(city: str) -> None: elif not latin_pattern.match(city) or city == "": raise ValidationError( message=validation_message, - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -60,7 +59,7 @@ def is_state_valid(state: str) -> None: elif not latin_pattern.match(state) or state == "": raise ValidationError( message=validation_message, - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -73,7 +72,7 @@ def is_postal_code_valid(postal_code: str) -> None: if not pattern.match(postal_code) or postal_code == "": raise ValidationError( message=validation_message, - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -84,7 +83,7 @@ def is_country_code_valid(country: str) -> None: if country not in (member.value for member in Country): raise ValidationError( message=f"Invalid address: [{country}] is not a valid country code.", - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -102,7 +101,7 @@ def is_api_key_valid(config: Dict[str, Any]) -> None: if "api_key" not in config or config["api_key"] == "": raise ValidationError( message=message, - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -110,7 +109,7 @@ def is_api_key_valid(config: Dict[str, Any]) -> None: if re.match(r"\s", config["api_key"]): raise ValidationError( message=message, - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=ErrorType.VALIDATION.value, error_code=ErrorCode.FIELD_VALUE_REQUIRED.value, ) @@ -129,7 +128,7 @@ def is_retries_valid(config: Dict[str, Any]) -> None: field_name="retries", reason="Retries must be zero or greater.", field_value=config["retries"], - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, ) @@ -146,7 +145,7 @@ def is_timeout_valid(config: Dict[str, Any]) -> None: field_name="timeout", reason="Timeout must be zero or greater.", field_value=config["timeout"], - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, ) @@ -162,7 +161,7 @@ def api_key_validation_error_assertions(error) -> None: assert error.request_id is None assert error.error_type is ErrorType.VALIDATION.value assert error.error_code is ErrorCode.FIELD_VALUE_REQUIRED.value - assert error.source is ErrorSource.SHIPENGINE.value + assert error.error_source is ErrorSource.SHIPENGINE.value assert error.message == "A ShipEngine API key must be specified." @@ -172,18 +171,18 @@ def timeout_validation_error_assertions(error) -> None: assert error.request_id is None assert error.error_type is ErrorType.VALIDATION.value assert error.error_code is ErrorCode.INVALID_FIELD_VALUE.value - assert error.source is ErrorSource.SHIPENGINE.value + assert error.error_source is ErrorSource.SHIPENGINE.value def check_response_for_errors(status_code: int, response_body: Dict[str, Any], config) -> None: """Checks response and status_code for 400, 404, 429, and 500 error cases and raises an approved exception.""" - - error = response_body["errors"][0] + if status_code != 200: + error = response_body["errors"][0] if status_code == 400: raise ShipEngineError( message=error["message"], - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=error["error_type"], error_code=error["error_code"], ) @@ -191,93 +190,55 @@ def check_response_for_errors(status_code: int, response_body: Dict[str, Any], c if status_code == 404: raise ShipEngineError( message=error["message"], - source=ErrorSource.SHIPENGINE.value, + error_source=ErrorSource.SHIPENGINE.value, error_type=error["error_type"], error_code=error["error_code"], ) # Check if status_code is 429 and raises an error if so. - if "errors" in response_body and status_code == 429: - retry_after = error["details"]["retryAfter"] - if retry_after > config.timeout: - raise ClientTimeoutError( - retry_after=config.timeout, - source=ErrorSource.SHIPENGINE.value, - request_id=response_body["request_id"], - ) - else: - raise RateLimitExceededError( - retry_after=retry_after, - source=ErrorSource.SHIPENGINE.value, - request_id=response_body["request_id"], - ) + if status_code == 429: + # TODO: Need to access retry after in response headers and add back in the below code. + # retry_after = error["details"]["retryAfter"] + # if retry_after > config.timeout: + # raise ClientTimeoutError( + # retry_after=config.timeout, + # error_source=ErrorSource.SHIPENGINE.value, + # request_id=response_body["request_id"], + # ) + # else: + raise RateLimitExceededError( + # retry_after=retry_after, + error_source=ErrorSource.SHIPENGINE.value, + request_id=response_body["request_id"], + ) # Check if the status code is 500 and raises an error if so. if status_code == 500: raise ClientSystemError( message=error["message"], request_id=response_body["request_id"], - source=error["error_source"], + error_source=error["error_source"], error_type=error["error_type"], error_code=error["error_code"], ) -def does_normalized_address_have_errors(result) -> None: - """ - 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. - """ - if len(result.errors) > 1: - error_list = list() - for err in result.errors: - error_list.append(err["message"]) - - str_errors = "\n".join(error_list) - - raise ShipEngineError( - message=f"Invalid address.\n{str_errors}", - request_id=result.request_id, - source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.ERROR.value, - error_code=ErrorCode.INVALID_ADDRESS.value, - ) - elif len(result.errors) == 1: - raise ShipEngineError( - message=f"Invalid address. {result.errors[0]['message']}", - request_id=result.request_id, - source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.ERROR.value, - error_code=result.errors[0]["code"], - ) - elif result.is_valid is False: - raise ShipEngineError( - message="Invalid address - The address provided could not be normalized.", - request_id=result.request_id, - source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.ERROR.value, - error_code=ErrorCode.INVALID_ADDRESS.value, - ) - - -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.", - source=ErrorSource.SHIPENGINE.value, - error_type=ErrorType.VALIDATION.value, - error_code=ErrorCode.INVALID_IDENTIFIER.value, - ) +# 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.", +# error_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.", +# error_source=ErrorSource.SHIPENGINE.value, +# error_type=ErrorType.VALIDATION.value, +# error_code=ErrorCode.INVALID_IDENTIFIER.value, +# ) diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py new file mode 100644 index 0000000..d71c493 --- /dev/null +++ b/tests/errors/test_errors.py @@ -0,0 +1,169 @@ +"""Tests for the ShipEngine SDK Errors""" +import pytest + +from shipengine_sdk.errors import ( + AccountStatusError, + BusinessRuleError, + ClientSecurityError, + ClientTimeoutError, + InvalidFieldValueError, + RateLimitExceededError, + ShipEngineError, + ValidationError, +) + + +def shipengine_error_defaults() -> ShipEngineError: + """Return a ShipEngineError that only has a message string passed in.""" + raise ShipEngineError(message="Testing the defaults for this error.") + + +def shipengine_error(): + """Return a ShipEngineError that have all fields populated by valid values.""" + raise ShipEngineError( + request_id="req_a523b1b19bd54054b7eb953f000e7f15", + message="The is a test exception", + error_source="shipengine", + error_type="validation", + error_code="invalid_address", + url="https://google.com", + ) + + +def shipengine_error_with_no_error_type() -> ShipEngineError: + """Return a ShipEngineError that only has the error_type set to None.""" + raise ShipEngineError( + request_id="req_a523b1b19bd54054b7eb953f000e7f15", + message="The is a test exception", + error_source="shipengine", + error_type=None, + error_code="invalid_address", + ) + + +def shipengine_error_with_bad_error_type() -> ShipEngineError: + """Return a ShipEngineError that has an invalid error_type.""" + raise ShipEngineError( + request_id="req_a523b1b19bd54054b7eb953f000e7f15", + message="The is a test exception", + error_source="shipengine", + error_type="tracking", + error_code="invalid_address", + ) + + +def shipengine_error_with_bad_error_source() -> ShipEngineError: + """Return a ShipEngineError that has an invalid error_source.""" + raise ShipEngineError( + request_id="req_a523b1b19bd54054b7eb953f000e7f15", + message="The is a test exception", + error_source="wayne_enterprises", + error_type="validation", + error_code="invalid_address", + ) + + +def shipengine_error_with_bad_error_code() -> ShipEngineError: + """Return a ShipEngineError that has an invalid error_code.""" + raise ShipEngineError( + request_id="req_a523b1b19bd54054b7eb953f000e7f15", + message="The is a test exception", + error_source="shipengine", + error_type="validation", + error_code="failure", + ) + + +def account_status() -> AccountStatusError: + raise AccountStatusError("There was an issue with your ShipEngine account.") + + +def business_rule_error() -> BusinessRuleError: + raise BusinessRuleError("Invalid postal code.") + + +def security_error() -> ClientSecurityError: + raise ClientSecurityError("Unauthorized - you API key is invalid.") + + +def validation_error() -> ValidationError: + raise ValidationError("The value provided must be an integer - object provided.") + + +def client_timeout_error() -> ClientTimeoutError: + raise ClientTimeoutError(300, "shipengine", "req_a523b1b19bd54054b7eb953f000e7f15") + + +def invalid_filed_value_error() -> InvalidFieldValueError: + raise InvalidFieldValueError("is_residential", "Value should be int but got str.", 1) + + +def rate_limit_exceeded_error() -> RateLimitExceededError: + raise RateLimitExceededError(300, "shipengine", "req_a523b1b19bd54054b7eb953f000e7f15") + + +class TestShipEngineErrors: + def test_shipengine_error(self) -> None: + with pytest.raises(ShipEngineError): + shipengine_error() + + def test_shipengine_error_with_bad_error_type(self) -> None: + with pytest.raises(ValueError): + shipengine_error_with_bad_error_type() + + def test_shipengine_error_with_bad_error_source(self) -> None: + with pytest.raises(ValueError): + shipengine_error_with_bad_error_source() + + def test_shipengine_error_with_bad_error_code(self) -> None: + with pytest.raises(ValueError): + shipengine_error_with_bad_error_code() + + def test_account_status(self) -> None: + with pytest.raises(AccountStatusError): + account_status() + + def test_business_rule_error(self) -> None: + with pytest.raises(BusinessRuleError): + business_rule_error() + + def test_security_error(self) -> None: + with pytest.raises(ClientSecurityError): + security_error() + + def test_validation_error(self) -> None: + with pytest.raises(ValidationError): + validation_error() + + def test_timeout_error(self) -> None: + with pytest.raises(ClientTimeoutError): + client_timeout_error() + + def test_invalid_filed_value_error(self) -> None: + with pytest.raises(InvalidFieldValueError): + invalid_filed_value_error() + + def test_rate_limit_exceeded_error(self) -> None: + with pytest.raises(RateLimitExceededError): + rate_limit_exceeded_error() + + def test_error_defaults(self) -> None: + """Test the error class default values.""" + with pytest.raises(ShipEngineError): + shipengine_error_defaults() + + def test_to_dict_method(self) -> None: + """Test the to_dict convenience method.""" + try: + shipengine_error() + except ShipEngineError as err: + d = err.to_dict() + assert type(d) is dict + + def test_to_json_method(self) -> None: + """Test the to_json convenience method.""" + try: + shipengine_error() + except ShipEngineError as err: + j = err.to_json() + assert type(j) is str diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..bd4a435 --- /dev/null +++ b/tests/services/__init__.py @@ -0,0 +1 @@ +"""Tests for the core services in the ShipEngine SDK.""" diff --git a/tests/services/test_get_rate_from_shipment.py b/tests/services/test_get_rate_from_shipment.py new file mode 100644 index 0000000..609ddfc --- /dev/null +++ b/tests/services/test_get_rate_from_shipment.py @@ -0,0 +1,211 @@ +"""Testing the get_rate_fro_shipment functionality in the ShipEngine SDK.""" +import json +import unittest +import urllib.parse as urlparse + +import responses + +from shipengine_sdk.enums import BaseURL, Endpoints +from tests.util import stub_shipengine_instance + + +class TestGetRateFromShipment(unittest.TestCase): + @responses.activate + def test_get_rate_from_shipment(self) -> None: + """Test get_rate_from_shipment functionality.""" + responses.add( + **{ + "method": responses.POST, + "url": urlparse.urljoin( + BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.GET_RATE_FROM_SHIPMENT.value + ), + "body": json.dumps( + { + "shipmentId": "se-141694059", + "carrierId": "se-161650", + "serviceCode": "usps_first_class_mail", + "externalOrderId": None, + "items": [], + "taxIdentifiers": None, + "externalShipmentId": None, + "shipDate": "2021-07-28T00:00:00Z", + "createdAt": "2021-07-28T16:56:40.257Z", + "modifiedAt": "2021-07-28T16:56:40.223Z", + "shipmentStatus": "pending", + "shipTo": { + "name": "James Atkinson", + "phone": None, + "companyName": None, + "addressLine1": "28793 Fox Fire Lane", + "addressLine2": None, + "addressLine3": None, + "cityLocality": "Shell Knob", + "stateProvince": "MO", + "postalCode": "65747", + "countryCode": "US", + "addressResidentialIndicator": "yes", + }, + "shipFrom": { + "name": "Medals of America", + "phone": "800-308-0849", + "companyName": None, + "addressLine1": "114 Southchase Blvd", + "addressLine2": None, + "addressLine3": None, + "cityLocality": "Fountain Inn", + "stateProvince": "SC", + "postalCode": "29644", + "countryCode": "US", + "addressResidentialIndicator": "unknown", + }, + "warehouseId": None, + "returnTo": { + "name": "Medals of America", + "phone": "800-308-0849", + "companyName": None, + "addressLine1": "114 Southchase Blvd", + "addressLine2": None, + "addressLine3": None, + "cityLocality": "Fountain Inn", + "stateProvince": "SC", + "postalCode": "29644", + "countryCode": "US", + "addressResidentialIndicator": "unknown", + }, + "confirmation": "none", + "customs": { + "contents": "merchandise", + "nonDelivery": "return_to_sender", + "customsItems": [], + }, + "advancedOptions": { + "billToAccount": None, + "billToCountryCode": None, + "billToParty": None, + "billToPostalCode": None, + "containsAlcohol": None, + "deliveryDutyPaid": None, + "dryIce": None, + "dryIceWeight": None, + "nonMachinable": None, + "saturdayDelivery": None, + "useUPSGroundFreightPricing": None, + "freightClass": None, + "customField1": None, + "customField2": None, + "customField3": None, + "originType": None, + "shipperRelease": None, + "collectOnDelivery": None, + }, + "originType": None, + "insuranceProvider": "none", + "tags": [], + "orderSourceCode": None, + "packages": [ + { + "packageCode": "package", + "weight": {"value": 2.9, "unit": "ounce"}, + "dimensions": { + "unit": "inch", + "length": 0, + "width": 0, + "height": 0, + }, + "insuredValue": {"currency": "usd", "amount": 0}, + "trackingNumber": None, + "labelMessages": { + "reference1": "4051492", + "reference2": None, + "reference3": None, + }, + "externalPackageId": None, + } + ], + "totalWeight": {"value": 2.9, "unit": "ounce"}, + "rateResponse": { + "rates": [ + { + "rateId": "se-784001113", + "rateType": "shipment", + "carrierId": "se-161650", + "shippingAmount": {"currency": "usd", "amount": 3.12}, + "insuranceAmount": {"currency": "usd", "amount": 0}, + "confirmationAmount": {"currency": "usd", "amount": 0}, + "otherAmount": {"currency": "usd", "amount": 0}, + "taxAmount": None, + "zone": 5, + "packageType": "package", + "deliveryDays": 3, + "guaranteedService": False, + "estimatedDeliveryDate": "2021-07-31T00:00:00Z", + "carrierDeliveryDays": "3", + "shipDate": "2021-07-28T00:00:00Z", + "negotiatedRate": False, + "serviceType": "USPS First Class Mail", + "serviceCode": "usps_first_class_mail", + "trackable": True, + "carrierCode": "usps", + "carrierNickname": "USPS", + "carrierFriendlyName": "USPS", + "validationStatus": "valid", + "warningMessages": [], + "errorMessages": [], + } + ], + "invalidRates": [], + "rateRequestId": "se-85117731", + "shipmentId": "se-141694059", + "createdAt": "2021-07-28T16:56:40.6148892Z", + "status": "completed", + "errors": [], + }, + } + ), + "status": 200, + "content_type": "application/json", + } + ) + + shipengine = stub_shipengine_instance() + result = shipengine.get_rates_from_shipment( + shipment={ + "rate_options": { + "carrier_ids": ["se-161650"], + "service_codes": ["usps_first_class_mail"], + "package_types": ["package"], + }, + "shipment": { + "service_code": "", + "ship_to": { + "name": "James Atkinson", + "phone": None, + "address_line1": "28793 Fox Fire Lane", + "city_locality": "Shell Knob", + "state_province": "MO", + "postal_code": "65747", + "country_code": "US", + "address_residential_indicator": "yes", + }, + "ship_from": { + "name": "Medals of America", + "phone": "800-308-0849", + "company_name": None, + "address_line1": "114 Southchase Blvd", + "address_line2": "", + "city_locality": "Fountain Inn", + "state_province": "SC", + "postal_code": "29644", + "country_code": "US", + "address_residential_indicator": "no", + }, + "packages": [ + { + "weight": {"value": 2.9, "unit": "ounce"}, + "label_messages": {"reference1": "4051492"}, + } + ], + }, + } + ) + self.assertEqual(result["rateResponse"]["rates"][0]["rateId"], "se-784001113") diff --git a/tests/services/test_list_carriers.py b/tests/services/test_list_carriers.py new file mode 100644 index 0000000..d015553 --- /dev/null +++ b/tests/services/test_list_carriers.py @@ -0,0 +1,857 @@ +"""Testing the list_carriers functionality in the ShipEngine SDK.""" +import json +import unittest +import urllib.parse as urlparse + +import responses + +from shipengine_sdk.enums import BaseURL, Endpoints +from tests.util import stub_shipengine_instance + + +class TestListCarriers(unittest.TestCase): + def test_something(self): + self.assertEqual(False, False) + + @responses.activate + def test_list_carriers(self) -> None: + """ + Tests that the list_carriers method properly interacts with the ShipEngine API. It ensures + that this method returns a successful response from ShipEngine API containing all carriers + associated with a given ShipEngine account. + """ + responses.add( + **{ + "method": responses.GET, + "url": urlparse.urljoin( + BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.LIST_CARRIERS.value + ), + "body": json.dumps( + { + "carriers": [ + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "account_number": "test_account_656171", + "requires_funded_amount": True, + "balance": 8452.04, + "nickname": "ShipEngine Test Account - Stamps.com", + "friendly_name": "Stamps.com", + "primary": False, + "has_multi_package_supporting_services": False, + "supports_label_messages": True, + "services": [ + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_first_class_mail", + "name": "USPS First Class Mail", + "domestic": True, + "international": False, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_media_mail", + "name": "USPS Media Mail", + "domestic": True, + "international": False, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_parcel_select", + "name": "USPS Parcel Select Ground", + "domestic": True, + "international": False, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_priority_mail", + "name": "USPS Priority Mail", + "domestic": True, + "international": False, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_priority_mail_express", + "name": "USPS Priority Mail Express", + "domestic": True, + "international": False, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_first_class_mail_international", + "name": "USPS First Class Mail Intl", + "domestic": False, + "international": True, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_priority_mail_international", + "name": "USPS Priority Mail Intl", + "domestic": False, + "international": True, + "is_multi_package_supported": False, + }, + { + "carrier_id": "se-656171", + "carrier_code": "stamps_com", + "service_code": "usps_priority_mail_express_international", + "name": "USPS Priority Mail Express Intl", + "domestic": False, + "international": True, + "is_multi_package_supported": False, + }, + ], + "packages": [ + { + "package_id": "None", + "package_code": "cubic", + "name": "Cubic", + "description": "Cubic", + }, + { + "package_id": "None", + "package_code": "flat_rate_envelope", + "name": "Flat Rate Envelope", + "description": 'USPS flat rate envelope. A special cardboard envelope provided by the USPS that clearly indicates "Flat Rate".', # noqa + }, + { + "package_id": "None", + "package_code": "flat_rate_legal_envelope", + "name": "Flat Rate Legal Envelope", + "description": "Flat Rate Legal Envelope", + }, + { + "package_id": "None", + "package_code": "flat_rate_padded_envelope", + "name": "Flat Rate Padded Envelope", + "description": "Flat Rate Padded Envelope", + }, + { + "package_id": "None", + "package_code": "large_envelope_or_flat", + "name": "Large Envelope or Flat", + "description": 'Large envelope or flat. Has one dimension that is between 11 1/2" and 15" long, 6 1/18" and 12" high, or 1/4" and 3/4" thick.', # noqa + }, + { + "package_id": "None", + "package_code": "large_flat_rate_box", + "name": "Large Flat Rate Box", + "description": "Large Flat Rate Box", + }, + { + "package_id": "None", + "package_code": "large_package", + "name": 'Large Package (any side \u003e 12")', + "description": 'Large package. Longest side plus the distance around the thickest part is over 84" and less than or equal to 108".', # noqa + }, + { + "package_id": "None", + "package_code": "letter", + "name": "Letter", + "description": "Letter", + }, + { + "package_id": "None", + "package_code": "medium_flat_rate_box", + "name": "Medium Flat Rate Box", + "description": 'USPS flat rate box. A special 11" x 8 1/2" x 5 1/2" or 14" x 3.5" x 12" USPS box that clearly indicates "Flat Rate Box"', # noqa + }, + { + "package_id": "None", + "package_code": "non_rectangular", + "name": "Non Rectangular Package", + "description": "Non-Rectangular package type that is cylindrical in shape.", + }, + { + "package_id": "None", + "package_code": "package", + "name": "Package", + "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa + }, + { + "package_id": "None", + "package_code": "regional_rate_box_a", + "name": "Regional Rate Box A", + "description": "Regional Rate Box A", + }, + { + "package_id": "None", + "package_code": "regional_rate_box_b", + "name": "Regional Rate Box B", + "description": "Regional Rate Box B", + }, + { + "package_id": "None", + "package_code": "small_flat_rate_box", + "name": "Small Flat Rate Box", + "description": "Small Flat Rate Box", + }, + { + "package_id": "None", + "package_code": "thick_envelope", + "name": "Thick Envelope", + "description": 'Thick envelope. Envelopes or flats greater than 3/4" at the thickest point.', # noqa + }, + ], + "options": [ + { + "name": "non_machinable", + "default_value": "False", + "description": "", + }, + { + "name": "bill_to_account", + "default_value": "None", + "description": "Bill To Account", + }, + { + "name": "bill_to_party", + "default_value": "None", + "description": "Bill To Party", + }, + { + "name": "bill_to_postal_code", + "default_value": "None", + "description": "Bill To Postal Code", + }, + { + "name": "bill_to_country_code", + "default_value": "None", + "description": "Bill To Country Code", + }, + ], + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "account_number": "test_account_656172", + "requires_funded_amount": False, + "balance": 0.0, + "nickname": "ShipEngine Test Account - UPS", + "friendly_name": "UPS", + "primary": False, + "has_multi_package_supporting_services": True, + "supports_label_messages": True, + "services": [ + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_standard_international", + "name": "UPS Standard®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_next_day_air_early_am", + "name": "UPS Next Day Air® Early", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_worldwide_express", + "name": "UPS Worldwide Express®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_next_day_air", + "name": "UPS Next Day Air®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_ground_international", + "name": "UPS Ground® (International)", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_worldwide_express_plus", + "name": "UPS Worldwide Express Plus®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_next_day_air_saver", + "name": "UPS Next Day Air Saver®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_worldwide_expedited", + "name": "UPS Worldwide Expedited®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_2nd_day_air_am", + "name": "UPS 2nd Day Air AM®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_2nd_day_air", + "name": "UPS 2nd Day Air®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_worldwide_saver", + "name": "UPS Worldwide Saver®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_2nd_day_air_international", + "name": "UPS 2nd Day Air® (International)", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_3_day_select", + "name": "UPS 3 Day Select®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_ground", + "name": "UPS® Ground", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656172", + "carrier_code": "ups", + "service_code": "ups_next_day_air_international", + "name": "UPS Next Day Air® (International)", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + ], + "packages": [ + { + "package_id": "None", + "package_code": "package", + "name": "Package", + "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa + }, + { + "package_id": "None", + "package_code": "ups__express_box_large", + "name": "UPS Express® Box - Large", + "description": "Express Box - Large", + }, + { + "package_id": "None", + "package_code": "ups_10_kg_box", + "name": "UPS 10 KG Box®", + "description": "10 KG Box", + }, + { + "package_id": "None", + "package_code": "ups_25_kg_box", + "name": "UPS 25 KG Box®", + "description": "25 KG Box", + }, + { + "package_id": "None", + "package_code": "ups_express_box", + "name": "UPS Express® Box", + "description": "Express Box", + }, + { + "package_id": "None", + "package_code": "ups_express_box_medium", + "name": "UPS Express® Box - Medium", + "description": "Express Box - Medium", + }, + { + "package_id": "None", + "package_code": "ups_express_box_small", + "name": "UPS Express® Box - Small", + "description": "Express Box - Small", + }, + { + "package_id": "None", + "package_code": "ups_express_pak", + "name": "UPS Express® Pak", + "description": "Pak", + }, + { + "package_id": "None", + "package_code": "ups_letter", + "name": "UPS Letter", + "description": "Letter", + }, + { + "package_id": "None", + "package_code": "ups_tube", + "name": "UPS Tube", + "description": "Tube", + }, + ], + "options": [ + { + "name": "bill_to_account", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_country_code", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_party", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_postal_code", + "default_value": "", + "description": "", + }, + { + "name": "collect_on_delivery", + "default_value": "", + "description": "", + }, + { + "name": "contains_alcohol", + "default_value": "False", + "description": "", + }, + { + "name": "delivered_duty_paid", + "default_value": "False", + "description": "", + }, + { + "name": "dry_ice", + "default_value": "False", + "description": "", + }, + { + "name": "dry_ice_weight", + "default_value": "0", + "description": "", + }, + { + "name": "freight_class", + "default_value": "", + "description": "", + }, + { + "name": "non_machinable", + "default_value": "False", + "description": "", + }, + { + "name": "saturday_delivery", + "default_value": "False", + "description": "", + }, + { + "name": "shipper_release", + "default_value": "False", + "description": "Driver may release package without signature", + }, + ], + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "account_number": "test_account_656173", + "requires_funded_amount": False, + "balance": 0.0, + "nickname": "ShipEngine Test Account - FedEx", + "friendly_name": "FedEx", + "primary": False, + "has_multi_package_supporting_services": True, + "supports_label_messages": True, + "services": [ + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_ground", + "name": "FedEx Ground®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_home_delivery", + "name": "FedEx Home Delivery®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_2day", + "name": "FedEx 2Day®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_2day_am", + "name": "FedEx 2Day® A.M.", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_express_saver", + "name": "FedEx Express Saver®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_standard_overnight", + "name": "FedEx Standard Overnight®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_priority_overnight", + "name": "FedEx Priority Overnight®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_first_overnight", + "name": "FedEx First Overnight®", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_1_day_freight", + "name": "FedEx 1Day® Freight", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_2_day_freight", + "name": "FedEx 2Day® Freight", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_3_day_freight", + "name": "FedEx 3Day® Freight", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_first_overnight_freight", + "name": "FedEx First Overnight® Freight", + "domestic": True, + "international": False, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_ground_international", + "name": "FedEx International Ground®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_economy", + "name": "FedEx International Economy®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_priority", + "name": "FedEx International Priority®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_first", + "name": "FedEx International First®", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_economy_freight", + "name": "FedEx International Economy® Freight", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_priority_freight", + "name": "FedEx International Priority® Freight", + "domestic": False, + "international": True, + "is_multi_package_supported": True, + }, + { + "carrier_id": "se-656173", + "carrier_code": "fedex", + "service_code": "fedex_international_connect_plus", + "name": "FedEx International Connect Plus®", + "domestic": False, + "international": True, + "is_multi_package_supported": False, + }, + ], + "packages": [ + { + "package_id": "None", + "package_code": "fedex_envelope_onerate", + "name": "FedEx One Rate® Envelope", + "description": "FedEx® Envelope", + }, + { + "package_id": "None", + "package_code": "fedex_extra_large_box_onerate", + "name": "FedEx One Rate® Extra Large Box", + "description": "FedEx® Extra Large Box", + }, + { + "package_id": "None", + "package_code": "fedex_large_box_onerate", + "name": "FedEx One Rate® Large Box", + "description": "FedEx® Large Box", + }, + { + "package_id": "None", + "package_code": "fedex_medium_box_onerate", + "name": "FedEx One Rate® Medium Box", + "description": "FedEx® Medium Box", + }, + { + "package_id": "None", + "package_code": "fedex_pak_onerate", + "name": "FedEx One Rate® Pak", + "description": "FedEx® Pak", + }, + { + "package_id": "None", + "package_code": "fedex_small_box_onerate", + "name": "FedEx One Rate® Small Box", + "description": "FedEx® Small Box", + }, + { + "package_id": "None", + "package_code": "fedex_tube_onerate", + "name": "FedEx One Rate® Tube", + "description": "FedEx® Tube", + }, + { + "package_id": "None", + "package_code": "fedex_10kg_box", + "name": "FedEx® 10kg Box", + "description": "FedEx® 10kg Box", + }, + { + "package_id": "None", + "package_code": "fedex_25kg_box", + "name": "FedEx® 25kg Box", + "description": "FedEx® 25kg Box", + }, + { + "package_id": "None", + "package_code": "fedex_box", + "name": "FedEx® Box", + "description": "FedEx® Box", + }, + { + "package_id": "None", + "package_code": "fedex_envelope", + "name": "FedEx® Envelope", + "description": "FedEx® Envelope", + }, + { + "package_id": "None", + "package_code": "fedex_pak", + "name": "FedEx® Pak", + "description": "FedEx® Pak", + }, + { + "package_id": "None", + "package_code": "fedex_tube", + "name": "FedEx® Tube", + "description": "FedEx® Tube", + }, + { + "package_id": "None", + "package_code": "package", + "name": "Package", + "description": 'Package. Longest side plus the distance around the thickest part is less than or equal to 84"', # noqa + }, + ], + "options": [ + { + "name": "bill_to_account", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_country_code", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_party", + "default_value": "", + "description": "", + }, + { + "name": "bill_to_postal_code", + "default_value": "", + "description": "", + }, + { + "name": "collect_on_delivery", + "default_value": "", + "description": "", + }, + { + "name": "contains_alcohol", + "default_value": "False", + "description": "", + }, + { + "name": "delivered_duty_paid", + "default_value": "False", + "description": "", + }, + { + "name": "dry_ice", + "default_value": "False", + "description": "", + }, + { + "name": "dry_ice_weight", + "default_value": "0", + "description": "", + }, + { + "name": "non_machinable", + "default_value": "False", + "description": "", + }, + { + "name": "saturday_delivery", + "default_value": "False", + "description": "", + }, + ], + }, + ], + "request_id": "6420e68b-b724-4d55-8180-a1828a3ca054", + "errors": [], + } + ), + "status": 200, + "content_type": "application/json", + } + ) + + shipengine = stub_shipengine_instance() + result = shipengine.list_carriers() + self.assertEqual(type(result["carriers"]), list) + self.assertGreater(len(result["carriers"]), 0) + self.assertEqual(len(result["carriers"]), 3) diff --git a/tests/services/test_validate_addresses.py b/tests/services/test_validate_addresses.py new file mode 100644 index 0000000..4c90b43 --- /dev/null +++ b/tests/services/test_validate_addresses.py @@ -0,0 +1,67 @@ +"""Testing the validate_addresses functionality in the ShipEngine SDK.""" +import json +import unittest +import urllib.parse as urlparse + +import responses + +from shipengine_sdk.enums import BaseURL, Endpoints + +from ..util import stub_shipengine_instance, valid_commercial_address + + +class TestValidateAddresses(unittest.TestCase): + @responses.activate + def test_validate_addresses(self) -> None: + """ + Tests that the validate_addresses method properly interacts with ShipEngine API, + and returns a successful response from ShipEngine API. + """ + responses.add( + **{ + "method": responses.POST, + "url": urlparse.urljoin( + BaseURL.SHIPENGINE_RPC_URL.value, Endpoints.ADDRESSES_VALIDATE.value + ), + "body": json.dumps( + [ + { + "status": "verified", + "original_address": { + "name": "ShipEngine", + "phone": "1-123-123-1234", + "company_name": "None", + "address_line1": "3800 N Lamar Blvd", + "address_line2": "ste 220", + "address_line3": "None", + "city_locality": "Austin", + "state_province": "TX", + "postal_code": "78756", + "country_code": "US", + "address_residential_indicator": "unknown", + }, + "matched_address": { + "name": "SHIPENGINE", + "phone": "1-123-123-1234", + "company_name": "None", + "address_line1": "3800 N LAMAR BLVD STE 220", + "address_line2": "", + "address_line3": "None", + "city_locality": "AUSTIN", + "state_province": "TX", + "postal_code": "78756-0003", + "country_code": "US", + "address_residential_indicator": "no", + }, + "messages": [], + } + ] + ), + "status": 200, + "content_type": "application/json", + } + ) + + shipengine = stub_shipengine_instance() + result = shipengine.validate_addresses(valid_commercial_address()) + self.assertEqual(result[0]["status"], "verified") diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index 25eb1e5..86d78a9 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -1,6 +1,17 @@ """Testing the ShipEngine object.""" +import pytest -from shipengine_sdk import __version__ +from shipengine_sdk import ShipEngine, __version__ +from shipengine_sdk.errors import ValidationError +from shipengine_sdk.util import api_key_validation_error_assertions + + +def shipengine_empty_api_key(): + return ShipEngine("") + + +def shipengine_no_api_key(): + return ShipEngine({"retries": 3}) class TestShipEngine: @@ -8,20 +19,20 @@ def test_version(self) -> None: """Test the package version of the ShipEngine SDK.""" assert __version__ == "0.0.1" - # def test_no_api_key_provided(self) -> None: - # """DX-1440 - No API Key at instantiation.""" - # try: - # shipengine_no_api_key() - # except Exception as e: - # api_key_validation_error_assertions(e) - # with pytest.raises(ValidationError): - # shipengine_no_api_key() - # - # def test_empty_api_key_provided(self) -> None: - # """DX-1441 - Empty API Key at instantiation.""" - # try: - # shipengine_empty_api_key() - # except Exception as e: - # api_key_validation_error_assertions(e) - # with pytest.raises(ValidationError): - # shipengine_empty_api_key() + def test_no_api_key_provided(self) -> None: + """DX-1440 - No API Key at instantiation.""" + try: + shipengine_no_api_key() + except Exception as e: + api_key_validation_error_assertions(e) + with pytest.raises(ValidationError): + shipengine_no_api_key() + + def test_empty_api_key_provided(self) -> None: + """DX-1441 - Empty API Key at instantiation.""" + try: + shipengine_empty_api_key() + except Exception as e: + api_key_validation_error_assertions(e) + with pytest.raises(ValidationError): + shipengine_empty_api_key() diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 363ef21..2ddbe05 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -1,8 +1,8 @@ """Testing the ShipEngineConfig object.""" import pytest -from shipengine_sdk import ShipEngineConfig -from shipengine_sdk.enums import BaseURL +from shipengine_sdk import ShipEngine, ShipEngineConfig +from shipengine_sdk.enums import BaseURL, Constants from shipengine_sdk.errors import InvalidFieldValueError, ValidationError from shipengine_sdk.util import api_key_validation_error_assertions from shipengine_sdk.util.sdk_assertions import timeout_validation_error_assertions @@ -66,7 +66,7 @@ def complete_valid_config() -> ShipEngineConfig: """ return ShipEngineConfig( dict( - api_key="baz_sim", + api_key=Constants.STUB_API_KEY.value, page_size=50, retries=2, timeout=10, @@ -74,6 +74,23 @@ def complete_valid_config() -> ShipEngineConfig: ) +def valid_commercial_address(): + return [ + { + "name": "ShipEngine", + "company": "Auctane", + "phone": "1-123-123-1234", + "address_line1": "3800 N Lamar Blvd", + "address_line2": "ste 220", + "city_locality": "Austin", + "state_province": "TX", + "postal_code": "78756", + "country_code": "US", + "address_residential_indicator": "unknown", + } + ] + + class TestShipEngineConfig: def test_valid_custom_config(self): """ @@ -81,7 +98,7 @@ def test_valid_custom_config(self): valid values for each attribute. """ valid_config: ShipEngineConfig = complete_valid_config() - assert valid_config.api_key == "baz_sim" + assert valid_config.api_key.startswith("TEST_") assert valid_config.base_uri is BaseURL.SHIPENGINE_RPC_URL.value assert valid_config.page_size == 50 assert valid_config.retries == 2 @@ -105,14 +122,14 @@ def test_empty_api_key_provided(self) -> None: with pytest.raises(ValidationError): config_with_empty_api_key() - def test_valid_retries(self): + def test_valid_retries(self) -> None: """Test case where a valid value is passed in for the retries.""" retries = 2 valid_retries = set_config_retries(retries) assert valid_retries.api_key == "baz_sim" assert valid_retries.retries == retries - def test_invalid_retries_provided(self): + def test_invalid_retries_provided(self) -> None: """DX-1442 - Invalid retries at instantiation.""" retries = -3 try: @@ -125,7 +142,7 @@ def test_invalid_retries_provided(self): with pytest.raises(InvalidFieldValueError): set_config_retries(retries) - def test_invalid_timeout_provided(self): + def test_invalid_timeout_provided(self) -> None: """DX-1443 - Invalid timeout at instantiation.""" timeout = -5 try: @@ -136,44 +153,44 @@ def test_invalid_timeout_provided(self): e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." ) - # def test_invalid_timeout_in_method_call(self): - # """DX-1447 - Invalid timeout in method call configuration.""" - # timeout = -5 - # try: - # shipengine = ShipEngine(stub_config()) - # shipengine.validate_address( - # address=valid_residential_address(), config=dict(timeout=timeout) - # ) - # except InvalidFieldValueError as e: - # timeout_validation_error_assertions(e) - # assert ( - # e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." - # ) - - # def test_invalid_retries_in_method_call(self): - # """DX-1446 - Invalid retries in method call configuration.""" - # retries = -5 - # try: - # shipengine = ShipEngine(stub_config()) - # shipengine.validate_address( - # address=valid_residential_address(), config=dict(retries=retries) - # ) - # except InvalidFieldValueError as e: - # timeout_validation_error_assertions(e) - # assert ( - # e.message == f"retries - Retries must be zero or greater. {retries} was provided." - # ) - - # def test_invalid_api_key_in_method_call(self): - # """DX-1445 - Invalid api_key in method call configuration.""" - # api_key = " " - # try: - # shipengine = ShipEngine(stub_config()) - # shipengine.validate_address( - # address=valid_residential_address(), config=dict(api_key=api_key) - # ) - # except Exception as e: - # api_key_validation_error_assertions(e) + def test_invalid_timeout_in_method_call(self) -> None: + """DX-1447 - Invalid timeout in method call configuration.""" + timeout = -5 + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_addresses( + address=valid_commercial_address(), config=dict(timeout=timeout) + ) + except InvalidFieldValueError as e: + timeout_validation_error_assertions(e) + assert ( + e.message == f"timeout - Timeout must be zero or greater. {timeout} was provided." + ) + + def test_invalid_retries_in_method_call(self) -> None: + """DX-1446 - Invalid retries in method call configuration.""" + retries = -5 + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_addresses( + address=valid_commercial_address(), config=dict(retries=retries) + ) + except InvalidFieldValueError as e: + timeout_validation_error_assertions(e) + assert ( + e.message == f"retries - Retries must be zero or greater. {retries} was provided." + ) + + def test_invalid_api_key_in_method_call(self) -> None: + """DX-1445 - Invalid api_key in method call configuration.""" + api_key = " " + try: + shipengine = ShipEngine(stub_config()) + shipengine.validate_addresses( + address=valid_commercial_address(), config=dict(api_key=api_key) + ) + except Exception as e: + api_key_validation_error_assertions(e) def test_config_defaults(self) -> None: """Test default retries.""" diff --git a/tests/test_snake_case_converter.py b/tests/test_snake_case_converter.py index b2f5f60..7c97eea 100644 --- a/tests/test_snake_case_converter.py +++ b/tests/test_snake_case_converter.py @@ -1,8 +1,9 @@ -"""Initial Docstring""" +"""Test test conversion helper function. snake_case -> camelCase.""" from shipengine_sdk.util import snake_to_camel class TestSnakeToCamelCase: def test_snake_to_camel(self): + """Test conversion of snake_case to camelCase.""" camel_case = snake_to_camel("python_is_awesome") assert camel_case == "pythonIsAwesome" diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 0000000..c8fef9e --- /dev/null +++ b/tests/util/__init__.py @@ -0,0 +1,2 @@ +"""Test helper functions and test data as functions used throughout the test suite.""" +from .test_helpers import * # noqa diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py new file mode 100644 index 0000000..3d9168c --- /dev/null +++ b/tests/util/test_helpers.py @@ -0,0 +1,52 @@ +"""Test data as functions and common assertion helper functions.""" +from typing import Any, Dict, List + +from shipengine_sdk import ShipEngine, ShipEngineConfig +from shipengine_sdk.enums import Constants + + +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=Constants.STUB_API_KEY.value, + page_size=50, + retries=retries, + timeout=15, + ) + + +def stub_shipengine_config() -> ShipEngineConfig: + """Return a valid test ShipEngineConfig object.""" + return ShipEngineConfig(config=stub_config()) + + +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(config=stub_config()) + + +def valid_commercial_address() -> List[Dict[str, Any]]: + return [ + { + "name": "ShipEngine", + "company": "Auctane", + "phone": "1-123-123-1234", + "address_line1": "3800 N Lamar Blvd", + "address_line2": "ste 220", + "city_locality": "Austin", + "state_province": "TX", + "postal_code": "78756", + "country_code": "US", + "address_residential_indicator": "unknown", + } + ] diff --git a/tox.ini b/tox.ini index f69bfac..c056edf 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,6 @@ deps = flake8 coverage coveralls - responses fuuid commands = pytest {posargs:} From 2b3cb8e314728184934daa55d63ec13d54569de3 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 14:38:59 -0500 Subject: [PATCH 06/13] fixed bug around responses for mocking --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index c056edf..f69bfac 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,7 @@ deps = flake8 coverage coveralls + responses fuuid commands = pytest {posargs:} From 35881aa1c0f6129c36b76dafb3fdfea45f454266 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 16:50:10 -0500 Subject: [PATCH 07/13] minor refactor and update CD --- .github/workflows/CD.yml | 45 +++++++++++++ .github/workflows/{main.yml => CI.yml} | 3 +- README.md | 6 +- docs/address_validation_example.md | 10 +-- docs/normalize_address_example.md | 6 +- docs/track_package_example.md | 5 +- poetry.lock | 67 ++++++++++++------- pyproject.toml | 16 ++++- pytest.ini | 2 +- {shipengine_sdk => shipengine}/__init__.py | 2 +- .../enums/__init__.py | 0 .../enums/country.py | 0 .../enums/error_code.py | 0 .../enums/error_source.py | 0 .../enums/error_type.py | 0 .../enums/regex_patterns.py | 0 .../errors/__init__.py | 7 +- .../http_client/__init__.py | 0 .../http_client/client.py | 2 +- {shipengine_sdk => shipengine}/shipengine.py | 2 +- .../shipengine_config.py | 0 .../util/__init__.py | 0 .../util/sdk_assertions.py | 2 +- tests/errors/test_errors.py | 2 +- tests/services/test_get_rate_from_shipment.py | 2 +- tests/services/test_list_carriers.py | 2 +- tests/services/test_validate_addresses.py | 2 +- tests/test_shipengine.py | 10 +-- tests/test_shipengine_config.py | 10 +-- tests/test_snake_case_converter.py | 2 +- tests/util/test_helpers.py | 4 +- 31 files changed, 139 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/CD.yml rename .github/workflows/{main.yml => CI.yml} (99%) rename {shipengine_sdk => shipengine}/__init__.py (91%) rename {shipengine_sdk => shipengine}/enums/__init__.py (100%) rename {shipengine_sdk => shipengine}/enums/country.py (100%) rename {shipengine_sdk => shipengine}/enums/error_code.py (100%) rename {shipengine_sdk => shipengine}/enums/error_source.py (100%) rename {shipengine_sdk => shipengine}/enums/error_type.py (100%) rename {shipengine_sdk => shipengine}/enums/regex_patterns.py (100%) rename {shipengine_sdk => shipengine}/errors/__init__.py (97%) rename {shipengine_sdk => shipengine}/http_client/__init__.py (100%) rename {shipengine_sdk => shipengine}/http_client/client.py (99%) rename {shipengine_sdk => shipengine}/shipengine.py (99%) rename {shipengine_sdk => shipengine}/shipengine_config.py (100%) rename {shipengine_sdk => shipengine}/util/__init__.py (100%) rename {shipengine_sdk => shipengine}/util/sdk_assertions.py (99%) diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml new file mode 100644 index 0000000..50c07d8 --- /dev/null +++ b/.github/workflows/CD.yml @@ -0,0 +1,45 @@ +on: + push: + branches: + - main + +name: ShipEngine Python CD +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/release-please-action@v2 + id: release + with: + token: ${{ secrets.GITHUB_TOKEN }} + release-type: python + package-name: shipengine + + # Checkout code if release was created + - uses: actions/checkout@v2 + if: ${{ setps.release.outputs.release_created }} + + # Setup Python if release was created + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + if: ${{ steps.release.outputs.release_created }} + + - name: Install dependancies + run: | + python -m pip install --upgrade pip + python -m pip install poetry + if: ${{ steps.release.outputs.release_created }} + + - name: Build the code + run: | + poetry build + if: ${{ steps.release.outputs.release_created }} + + - name: Publish package + run: | + poetry publish + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + if: ${{ steps.release.outputs.release_created }} diff --git a/.github/workflows/main.yml b/.github/workflows/CI.yml similarity index 99% rename from .github/workflows/main.yml rename to .github/workflows/CI.yml index 504eafe..c7f39be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/CI.yml @@ -1,8 +1,6 @@ # 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: ShipEngine SDK - on: push: branches: @@ -11,6 +9,7 @@ on: branches: - main +name: ShipEngine SDK CI jobs: lint_and_pytest: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 9400a61..d10e795 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,22 @@ pip install shipengine Instantiate ShipEngine Class ---------------------------- + ```python import os -from shipengine_sdk import ShipEngine +from shipengine import ShipEngine api_key = os.getenv("SHIPENGINE_API_KEY") shipengine = ShipEngine(api_key) ``` - You can also pass in a `dictionary` containing configuration options instead of just passing in a string that is your `API Key`. + ```python import os -from shipengine_sdk import ShipEngine +from shipengine import ShipEngine api_key = os.getenv("SHIPENGINE_API_KEY") diff --git a/docs/address_validation_example.md b/docs/address_validation_example.md index 8a62bf9..3434e96 100644 --- a/docs/address_validation_example.md +++ b/docs/address_validation_example.md @@ -61,7 +61,7 @@ Input Parameters ---------------- The `validate_address` method accepts an address object containing the properties listed below. -You can import the [`Address`](../shipengine_sdk/models/address/__init__.py) +You can import the [`Address`](../shipengine/models/address/__init__.py) type into your project to take advantage of your IDE's code completion functionality. @@ -108,7 +108,7 @@ A *string* between `0` and `1000` characters indicating the company name, if thi Output ------ The `validate_address` method returns an address validation result object containing the properties listed below. -You can import the [`AddressValidationResult`](../shipengine_sdk/models/address/__init__.py) +You can import the [`AddressValidationResult`](../shipengine/models/address/__init__.py) type into your project to take advantage of your IDE's code completion functionality. * `is_valid`
@@ -179,8 +179,8 @@ Examples: ```python import os -from shipengine_sdk import ShipEngine -from shipengine_sdk.models import Address +from shipengine import ShipEngine +from shipengine.models import Address api_key = os.getenv("SHIPENGINE_API_KEY") @@ -309,5 +309,5 @@ Exceptions ========== - This method will only throw an exception that is an instance/extension of - ([ShipEngineError](../shipengine_sdk/errors/__init__.py)) if there is a problem if a problem occurs, such as a + ([ShipEngineError](../shipengine/errors/__init__.py)) if there is a problem if a problem occurs, such as a network error or an error response from the API. diff --git a/docs/normalize_address_example.md b/docs/normalize_address_example.md index 9b426f4..29c322f 100644 --- a/docs/normalize_address_example.md +++ b/docs/normalize_address_example.md @@ -32,7 +32,7 @@ containing method-level configuration options. - **Behavior**: The `normalize_address` method will either return a normalized version of the address you pass in. This will throw an exception if address validation fails, or an invalid address is provided. The normalized address will - be returned as an instance of the [Address](../shipengine_sdk/models/address/__init__.py) class. + be returned as an instance of the [Address](../shipengine/models/address/__init__.py) class. - **Method level configuration** - You can optionally pass in an list that contains `configuration` values to be used for the current method call. The options are `api_key`, `base_uri`, `page_size`, @@ -150,8 +150,8 @@ Examples: ```python import os -from shipengine_sdk import ShipEngine -from shipengine_sdk.models import Address +from shipengine import ShipEngine +from shipengine.models import Address api_key = os.getenv("SHIPENGINE_API_KEY") diff --git a/docs/track_package_example.md b/docs/track_package_example.md index e6c7f74..baa851d 100644 --- a/docs/track_package_example.md +++ b/docs/track_package_example.md @@ -246,11 +246,12 @@ An *array of objects* representing the individual tracking events that have occu Example ======= + ```python import os -from shipengine_sdk import ShipEngine -from shipengine_sdk.models import TrackingQuery +from shipengine import ShipEngine +from shipengine.models import TrackingQuery api_key = os.getenv("SHIPENGINE_API_KEY") diff --git a/poetry.lock b/poetry.lock index 566626e..cee6afa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -245,6 +245,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "dunamai" +version = "1.5.5" +description = "Dynamic version generation" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + [[package]] name = "execnet" version = "1.9.0" @@ -359,7 +367,7 @@ plugins = ["setuptools"] name = "jinja2" version = "3.0.1" description = "A very fast and expressive template engine." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -373,7 +381,7 @@ i18n = ["Babel (>=2.7)"] name = "markupsafe" version = "2.0.1" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -479,6 +487,19 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "poetry-dynamic-versioning" +version = "0.13.0" +description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + +[package.dependencies] +dunamai = ">=1.5,<2.0" +jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} +tomlkit = ">=0.4" + [[package]] name = "pre-commit" version = "2.13.0" @@ -667,22 +688,6 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] -[[package]] -name = "responses" -version = "0.13.3" -description = "A utility library for mocking out the `requests` Python library." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -requests = ">=2.0" -six = "*" -urllib3 = ">=1.25.10" - -[package.extras] -tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] - [[package]] name = "six" version = "1.16.0" @@ -817,6 +822,14 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "tomlkit" +version = "0.7.2" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "tox" version = "3.24.1" @@ -940,7 +953,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "44a12436d6071b62002beab90bd98605720f47f4c08c492d52f1b482efaeadbf" +content-hash = "1f2c582aacd4bf12da11b7944e65440a1ddc3aaaaf80ac6c0bcb6d478db78f30" [metadata.files] aiohttp = [ @@ -1114,6 +1127,10 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +dunamai = [ + {file = "dunamai-1.5.5-py3-none-any.whl", hash = "sha256:525ac30db6ca4f8e48b9f198c2e8fbc2a9ce3ea189768361c621ea635212ee49"}, + {file = "dunamai-1.5.5.tar.gz", hash = "sha256:32f30db71e8fd1adeb42fac45c04433680e47a28298447cd30304e0bba95a7dd"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, @@ -1268,6 +1285,10 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +poetry-dynamic-versioning = [ + {file = "poetry-dynamic-versioning-0.13.0.tar.gz", hash = "sha256:52e64165c811573e719b43310a09416c894afa6662a4035de5d888199ee49760"}, + {file = "poetry_dynamic_versioning-0.13.0-py3-none-any.whl", hash = "sha256:46754061380ac772f49f60036471223804edc5a07b6aaee96e0a0793b1efae6a"}, +] pre-commit = [ {file = "pre_commit-2.13.0-py2.py3-none-any.whl", hash = "sha256:b679d0fddd5b9d6d98783ae5f10fd0c4c59954f375b70a58cbe1ce9bcf9809a4"}, {file = "pre_commit-2.13.0.tar.gz", hash = "sha256:764972c60693dc668ba8e86eb29654ec3144501310f7198742a767bec385a378"}, @@ -1388,10 +1409,6 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] -responses = [ - {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, - {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, -] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1435,6 +1452,10 @@ toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] +tomlkit = [ + {file = "tomlkit-0.7.2-py2.py3-none-any.whl", hash = "sha256:173ad840fa5d2aac140528ca1933c29791b79a374a0861a80347f42ec9328117"}, + {file = "tomlkit-0.7.2.tar.gz", hash = "sha256:d7a454f319a7e9bd2e249f239168729327e4dd2d27b17dc68be264ad1ce36754"}, +] tox = [ {file = "tox-3.24.1-py2.py3-none-any.whl", hash = "sha256:60eda26fa47b7130e6fc1145620b1fd897963af521093c3685c3f63d1c394029"}, {file = "tox-3.24.1.tar.gz", hash = "sha256:9850daeb96d21b4abf049bc5f197426123039e383ebfed201764e9355fc5a880"}, diff --git a/pyproject.toml b/pyproject.toml index fe3c149..2c47702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,21 @@ [tool.poetry] -name = "shipengine_sdk" -version = "0.0.1" +name = "shipengine" +version = "0.0.0" description = "A Python library for ShipEngine." authors = ["KaseyCantu "] license = "Apache-2.0" readme = "README.md" repository = "https://github.com/ShipEngine/shipengine-python" +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" +style = "semver" +format = "{base}" + +[tool.poetry-dynamic-versioning.substitution] +files = ["*.py", "*/__init__.py", "*/__version__.py", "*/_version.py"] + [tool.poetry.dependencies] python = "^3.7" aiohttp = "^3.7.4" @@ -14,6 +23,7 @@ requests = "^2.25.1" python-dotenv = "^0.15.0" dataclasses-json = "^0.5.3" fuuid = "^0.1.0" +poetry-dynamic-versioning = "^0.13.0" [tool.poetry.dev-dependencies] pytest = ">=5.0" @@ -45,5 +55,5 @@ relative_files = true [tool.poetry.scripts] [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"] build-backend = "poetry.core.masonry.api" diff --git a/pytest.ini b/pytest.ini index eff6106..5ea4619 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -addopts = -ra -v --cov=shipengine_sdk +addopts = -ra -v --cov=shipengine testpaths = tests filterwarnings = ignore:"@coroutine" decorator is deprecated since Python 3.8, use "async def" instead:DeprecationWarning diff --git a/shipengine_sdk/__init__.py b/shipengine/__init__.py similarity index 91% rename from shipengine_sdk/__init__.py rename to shipengine/__init__.py index 0bbbcc5..ebd4ff4 100644 --- a/shipengine_sdk/__init__.py +++ b/shipengine/__init__.py @@ -1,5 +1,5 @@ """ShipEngine SDK.""" -__version__ = "0.0.1" +__version__ = "0.0.0" import logging from logging import NullHandler diff --git a/shipengine_sdk/enums/__init__.py b/shipengine/enums/__init__.py similarity index 100% rename from shipengine_sdk/enums/__init__.py rename to shipengine/enums/__init__.py diff --git a/shipengine_sdk/enums/country.py b/shipengine/enums/country.py similarity index 100% rename from shipengine_sdk/enums/country.py rename to shipengine/enums/country.py diff --git a/shipengine_sdk/enums/error_code.py b/shipengine/enums/error_code.py similarity index 100% rename from shipengine_sdk/enums/error_code.py rename to shipengine/enums/error_code.py diff --git a/shipengine_sdk/enums/error_source.py b/shipengine/enums/error_source.py similarity index 100% rename from shipengine_sdk/enums/error_source.py rename to shipengine/enums/error_source.py diff --git a/shipengine_sdk/enums/error_type.py b/shipengine/enums/error_type.py similarity index 100% rename from shipengine_sdk/enums/error_type.py rename to shipengine/enums/error_type.py diff --git a/shipengine_sdk/enums/regex_patterns.py b/shipengine/enums/regex_patterns.py similarity index 100% rename from shipengine_sdk/enums/regex_patterns.py rename to shipengine/enums/regex_patterns.py diff --git a/shipengine_sdk/errors/__init__.py b/shipengine/errors/__init__.py similarity index 97% rename from shipengine_sdk/errors/__init__.py rename to shipengine/errors/__init__.py index b2ae22c..a1e7968 100644 --- a/shipengine_sdk/errors/__init__.py +++ b/shipengine/errors/__init__.py @@ -2,12 +2,7 @@ import json from typing import Optional -from shipengine_sdk.enums import ( - ErrorCode, - ErrorSource, - ErrorType, - does_member_value_exist, -) +from shipengine.enums import ErrorCode, ErrorSource, ErrorType, does_member_value_exist class ShipEngineError(Exception): diff --git a/shipengine_sdk/http_client/__init__.py b/shipengine/http_client/__init__.py similarity index 100% rename from shipengine_sdk/http_client/__init__.py rename to shipengine/http_client/__init__.py diff --git a/shipengine_sdk/http_client/client.py b/shipengine/http_client/client.py similarity index 99% rename from shipengine_sdk/http_client/client.py rename to shipengine/http_client/client.py index b89bf17..d938635 100644 --- a/shipengine_sdk/http_client/client.py +++ b/shipengine/http_client/client.py @@ -12,7 +12,7 @@ from requests.auth import AuthBase from requests.packages.urllib3.util.retry import Retry -from shipengine_sdk import __version__ +from shipengine import __version__ from ..enums import ErrorCode, ErrorSource, ErrorType, HTTPVerbs from ..errors import RateLimitExceededError, ShipEngineError diff --git a/shipengine_sdk/shipengine.py b/shipengine/shipengine.py similarity index 99% rename from shipengine_sdk/shipengine.py rename to shipengine/shipengine.py index 73b2e5f..23a89f2 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine/shipengine.py @@ -1,7 +1,7 @@ """The entrypoint to the ShipEngine API SDK.""" from typing import Any, Dict, List, Union -from shipengine_sdk.enums import Endpoints +from shipengine.enums import Endpoints from .http_client import ShipEngineClient from .shipengine_config import ShipEngineConfig diff --git a/shipengine_sdk/shipengine_config.py b/shipengine/shipengine_config.py similarity index 100% rename from shipengine_sdk/shipengine_config.py rename to shipengine/shipengine_config.py diff --git a/shipengine_sdk/util/__init__.py b/shipengine/util/__init__.py similarity index 100% rename from shipengine_sdk/util/__init__.py rename to shipengine/util/__init__.py diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine/util/sdk_assertions.py similarity index 99% rename from shipengine_sdk/util/sdk_assertions.py rename to shipengine/util/sdk_assertions.py index d3d9e00..791139e 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine/util/sdk_assertions.py @@ -2,7 +2,7 @@ import re from typing import Any, Dict, List -from shipengine_sdk.enums import Country, ErrorCode, ErrorSource, ErrorType +from shipengine.enums import Country, ErrorCode, ErrorSource, ErrorType from ..errors import ( ClientSystemError, diff --git a/tests/errors/test_errors.py b/tests/errors/test_errors.py index d71c493..dea3afc 100644 --- a/tests/errors/test_errors.py +++ b/tests/errors/test_errors.py @@ -1,7 +1,7 @@ """Tests for the ShipEngine SDK Errors""" import pytest -from shipengine_sdk.errors import ( +from shipengine.errors import ( AccountStatusError, BusinessRuleError, ClientSecurityError, diff --git a/tests/services/test_get_rate_from_shipment.py b/tests/services/test_get_rate_from_shipment.py index 609ddfc..9789475 100644 --- a/tests/services/test_get_rate_from_shipment.py +++ b/tests/services/test_get_rate_from_shipment.py @@ -5,7 +5,7 @@ import responses -from shipengine_sdk.enums import BaseURL, Endpoints +from shipengine.enums import BaseURL, Endpoints from tests.util import stub_shipengine_instance diff --git a/tests/services/test_list_carriers.py b/tests/services/test_list_carriers.py index d015553..cd3bd35 100644 --- a/tests/services/test_list_carriers.py +++ b/tests/services/test_list_carriers.py @@ -5,7 +5,7 @@ import responses -from shipengine_sdk.enums import BaseURL, Endpoints +from shipengine.enums import BaseURL, Endpoints from tests.util import stub_shipengine_instance diff --git a/tests/services/test_validate_addresses.py b/tests/services/test_validate_addresses.py index 4c90b43..2483700 100644 --- a/tests/services/test_validate_addresses.py +++ b/tests/services/test_validate_addresses.py @@ -5,7 +5,7 @@ import responses -from shipengine_sdk.enums import BaseURL, Endpoints +from shipengine.enums import BaseURL, Endpoints from ..util import stub_shipengine_instance, valid_commercial_address diff --git a/tests/test_shipengine.py b/tests/test_shipengine.py index 86d78a9..462d267 100644 --- a/tests/test_shipengine.py +++ b/tests/test_shipengine.py @@ -1,9 +1,9 @@ """Testing the ShipEngine object.""" import pytest -from shipengine_sdk import ShipEngine, __version__ -from shipengine_sdk.errors import ValidationError -from shipengine_sdk.util import api_key_validation_error_assertions +from shipengine import ShipEngine +from shipengine.errors import ValidationError +from shipengine.util import api_key_validation_error_assertions def shipengine_empty_api_key(): @@ -15,10 +15,6 @@ def shipengine_no_api_key(): class TestShipEngine: - def test_version(self) -> None: - """Test the package version of the ShipEngine SDK.""" - assert __version__ == "0.0.1" - def test_no_api_key_provided(self) -> None: """DX-1440 - No API Key at instantiation.""" try: diff --git a/tests/test_shipengine_config.py b/tests/test_shipengine_config.py index 2ddbe05..7d784b2 100644 --- a/tests/test_shipengine_config.py +++ b/tests/test_shipengine_config.py @@ -1,11 +1,11 @@ """Testing the ShipEngineConfig object.""" import pytest -from shipengine_sdk import ShipEngine, ShipEngineConfig -from shipengine_sdk.enums import BaseURL, Constants -from shipengine_sdk.errors import InvalidFieldValueError, ValidationError -from shipengine_sdk.util import api_key_validation_error_assertions -from shipengine_sdk.util.sdk_assertions import timeout_validation_error_assertions +from shipengine import ShipEngine, ShipEngineConfig +from shipengine.enums import BaseURL, Constants +from shipengine.errors import InvalidFieldValueError, ValidationError +from shipengine.util import api_key_validation_error_assertions +from shipengine.util.sdk_assertions import timeout_validation_error_assertions def stub_config() -> dict: diff --git a/tests/test_snake_case_converter.py b/tests/test_snake_case_converter.py index 7c97eea..fbd8f78 100644 --- a/tests/test_snake_case_converter.py +++ b/tests/test_snake_case_converter.py @@ -1,5 +1,5 @@ """Test test conversion helper function. snake_case -> camelCase.""" -from shipengine_sdk.util import snake_to_camel +from shipengine.util import snake_to_camel class TestSnakeToCamelCase: diff --git a/tests/util/test_helpers.py b/tests/util/test_helpers.py index 3d9168c..74baf81 100644 --- a/tests/util/test_helpers.py +++ b/tests/util/test_helpers.py @@ -1,8 +1,8 @@ """Test data as functions and common assertion helper functions.""" from typing import Any, Dict, List -from shipengine_sdk import ShipEngine, ShipEngineConfig -from shipengine_sdk.enums import Constants +from shipengine import ShipEngine, ShipEngineConfig +from shipengine.enums import Constants def stub_config( From e5435cba0ab8e8adad86f57178ccb6590ee58335 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 16:52:57 -0500 Subject: [PATCH 08/13] updated requirements.txt --- requirements.txt | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/requirements.txt b/requirements.txt index ec44660..f2fa686 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,44 +1,49 @@ aiohttp==3.7.4.post0; python_version >= "3.6" alabaster==0.7.12; python_version >= "3.5" -appdirs==1.4.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +appdirs==1.4.4; python_version >= "3.6" async-timeout==3.0.1; python_full_version >= "3.5.3" and python_version >= "3.6" atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" babel==2.9.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" +backports.entry-points-selectable==1.1.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "2.7" base58==2.1.0; python_version >= "3.7" and python_version < "4.0" black==20.8b1; python_version >= "3.6" -certifi==2021.5.30; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" +certifi==2021.5.30; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" cfgv==3.3.0; python_full_version >= "3.6.1" chardet==4.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +charset-normalizer==2.0.4; python_full_version >= "3.6.0" and python_version >= "3.5" click==8.0.1; python_version >= "3.6" colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") -coveralls==3.1.0; python_version >= "3.5" +coveralls==3.2.0; python_version >= "3.5" dataclasses-json==0.5.4; python_version >= "3.6" distlib==0.3.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" docopt==0.6.2; python_version >= "3.5" docutils==0.16; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" +dunamai==1.5.5; python_version >= "3.5" and python_version < "4.0" execnet==1.9.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" filelock==3.0.12; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") fuuid==0.1.0; python_version >= "3.7" and python_version < "4.0" -identify==2.2.10; python_full_version >= "3.6.1" -idna==2.10; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +identify==2.2.12; python_full_version >= "3.6.1" +idna==3.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6" imagesize==1.2.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" -importlib-metadata==4.6.1; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6") and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.4.0" and python_version >= "3.6" and python_version < "3.8") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") +importlib-metadata==4.6.3; python_version < "3.8" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.6") and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.4.0" and python_version >= "3.6" and python_version < "3.8") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") iniconfig==1.1.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -isort==5.9.1; python_full_version >= "3.6.1" and python_version < "4.0" -jinja2==3.0.1; python_version >= "3.6" -markupsafe==2.0.1; python_version >= "3.6" -marshmallow==3.12.1; python_version >= "3.6" +isort==5.9.3; python_full_version >= "3.6.1" and python_version < "4.0" +jinja2==3.0.1; python_version >= "3.6" and python_version < "4.0" +markupsafe==2.0.1; python_version >= "3.6" and python_version < "4.0" +marshmallow==3.13.0; python_version >= "3.6" marshmallow-enum==1.5.1; python_version >= "3.6" mccabe==0.6.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" multidict==5.1.0; python_version >= "3.6" mypy-extensions==0.4.3; python_version >= "3.6" nodeenv==1.6.0; python_full_version >= "3.6.1" packaging==21.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -pathspec==0.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pathspec==0.9.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +platformdirs==2.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pluggy==0.13.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +poetry-dynamic-versioning==0.13.0; python_version >= "3.5" and python_version < "4.0" pre-commit==2.13.0; python_full_version >= "3.6.1" py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" @@ -53,9 +58,8 @@ pytest-watch==4.2.0 python-dotenv==0.15.0 pytz==2021.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5" pyyaml==5.4.1; python_full_version >= "3.6.1" -regex==2021.7.6; python_version >= "3.6" -requests==2.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +regex==2021.8.3; python_version >= "3.6" +requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" snowballstemmer==2.1.0; python_version >= "3.5" sphinx==3.5.4; python_version >= "3.5" @@ -67,12 +71,13 @@ sphinxcontrib-qthelp==1.0.3; python_version >= "3.5" sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.5" stringcase==1.2.0; python_version >= "3.6" toml==0.10.2; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") -tox==3.23.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +tomlkit==0.7.2; python_version >= "3.5" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.5" and python_version < "4.0" and python_full_version >= "3.5.0" +tox==3.24.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") typed-ast==1.4.3; python_version >= "3.6" typing-extensions==3.10.0.0; python_version < "3.8" and python_version >= "3.6" typing-inspect==0.7.1; python_version >= "3.6" -urllib3==1.26.6; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.5" -virtualenv==20.4.7; python_full_version >= "3.6.1" +urllib3==1.26.6; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.5" +virtualenv==20.7.0; python_full_version >= "3.6.1" watchdog==2.1.3; python_version >= "3.6" yarl==1.6.3; python_version >= "3.6" zipp==3.5.0; python_version < "3.8" and python_version >= "3.6" From 1f647889f950d5f3d5e2f5fc76bed52926b3aeb0 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 17:04:54 -0500 Subject: [PATCH 09/13] Adjustments to tox.ini regarding versioning --- tox.ini | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index f69bfac..667a56e 100644 --- a/tox.ini +++ b/tox.ini @@ -8,9 +8,10 @@ envlist = linting, py37, py38, py39 -minversion = 3.6.0 -isolated_build = True -skip_missing_interpreters = True +minversion = 3.7.0 +isolated_build = true +skip_missing_interpreters = true +skipsdist = true [tox:.package] basepython = python3.7 @@ -41,12 +42,12 @@ commands = ; coveralls --submit={toxworkdir}/.coverage.{envname} [testenv:lint] -skip_install = True +skip_install = true deps = pre-commit>=2.9.3 commands = pre-commit run --show-diff-on-failure {posargs:} [coverage:run] -relative_files = True +relative_files = true [flake8] max-line-length = 100 From a419dbd99845cfa1fddbe799234c453e49f88c07 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 17:15:57 -0500 Subject: [PATCH 10/13] Adjusted tox.ini due to issues with py38 --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 667a56e..31b51a3 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,9 @@ [tox] envlist = linting, - py37, - py38, - py39 + python3.7, + python3.8, + python3.9 minversion = 3.7.0 isolated_build = true skip_missing_interpreters = true From 96c954b792442a396d30cfe0a48c667f54443da8 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 17:18:59 -0500 Subject: [PATCH 11/13] fixed deps bug :bug: --- poetry.lock | 22 +++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index cee6afa..a891f4e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -688,6 +688,22 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "responses" +version = "0.13.3" +description = "A utility library for mocking out the `requests` Python library." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +requests = ">=2.0" +six = "*" +urllib3 = ">=1.25.10" + +[package.extras] +tests = ["coverage (>=3.7.1,<6.0.0)", "pytest-cov", "pytest-localserver", "flake8", "pytest (>=4.6,<5.0)", "pytest (>=4.6)", "mypy"] + [[package]] name = "six" version = "1.16.0" @@ -953,7 +969,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "1f2c582aacd4bf12da11b7944e65440a1ddc3aaaaf80ac6c0bcb6d478db78f30" +content-hash = "01a440f2e93072393a4ac176eb85ed8bca6eabf566e6c1bc47dab1e7b14e2a9f" [metadata.files] aiohttp = [ @@ -1409,6 +1425,10 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +responses = [ + {file = "responses-0.13.3-py2.py3-none-any.whl", hash = "sha256:b54067596f331786f5ed094ff21e8d79e6a1c68ef625180a7d34808d6f36c11b"}, + {file = "responses-0.13.3.tar.gz", hash = "sha256:18a5b88eb24143adbf2b4100f328a2f5bfa72fbdacf12d97d41f07c26c45553d"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index 2c47702..68a0842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ coveralls = "^3.1.0" pre-commit = "2.13.0" pytest-cache = "^1.0" pytest-watch = "^4.2.0" +responses = "^0.13.3" [tool.black] line-length = 100 From e545ed5687f259482cc597b52959da0bf315e99b Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 17:23:00 -0500 Subject: [PATCH 12/13] fixed deps bug :bug: --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 68a0842..a7335cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ coverage = "^5.5" isort = "^5.8.0" coveralls = "^3.1.0" pre-commit = "2.13.0" -pytest-cache = "^1.0" pytest-watch = "^4.2.0" responses = "^0.13.3" From 620f179297beb802ae7e6224f179c1195bd5a9c3 Mon Sep 17 00:00:00 2001 From: KaseyCantu Date: Wed, 4 Aug 2021 17:24:27 -0500 Subject: [PATCH 13/13] fixed deps bug :bug: --- poetry.lock | 32 +------------------------------- requirements.txt | 3 +-- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index a891f4e..c98f783 100644 --- a/poetry.lock +++ b/poetry.lock @@ -253,17 +253,6 @@ category = "main" optional = false python-versions = ">=3.5,<4.0" -[[package]] -name = "execnet" -version = "1.9.0" -description = "execnet: rapid multi-Python deployment" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -testing = ["pre-commit"] - [[package]] name = "filelock" version = "3.0.12" @@ -579,18 +568,6 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] -[[package]] -name = "pytest-cache" -version = "1.0" -description = "pytest plugin with mechanisms for caching across test runs" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -execnet = ">=1.1.dev1" -pytest = ">=2.2" - [[package]] name = "pytest-cov" version = "2.12.1" @@ -969,7 +946,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "01a440f2e93072393a4ac176eb85ed8bca6eabf566e6c1bc47dab1e7b14e2a9f" +content-hash = "c6f0138bb35e8ebd809a9d3a66b240e1250eabf6767686df4bc0b85ec44904e7" [metadata.files] aiohttp = [ @@ -1147,10 +1124,6 @@ dunamai = [ {file = "dunamai-1.5.5-py3-none-any.whl", hash = "sha256:525ac30db6ca4f8e48b9f198c2e8fbc2a9ce3ea189768361c621ea635212ee49"}, {file = "dunamai-1.5.5.tar.gz", hash = "sha256:32f30db71e8fd1adeb42fac45c04433680e47a28298447cd30304e0bba95a7dd"}, ] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, @@ -1333,9 +1306,6 @@ pytest = [ {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, ] -pytest-cache = [ - {file = "pytest-cache-1.0.tar.gz", hash = "sha256:be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9"}, -] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, diff --git a/requirements.txt b/requirements.txt index f2fa686..433e97f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,6 @@ distlib==0.3.2; python_version >= "2.7" and python_full_version < "3.0.0" or pyt docopt==0.6.2; python_version >= "3.5" docutils==0.16; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" dunamai==1.5.5; python_version >= "3.5" and python_version < "4.0" -execnet==1.9.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" filelock==3.0.12; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") fuuid==0.1.0; python_version >= "3.7" and python_version < "4.0" @@ -51,7 +50,6 @@ pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or py pygments==2.9.0; python_version >= "3.5" pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6" pytest==6.2.4; python_version >= "3.6" -pytest-cache==1.0 pytest-cov==2.12.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") pytest-mock==3.6.1; python_version >= "3.6" pytest-watch==4.2.0 @@ -60,6 +58,7 @@ pytz==2021.1; python_version >= "3.5" and python_full_version < "3.0.0" or pytho pyyaml==5.4.1; python_full_version >= "3.6.1" regex==2021.8.3; python_version >= "3.6" requests==2.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0") +responses==0.13.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" snowballstemmer==2.1.0; python_version >= "3.5" sphinx==3.5.4; python_version >= "3.5"