From aea55861843231b6b7361c94a6b7a1befed4d881 Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Thu, 18 Jul 2024 13:50:33 -0600 Subject: [PATCH] - Stash --- easypost/constant.py | 2 +- easypost/easypost_client.py | 477 +++++++++++++++--- easypost/easypost_object.py | 210 +------- easypost/hooks/request_hook.py | 11 +- easypost/http.py | 9 + easypost/models/__init__.py | 25 - easypost/models/address.py | 34 +- easypost/models/api_key.py | 13 +- easypost/models/batch.py | 28 +- easypost/models/batch_shipment.py | 10 + easypost/models/beta/__init__.py | 0 easypost/models/beta/stateless_rate.py | 11 + easypost/models/billing.py | 5 - easypost/models/brand.py | 13 +- easypost/models/carrier_account.py | 17 +- easypost/models/carrier_account_type.py | 7 + easypost/models/carrier_detail.py | 20 + easypost/models/carrier_fields.py | 18 + easypost/models/carrier_metadata.py | 62 +++ easypost/models/carrier_type.py | 14 + easypost/models/customs_info.py | 18 +- easypost/models/customs_item.py | 12 +- easypost/models/end_shipper.py | 28 +- easypost/models/error.py | 43 ++ easypost/models/event.py | 24 +- easypost/models/fee.py | 11 + easypost/models/form.py | 10 + easypost/models/insurance.py | 31 +- easypost/models/message.py | 11 + easypost/models/options.py | 111 ++++ easypost/models/order.py | 19 + easypost/models/parcel.py | 9 +- easypost/models/payload.py | 14 +- easypost/models/payment_method.py | 43 ++ easypost/models/payment_methods_summary.py | 16 + easypost/models/pickup.py | 19 + easypost/models/pickup_rate.py | 5 +- easypost/models/postage_label.py | 17 +- easypost/models/rate.py | 20 +- .../rate_with_estimated_delivery_date.py | 18 + easypost/models/referral_customer.py | 35 ++ easypost/models/refund.py | 21 +- easypost/models/report.py | 23 +- easypost/models/scan_form.py | 24 +- easypost/models/shipment.py | 52 +- easypost/models/smart_rate.py | 25 + easypost/models/smart_rate_accuracy.py | 11 + easypost/models/sort_direction.py | 6 + easypost/models/tax_identifier.py | 11 + easypost/models/time_in_transit.py | 35 ++ easypost/models/tracker.py | 33 +- easypost/models/tracking_detail.py | 15 + easypost/models/tracking_location.py | 11 + easypost/models/user.py | 34 +- easypost/models/verification.py | 15 + easypost/models/verification_details.py | 10 + easypost/models/verifications.py | 10 + easypost/models/webhook.py | 7 +- easypost/services/__init__.py | 26 - easypost/services/address_service.py | 58 +-- easypost/services/api_key_service.py | 13 +- easypost/services/base_service.py | 42 +- easypost/services/batch_service.py | 70 +-- easypost/services/beta_rate_service.py | 20 +- easypost/services/billing_service.py | 4 +- easypost/services/carrier_account_service.py | 5 +- easypost/services/carrier_metadata_service.py | 2 +- easypost/services/customs_info_service.py | 5 +- easypost/services/customs_item_service.py | 5 +- easypost/services/end_shipper_service.py | 10 +- easypost/services/event_service.py | 9 +- easypost/services/insurance_service.py | 5 +- easypost/services/order_service.py | 5 +- easypost/services/parcel_service.py | 5 +- easypost/services/pickup_service.py | 5 +- easypost/services/rate_service.py | 5 +- .../services/referral_customer_service.py | 5 +- easypost/services/refund_service.py | 5 +- easypost/services/report_service.py | 5 +- easypost/services/scan_form_service.py | 5 +- easypost/services/shipment_service.py | 9 +- easypost/services/tracker_service.py | 5 +- easypost/services/user_service.py | 9 +- easypost/services/webhook_service.py | 5 +- tests/test_address.py | 12 +- tests/test_api_key.py | 2 +- tests/test_batch.py | 2 +- tests/test_carrier_account.py | 2 +- tests/test_customs_info.py | 2 +- tests/test_customs_item.py | 2 +- tests/test_end_shipper.py | 2 +- tests/test_event.py | 6 +- tests/test_insurance.py | 2 +- tests/test_order.py | 6 +- tests/test_parcel.py | 2 +- tests/test_pickup.py | 2 +- tests/test_rate.py | 2 +- tests/test_referral_customer.py | 2 +- tests/test_refund.py | 2 +- tests/test_report.py | 2 +- tests/test_scan_form.py | 2 +- tests/test_shipment.py | 6 +- tests/test_tracker.py | 2 +- tests/test_user.py | 6 +- tests/test_webhook.py | 2 +- 105 files changed, 1698 insertions(+), 555 deletions(-) create mode 100644 easypost/http.py create mode 100644 easypost/models/batch_shipment.py create mode 100644 easypost/models/beta/__init__.py create mode 100644 easypost/models/beta/stateless_rate.py delete mode 100644 easypost/models/billing.py create mode 100644 easypost/models/carrier_account_type.py create mode 100644 easypost/models/carrier_detail.py create mode 100644 easypost/models/carrier_fields.py create mode 100644 easypost/models/carrier_metadata.py create mode 100644 easypost/models/carrier_type.py create mode 100644 easypost/models/error.py create mode 100644 easypost/models/fee.py create mode 100644 easypost/models/form.py create mode 100644 easypost/models/message.py create mode 100644 easypost/models/options.py create mode 100644 easypost/models/payment_method.py create mode 100644 easypost/models/payment_methods_summary.py create mode 100644 easypost/models/rate_with_estimated_delivery_date.py create mode 100644 easypost/models/referral_customer.py create mode 100644 easypost/models/smart_rate.py create mode 100644 easypost/models/smart_rate_accuracy.py create mode 100644 easypost/models/sort_direction.py create mode 100644 easypost/models/tax_identifier.py create mode 100644 easypost/models/time_in_transit.py create mode 100644 easypost/models/tracking_detail.py create mode 100644 easypost/models/tracking_location.py create mode 100644 easypost/models/verification.py create mode 100644 easypost/models/verification_details.py create mode 100644 easypost/models/verifications.py diff --git a/easypost/constant.py b/easypost/constant.py index 0f9feb18..f654650b 100644 --- a/easypost/constant.py +++ b/easypost/constant.py @@ -17,7 +17,7 @@ INVALID_REQUEST_LIB_ERROR = "Bug discovered: invalid request_lib: {}. Please report to {}." INVALID_REQUEST_METHOD_ERROR = "Bug discovered: invalid request method: {}. Please report to {}." INVALID_REQUEST_PARAMETERS_ERROR = "Only GET and DELETE requests support parameters." -INVALID_REQUESTS_VERSION_ERROR = 'EasyPost requires an up to date requests library. Update requests via "pip install -U requests" or contact us at {}.' +INVALID_REQUESTS_VERSION_ERROR = 'EasyPost requires an up-to-date requests library. Update objectrest via "pip install -U requests" or contact us at {}.' INVALID_RESPONSE_BODY_ERROR = "Invalid response from API: ({}) {}" INVALID_SIGNATURE_ERROR = "Webhook received does not contain an HMAC signature." INVALID_WEBHOOK_VALIDATION_ERROR = "Webhook received did not originate from EasyPost or had a webhook secret mismatch." diff --git a/easypost/easypost_client.py b/easypost/easypost_client.py index c7ab75d9..c3fd04ab 100644 --- a/easypost/easypost_client.py +++ b/easypost/easypost_client.py @@ -1,41 +1,83 @@ +import enum +import json +import platform +import time +import uuid +from datetime import ( + datetime, + timezone, +) +from typing import ( + Any, + Dict, + Optional, + Tuple, +) +from urllib.parse import urlencode + +import requests from easypost.constant import ( API_BASE, - API_VERSION, - INVALID_REQUESTS_VERSION_ERROR, + COMMUNICATION_ERROR, + INVALID_REQUEST_LIB_ERROR, + INVALID_REQUEST_METHOD_ERROR, + INVALID_REQUEST_PARAMETERS_ERROR, + INVALID_RESPONSE_BODY_ERROR, SUPPORT_EMAIL, TIMEOUT, + TIMEOUT_ERROR, + VERSION, +) +from easypost.easypost_object import EasyPostObject +from easypost.errors import ( + EasyPostError, + HttpError, + JsonError, + RedirectError, + UnknownApiError, ) from easypost.hooks import ( RequestHook, ResponseHook, ) -from easypost.services import ( - AddressService, - ApiKeyService, - BatchService, - BetaRateService, - BetaReferralCustomerService, - BillingService, - CarrierAccountService, - CarrierMetadataService, - CustomsInfoService, - CustomsItemService, - EndShipperService, - EventService, - InsuranceService, - OrderService, - ParcelService, - PickupService, - RateService, - ReferralCustomerService, - RefundService, - ReportService, - ScanFormService, - ShipmentService, - TrackerService, - UserService, - WebhookService, -) +from easypost.http import HttpMethod +from easypost.requestor import STATUS_CODE_TO_ERROR_MAPPING +from easypost.services.address_service import AddressService +from easypost.services.api_key_service import ApiKeyService +from easypost.services.batch_service import BatchService +from easypost.services.beta_rate_service import BetaRateService +from easypost.services.beta_referral_customer_service import BetaReferralCustomerService +from easypost.services.billing_service import BillingService +from easypost.services.carrier_account_service import CarrierAccountService +from easypost.services.carrier_metadata_service import CarrierMetadataService +from easypost.services.customs_info_service import CustomsInfoService +from easypost.services.customs_item_service import CustomsItemService +from easypost.services.end_shipper_service import EndShipperService +from easypost.services.event_service import EventService +from easypost.services.insurance_service import InsuranceService +from easypost.services.order_service import OrderService +from easypost.services.parcel_service import ParcelService +from easypost.services.pickup_service import PickupService +from easypost.services.rate_service import RateService +from easypost.services.referral_customer_service import ReferralCustomerService +from easypost.services.refund_service import RefundService +from easypost.services.report_service import ReportService +from easypost.services.scan_form_service import ScanFormService +from easypost.services.shipment_service import ShipmentService +from easypost.services.tracker_service import TrackerService +from easypost.services.user_service import UserService +from easypost.services.webhook_service import WebhookService +from requests import JSONDecodeError + + +class ApiVersion(enum.Enum): + Beta = "beta" + V2 = "v2" + + +class RequestEngine(enum.Enum): + URLFETCH = "urlfetch" + REQUESTS = "requests" class EasyPostClient: @@ -44,75 +86,69 @@ class EasyPostClient: def __init__( self, api_key: str, - api_base: str = f"{API_BASE}/{API_VERSION}", + api_base: str = API_BASE, + api_version: ApiVersion = ApiVersion.V2, timeout: int = TIMEOUT, ): # Client configuration - self.api_key = api_key - self.api_base = api_base - self.timeout = timeout + self.api_key: str = api_key + self._api_base: str = api_base + self._api_version: ApiVersion = api_version + self.timeout: int = timeout # Services - self.address = AddressService(self) - self.api_keys = ApiKeyService(self) - self.batch = BatchService(self) - self.beta_rate = BetaRateService(self) - self.beta_referral_customer = BetaReferralCustomerService(self) - self.billing = BillingService(self) - self.carrier_account = CarrierAccountService(self) - self.carrier_metadata = CarrierMetadataService(self) - self.customs_info = CustomsInfoService(self) - self.customs_item = CustomsItemService(self) - self.end_shipper = EndShipperService(self) - self.event = EventService(self) - self.insurance = InsuranceService(self) - self.order = OrderService(self) - self.parcel = ParcelService(self) - self.rate = RateService(self) - self.pickup = PickupService(self) - self.referral_customer = ReferralCustomerService(self) - self.refund = RefundService(self) - self.report = ReportService(self) - self.scan_form = ScanFormService(self) - self.shipment = ShipmentService(self) - self.tracker = TrackerService(self) - self.user = UserService(self) - self.webhook = WebhookService(self) + self.address: AddressService = AddressService(self) + self.api_keys: ApiKeyService = ApiKeyService(self) + self.batch: BatchService = BatchService(self) + self.beta_rate: BetaRateService = BetaRateService(self) + self.beta_referral_customer: BetaReferralCustomerService = BetaReferralCustomerService(self) + self.billing: BillingService = BillingService(self) + self.carrier_account: CarrierAccountService = CarrierAccountService(self) + self.carrier_metadata: CarrierMetadataService = CarrierMetadataService(self) + self.customs_info: CustomsInfoService = CustomsInfoService(self) + self.customs_item: CustomsItemService = CustomsItemService(self) + self.end_shipper: EndShipperService = EndShipperService(self) + self.event: EventService = EventService(self) + self.insurance: InsuranceService = InsuranceService(self) + self.order: OrderService = OrderService(self) + self.parcel: ParcelService = ParcelService(self) + self.rate: RateService = RateService(self) + self.pickup: PickupService = PickupService(self) + self.referral_customer: ReferralCustomerService = ReferralCustomerService(self) + self.refund: RefundService = RefundService(self) + self.report: ReportService = ReportService(self) + self.scan_form: ScanFormService = ScanFormService(self) + self.shipment: ShipmentService = ShipmentService(self) + self.tracker: TrackerService = TrackerService(self) + self.user: UserService = UserService(self) + self.webhook: WebhookService = WebhookService(self) # Hooks - self._request_hook = RequestHook() - self._response_hook = ResponseHook() + self._request_hook: RequestHook = RequestHook() + self._response_hook: ResponseHook = ResponseHook() # use urlfetch as request_lib on google app engine, otherwise use requests - self._request_lib = None + self._request_engine: Optional[RequestEngine] = None + self._requests_session: Optional[requests.Session] = None try: from google.appengine.api import urlfetch # type: ignore - self._request_lib = "urlfetch" + self._request_engine = RequestEngine.URLFETCH # use the GAE application-wide "deadline" (or its default) - timeout = urlfetch.get_default_fetch_deadline() or self.timeout + self.timeout = urlfetch.get_default_fetch_deadline() or self.timeout except ImportError: try: - import requests + import objectrest - self._request_lib = "requests" + self._request_engine = RequestEngine.REQUESTS self._requests_session = requests.Session() - requests_http_adapter = requests.adapters.HTTPAdapter(max_retries=3) - self._requests_session.mount( - prefix=self.api_base.split(f"/{API_VERSION}")[0], - adapter=requests_http_adapter, - ) - except Exception: - raise ImportError(INVALID_REQUESTS_VERSION_ERROR.format(SUPPORT_EMAIL)) + except ImportError: + raise ImportError(INVALID_REQUEST_LIB_ERROR.format(SUPPORT_EMAIL)) - try: - requests_version = requests.__version__ - major_version, _, _ = [int(i) for i in requests_version.split(".")] - except Exception: - raise ImportError(INVALID_REQUESTS_VERSION_ERROR.format(SUPPORT_EMAIL)) - else: - if major_version < 1: - raise ImportError(INVALID_REQUESTS_VERSION_ERROR.format(SUPPORT_EMAIL)) + @property + def api_base_url(self): + """Return the base URL for the EasyPost API.""" + return f"{self._api_base}/{self._api_version.value}" def subscribe_to_request_hook(self, function): """Subscribe functions to run when a request occurs.""" @@ -129,3 +165,284 @@ def subscribe_to_response_hook(self, function): def unsubscribe_from_response_hook(self, function): """Unsubscribe functions from running when a response occurs.""" self._response_hook -= function + + def _build_user_agent(self, api_version: ApiVersion) -> str: + """Build the user agent string.""" + details = { + "client_version": VERSION, + "implementation": "NA", + "os_arch": "NA", + "os_version": "NA", + "os": "NA", + "python_version": "NA", + } + + # Attempt to populate the user-agent header + for attr, func in ( + ("implementation", platform.python_implementation), + ("os_details", platform.uname), + ("python_version", platform.python_version), + ): + try: + val = func() # type: ignore + if attr == "os_details": + details["os"] = val[0] + details["os_version"] = val[2] + details["os_arch"] = val[4] + else: + user_agent[attr] = val # type: ignore + except Exception: # nosec + # If we fail to get OS info, do nothing as we already set fallbacks for these values + pass + + return ( + f"EasyPost/{api_version.value} PythonClient/{VERSION} Python/{details['python_version']}" # type: ignore + f" OS/{details['os']} OSVersion/{details['os_version']} OSArch/{details['os_arch']}" + f" Implementation/{details['implementation']}" + ) + + def _build_headers(self, api_version: ApiVersion) -> Dict[str, str]: + """Build the headers for the request.""" + return {"Authorization": f"Bearer {self.api_key}", "User-Agent": self._build_user_agent(api_version)} + + @classmethod + def _objects_to_ids(cls, param: Dict[str, Any]) -> Dict[str, Any]: + """If providing an object as a parameter to another object, + only pass along the ID so the API will use the object reference correctly. + """ + if isinstance(param, EasyPostObject): + return {"id": param.id} + elif isinstance(param, dict): + data = {} + for k, v in param.items(): + if isinstance(v, list): + data[k] = [cls._objects_to_ids(item) for item in v] # type: ignore + else: + data[k] = cls._objects_to_ids(v) # type: ignore + return data + else: + return param + + def _add_query_params_to_url(self, url: str, params: Dict[str, Any]) -> str: + """Add query parameters to a URL.""" + if not params: + return url + + converted_params = [] + + for key, value in sorted(params.items()): + if value is None: + continue # Don't add Nones to the query + elif isinstance(value, datetime): + value = int(time.mktime(value.timetuple())) # to UTC timestamp + converted_params.append((key, value)) + + encoded_params = urlencode(query=converted_params) + + if encoded_params: + return "%s?%s" % (url, encoded_params) + + return url + + def _request_with_urlfetch( + self, method: HttpMethod, abs_url: str, headers: Dict[str, Any], params: Dict[str, Any] + ) -> Tuple[str, int, Dict[str, Any]]: + """Make a request by using the `urlfetch` library.""" + fetch_args = { + "method": method.value, + "headers": headers, + "validate_certificate": False, + "deadline": self.timeout, + } + + if method in [HttpMethod.GET, HttpMethod.DELETE]: + # GET/DELETE requests use query params + fetch_args["url"] = self._add_query_params_to_url(url=abs_url, params=params) + elif method in [HttpMethod.POST, HttpMethod.PUT]: + fetch_args["url"] = abs_url + # POST/PUT requests use body params + fetch_args["payload"] = json.dumps( + params, default=lambda v: v.decode("utf-8") if isinstance(v, bytes) else v + ) + else: + raise EasyPostError(INVALID_REQUEST_METHOD_ERROR.format(method, SUPPORT_EMAIL)) + + try: + from google.appengine.api import urlfetch # type: ignore + + result = urlfetch.fetch(**fetch_args) # type: ignore + + response_body: str = result.content + response_status_code: int = result.status_code + response_headers: dict[str, Any] = result.headers + except Exception as e: + raise HttpError(COMMUNICATION_ERROR.format(SUPPORT_EMAIL, e)) + + return response_body, response_status_code, response_headers + + def _request_with_requests( + self, method: HttpMethod, abs_url: str, headers: Dict[str, Any], params: Dict[str, Any] + ) -> Tuple[str, int, Dict[str, Any]]: + """Make a request by using the `request` library.""" + if method in [HttpMethod.GET, HttpMethod.DELETE]: + request_query = params + request_body = None + elif method in [HttpMethod.POST, HttpMethod.PATCH, HttpMethod.PUT]: + request_query = None + request_body = params + else: + raise EasyPostError(INVALID_REQUEST_METHOD_ERROR.format(method, SUPPORT_EMAIL)) + + if request_query and method not in [HttpMethod.GET, HttpMethod.DELETE]: + raise EasyPostError(INVALID_REQUEST_PARAMETERS_ERROR) + + try: + result = self._requests_session.request( + method=method.value, + url=abs_url, + params=request_query, + headers=headers, + json=request_body, + timeout=self.timeout, + verify=True, + ) + response_body: str = result.text + response_status: int = result.status_code + response_headers: dict[str, Any] = result.headers + except requests.exceptions.Timeout: + raise TimeoutError(TIMEOUT_ERROR) + except Exception as e: + raise HttpError(COMMUNICATION_ERROR.format(SUPPORT_EMAIL, e)) + + return response_body, response_status, response_headers + + def _interpret_response(self, response_body: str, response_status: int) -> dict: + if response_status == 204: + # HTTP 204 does not have any response body and we can just return here + return {} + + try: + response_json = json.loads(response_body) + except JSONDecodeError: + raise JsonError(INVALID_RESPONSE_BODY_ERROR.format(response_body, response_status)) + + if response_status < 200 or response_status >= 300: + self._handle_api_error( + response_status=response_status, response_body=response_body, response_json=response_json + ) + + return response_json + + def _handle_api_error(self, response_status: int, response_body: str, response_json: dict) -> None: + """Handles API errors returned from the EasyPost API.""" + try: + error = response_json["error"] + except (KeyError, TypeError): + raise JsonError( + message=INVALID_RESPONSE_BODY_ERROR.format(response_status, response_body), + http_status=response_status, + http_body=response_body, + ) + + if 300 <= response_status < 400: + raise RedirectError(message=error, http_status=response_status, http_body=response_body) + + error_type = STATUS_CODE_TO_ERROR_MAPPING.get(response_status, UnknownApiError) + + raise error_type( + message=error.get("message", ""), + http_status=response_status, + http_body=response_body, + ) + + def request( + self, + klass: type, + method: HttpMethod, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + root_key: Optional[str] = None, + override_api_version: Optional[ApiVersion] = None, + ) -> Any: + """ + Make a request to the EasyPost API. + + :param klass: The class of the object to construct from the response JSON. + :type klass: type + :param method: The HTTP method to use. + :type method: HttpMethod + :param endpoint: The endpoint to hit. + :type endpoint: str + :param params: The parameters to send in the request. + :type params: Optional[Dict[str, Any]] + :param root_key: The root key of the response JSON. + :type root_key: Optional[str] + :param override_api_version: The API version to use. + :type override_api_version: Optional[str] + :return: An instance of `klass` constructed from the response JSON. + :rtype: Any + """ + # Prepare properties for the request + params = params or {} + params = self._objects_to_ids(param=params) # TODO: Why passing a whole dict in here? + + api_version = override_api_version or self._api_version + + full_url = f"{self.api_base_url}/{endpoint}" + + headers = self._build_headers(api_version=api_version) + + # Run the request hook + request_uuid = uuid.uuid4() + request_timestamp = datetime.now(timezone.utc) + self._request_hook( + request_uuid=request_uuid, + request_timestamp=request_timestamp, + method=method, + url=full_url, + headers=headers, + params=params, + ) + + # Make the request + if self._request_engine == RequestEngine.URLFETCH: + http_body, http_status, http_headers = self._request_with_urlfetch( + method=method, + abs_url=full_url, + headers=headers, + params=params, + ) + elif self._request_engine == RequestEngine.REQUESTS: + http_body, http_status, http_headers = self._request_with_requests( + method=method, + abs_url=full_url, + headers=headers, + params=params, + ) + else: + raise EasyPostError(INVALID_REQUEST_LIB_ERROR.format(self._request_engine, SUPPORT_EMAIL)) + + # Run the response hook + response_timestamp = datetime.now(timezone.utc) + self._response_hook( + request_uuid=request_uuid, + request_timestamp=request_timestamp, + response_timestamp=response_timestamp, + method=method, + url=full_url, + headers=http_headers, + response_body=http_body, + response_status=http_status, + ) + + # Interpret the response + valid_response_json = self._interpret_response(response_body=http_body, response_status=http_status) + + # Any HTTP error would have been handled at this point, so we can deserialize the response into an object now + if root_key: + try: + valid_response_json = valid_response_json[root_key] + except KeyError: + raise JsonError(INVALID_RESPONSE_BODY_ERROR.format(http_status, http_body)) + + return klass(**valid_response_json) diff --git a/easypost/easypost_object.py b/easypost/easypost_object.py index 61bb09d5..6fd26430 100644 --- a/easypost/easypost_object.py +++ b/easypost/easypost_object.py @@ -1,56 +1,14 @@ -import importlib -import json -import re +from datetime import datetime from typing import ( Any, Dict, - List, Optional, ) -from easypost.constant import NO_ATTRIBUTE_ERROR - - -EASYPOST_OBJECT_ID_PREFIX_TO_CLASS_NAME_MAP: Dict[str, Any] = { - "adr": "Address", - "ak": "ApiKey", - "batch": "Batch", - "brd": "Brand", - "ca": "CarrierAccount", - "cfrep": "Report", - "cstinfo": "CustomsInfo", - "cstitem": "CustomsItem", - "es": "EndShipper", - "evt": "Event", - "hook": "Webhook", - "ins": "Insurance", - "order": "Order", - "payload": "Payload", - "pickup": "Pickup", - "pickuprate": "PickupRate", - "pl": "PostageLabel", - "plrep": "Report", - "prcl": "Parcel", - "rate": "Rate", - "refrep": "Report", - "rfnd": "Refund", - "sf": "ScanForm", - "shp": "Shipment", - "shpinvrep": "Report", - "shprep": "Report", - "trk": "Tracker", - "trkrep": "Report", - "user": "User", -} - -OBJECT_CLASS_NAME_OVERRIDES: Dict[str, Any] = { - "CashFlowReport": "Report", - "PaymentLogReport": "Report", - "RefundReport": "Report", - "ShipmentInvoiceReport": "Report", - "ShipmentReport": "Report", - "TrackerReport": "Report", -} +from pydantic import ( + BaseModel, + Field, +) def convert_to_easypost_object( @@ -58,148 +16,43 @@ def convert_to_easypost_object( parent: object = None, name: Optional[str] = None, ): - """Convert a response to an EasyPostObject.""" - if isinstance(response, list): - return [convert_to_easypost_object(response=item, parent=parent) for item in response] - elif isinstance(response, dict): - object_type_str = response.get("object", EasyPostObject) - class_name = OBJECT_CLASS_NAME_OVERRIDES.get(object_type_str, EasyPostObject) - object_id = response.get("id") - - if object_id is not None: - # If an object ID is present, use it to find the class type instead. - object_id_prefix = object_id.split("_")[0] - class_name = EASYPOST_OBJECT_ID_PREFIX_TO_CLASS_NAME_MAP.get(object_id_prefix, EasyPostObject) - - # Dynamically import class models due to circular imports of EasyPostObject - class_model = ( - getattr( - importlib.import_module(f'easypost.models.{re.sub(r"(? None: - self.__dict__[k] = v - - if k not in self._immutable_values: - self._values.add(k) - self._unsaved_values.add(k) - cur = self - cur_parent = self._parent - - while cur_parent: - if cur._name: - cur_parent._unsaved_values.add(cur._name) - cur = cur_parent - cur_parent = cur._parent - - def __getattr__(self, k) -> Any: - try: - return self.__dict__[k] - except KeyError: - pass - raise AttributeError(NO_ATTRIBUTE_ERROR.format(type(self).__name__, k)) - - def __getitem__(self, k): - return self.__dict__[k] - - def get(self, k, default: Any = None) -> Any: - try: - return self[k] - except KeyError: - return default - - def setdefault(self, k, default: Any = None) -> Any: - try: - return self[k] - except KeyError: - self[k] = default - return default - - def __setitem__(self, k, v) -> None: - setattr(self, k, v) + return None - @property - def keys(self) -> List[str]: - return self._values.keys() - @property - def values(self) -> List[Any]: - return self._values.keys() - - @classmethod - def construct_from( - cls, - values: Dict[str, Any], - parent: object = None, - name: Optional[str] = None, - ) -> object: - """Construct an EasyPostObject from values returned by the API.""" - instance = cls(id=values.get("id"), parent=parent, name=name) - instance.convert_each_value(values=values) - - return instance - - def convert_each_value(self, values: Dict[str, Any]) -> None: - """Convert each value of a response into an EasyPostObject.""" - for k, v in sorted(values.items()): - if k == "id" and self.id != v: - self.id = v - if k in self._immutable_values: - continue - self.__dict__[k] = convert_to_easypost_object(response=v, parent=self, name=k) - self._values.add(k) +class EphemeralEasyPostObject(BaseModel): + mode: Optional[str] = Field(None, alias="mode") + object_type: Optional[str] = Field(None, alias="object") - def __repr__(self) -> str: - """String representation of an EasyPostObject.""" - type_string = "" + def __str__(self): + return f"<{self.__class__.__name__} object={self.object_type} mode={self.mode}>" + + +class PaginatedCollection(BaseModel): + has_more: Optional[bool] - if isinstance(self.get("object"), str): - type_string = " %s" % self.get("object").encode("utf8") - json_string = json.dumps(obj=self.to_dict(), sort_keys=True, indent=2, cls=EasyPostObjectEncoder) +class EasyPostObject(EphemeralEasyPostObject): + id: Optional[str] = Field(None, alias="id") + created_at: Optional[datetime] = Field(None, alias="created_at") + updated_at: Optional[datetime] = Field(None, alias="updated_at") - return "<%s%s at %s> JSON: %s" % (type(self).__name__, type_string, hex(id(self)), json_string) + @property + def prefix(self) -> str: + return self.id.split("_")[0] def __str__(self) -> str: - return self.to_json(indent=2) + return f"<{self.__class__.__name__} object={self.object_type} mode={self.mode} id={self.id}>" + + def __repr__(self) -> str: + return self.__str__() def __eq__(self, other) -> bool: if not isinstance(other, EasyPostObject): return False return self.__str__() == other.__str__() - def to_json(self, indent: Optional[int] = None) -> str: - """Convert current object to json string.""" - return json.dumps(obj=self.to_dict(), sort_keys=True, indent=indent, cls=EasyPostObjectEncoder) - def to_dict(self) -> Dict[str, Any]: + # TODO: Re-assess """Convert current object to a dict.""" def _serialize(o): @@ -215,12 +68,3 @@ def _serialize(o): v = _serialize(v) d[k] = v return d - - -class EasyPostObjectEncoder(json.JSONEncoder): - def default(self, obj: Any) -> Any: - """Convert an EasyPostObject to a dict.""" - if isinstance(obj, EasyPostObject): - return obj.to_dict() - else: - return json.JSONEncoder.default(self, o=obj) diff --git a/easypost/hooks/request_hook.py b/easypost/hooks/request_hook.py index 99bca31d..b099589f 100644 --- a/easypost/hooks/request_hook.py +++ b/easypost/hooks/request_hook.py @@ -1,7 +1,16 @@ +import datetime +from typing import Dict +from uuid import UUID + from easypost.hooks import EventHook class RequestHook(EventHook): """An event that gets triggered when an HTTP request begins.""" - pass + method: str + path: str + headers: Dict[str, str] + request_body: Dict[str, str] + request_timestamp: datetime.datetime + request_uuid: UUID diff --git a/easypost/http.py b/easypost/http.py new file mode 100644 index 00000000..d29a6aa3 --- /dev/null +++ b/easypost/http.py @@ -0,0 +1,9 @@ +import enum + + +class HttpMethod(enum.Enum): + GET = "get" + POST = "post" + PUT = "put" + PATCH = "patch" + DELETE = "delete" diff --git a/easypost/models/__init__.py b/easypost/models/__init__.py index 79320a7d..9c0fa90a 100644 --- a/easypost/models/__init__.py +++ b/easypost/models/__init__.py @@ -1,26 +1 @@ # flake8: noqa -from easypost.models.address import Address -from easypost.models.api_key import ApiKey -from easypost.models.batch import Batch -from easypost.models.billing import Billing -from easypost.models.brand import Brand -from easypost.models.carrier_account import CarrierAccount -from easypost.models.customs_info import CustomsInfo -from easypost.models.customs_item import CustomsItem -from easypost.models.end_shipper import EndShipper -from easypost.models.event import Event -from easypost.models.insurance import Insurance -from easypost.models.order import Order -from easypost.models.parcel import Parcel -from easypost.models.payload import Payload -from easypost.models.pickup import Pickup -from easypost.models.pickup_rate import PickupRate -from easypost.models.postage_label import PostageLabel -from easypost.models.rate import Rate -from easypost.models.refund import Refund -from easypost.models.report import Report -from easypost.models.scan_form import ScanForm -from easypost.models.shipment import Shipment -from easypost.models.tracker import Tracker -from easypost.models.user import User -from easypost.models.webhook import Webhook diff --git a/easypost/models/address.py b/easypost/models/address.py index 328841c7..ae7c169b 100644 --- a/easypost/models/address.py +++ b/easypost/models/address.py @@ -1,5 +1,35 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.verifications import Verifications +from pydantic import Field class Address(EasyPostObject): - pass + carrier_facility: Optional[str] = Field(None, alias="carrier_facility") + city: Optional[str] = Field(None, alias="city") + company: Optional[str] = Field(None, alias="company") + country: Optional[str] = Field(None, alias="country") + email: Optional[str] = Field(None, alias="email") + error: Optional[str] = Field(None, alias="error") + federal_tax_id: Optional[str] = Field(None, alias="federal_tax_id") + message: Optional[str] = Field(None, alias="message") + name: Optional[str] = Field(None, alias="name") + phone: Optional[str] = Field(None, alias="phone") + residential: Optional[bool] = Field(None, alias="residential") + state: Optional[str] = Field(None, alias="state") + state_tax_id: Optional[str] = Field(None, alias="state_tax_id") + street1: Optional[str] = Field(None, alias="street1") + street2: Optional[str] = Field(None, alias="street2") + verifications: Optional[Verifications] = Field(None, alias="verifications") + zip: Optional[str] = Field(None, alias="zip") + + +class AddressCollection(PaginatedCollection): + addresses: List[Address] = Field([], alias="addresses") diff --git a/easypost/models/api_key.py b/easypost/models/api_key.py index e35b14e5..4bc57958 100644 --- a/easypost/models/api_key.py +++ b/easypost/models/api_key.py @@ -1,5 +1,16 @@ +from typing import ( + List, + Optional, +) + from easypost.easypost_object import EasyPostObject +from pydantic import Field class ApiKey(EasyPostObject): - pass + key: Optional[str] = Field(None, alias="key") + + +class ApiKeyCollection(EasyPostObject): + children: Optional[List["ApiKeyCollection"]] = Field(None, alias="children") + keys: List[ApiKey] = Field(None, alias="keys") diff --git a/easypost/models/batch.py b/easypost/models/batch.py index 44b4ace3..97ccbfe5 100644 --- a/easypost/models/batch.py +++ b/easypost/models/batch.py @@ -1,5 +1,29 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + Dict, + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.batch_shipment import BatchShipment +from easypost.models.scan_form import ScanForm +from pydantic import Field class Batch(EasyPostObject): - pass + error: Optional[str] = Field(None, alias="error") + label_url: Optional[str] = Field(None, alias="label_url") + message: Optional[str] = Field(None, alias="message") + num_shipments: Optional[int] = Field(None, alias="num_shipments") + reference: Optional[str] = Field(None, alias="reference") + scan_form: Optional[ScanForm] = Field(None, alias="scan_form") + shipments: Optional[List[BatchShipment]] = Field(None, alias="shipments") + state: Optional[str] = Field(None, alias="state") + status: Optional[Dict[str, int]] = Field(None, alias="status") + + +class BatchCollection(PaginatedCollection): + batches: List[Batch] = Field([], alias="batches") diff --git a/easypost/models/batch_shipment.py b/easypost/models/batch_shipment.py new file mode 100644 index 00000000..d9d210cd --- /dev/null +++ b/easypost/models/batch_shipment.py @@ -0,0 +1,10 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class BatchShipment(EasyPostObject): + batch_message: Optional[str] = Field(None, alias="batch_message") + batch_status: Optional[str] = Field(None, alias="batch_status") + tracking_code: Optional[str] = Field(None, alias="tracking_code") diff --git a/easypost/models/beta/__init__.py b/easypost/models/beta/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/easypost/models/beta/stateless_rate.py b/easypost/models/beta/stateless_rate.py new file mode 100644 index 00000000..d88d48f7 --- /dev/null +++ b/easypost/models/beta/stateless_rate.py @@ -0,0 +1,11 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Fee(EasyPostObject): + amount: Optional[float] = Field(None, alias="amount") + charged: Optional[bool] = Field(None, alias="charged") + refunded: Optional[bool] = Field(None, alias="refunded") + type: Optional[str] = Field(None, alias="type") diff --git a/easypost/models/billing.py b/easypost/models/billing.py deleted file mode 100644 index 57d2528f..00000000 --- a/easypost/models/billing.py +++ /dev/null @@ -1,5 +0,0 @@ -from easypost.easypost_object import EasyPostObject - - -class Billing(EasyPostObject): - pass diff --git a/easypost/models/brand.py b/easypost/models/brand.py index c3df6af0..db1f81e2 100644 --- a/easypost/models/brand.py +++ b/easypost/models/brand.py @@ -1,5 +1,16 @@ +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class Brand(EasyPostObject): - pass + ad: Optional[str] = Field(None, alias="ad") + ad_href: Optional[str] = Field(None, alias="ad_href") + background_color: Optional[str] = Field(None, alias="background_color") + color: Optional[str] = Field(None, alias="color") + logo: Optional[str] = Field(None, alias="logo") + logo_href: Optional[str] = Field(None, alias="logo_href") + name: Optional[str] = Field(None, alias="name") + theme: Optional[str] = Field(None, alias="theme") + user_id: Optional[str] = Field(None, alias="user_id") diff --git a/easypost/models/carrier_account.py b/easypost/models/carrier_account.py index 6275651c..519fe39b 100644 --- a/easypost/models/carrier_account.py +++ b/easypost/models/carrier_account.py @@ -1,5 +1,20 @@ +from typing import ( + Dict, + Optional, +) + from easypost.easypost_object import EasyPostObject +from easypost.models.carrier_fields import CarrierFields +from pydantic import Field class CarrierAccount(EasyPostObject): - pass + billing_type: Optional[str] = Field(None, alias="billing_type") + clone: Optional[bool] = Field(None, alias="clone") + credentials: Optional[Dict[str, object]] = Field(None, alias="credentials") + description: Optional[str] = Field(None, alias="description") + fields: Optional[CarrierFields] = Field(None, alias="fields") + readable: Optional[str] = Field(None, alias="readable") + reference: Optional[str] = Field(None, alias="reference") + test_credentials: Optional[Dict[str, object]] = Field(None, alias="test_credentials") + type: Optional[str] = Field(None, alias="type") diff --git a/easypost/models/carrier_account_type.py b/easypost/models/carrier_account_type.py new file mode 100644 index 00000000..0e528ae1 --- /dev/null +++ b/easypost/models/carrier_account_type.py @@ -0,0 +1,7 @@ +import enum + + +class CarrierAccountType(enum.Enum): + FedEx = "FedExAccount" + FedExSmartPost = "FedExSmartpostAccount" + UPS = "UPSAccount" diff --git a/easypost/models/carrier_detail.py b/easypost/models/carrier_detail.py new file mode 100644 index 00000000..b0a349d1 --- /dev/null +++ b/easypost/models/carrier_detail.py @@ -0,0 +1,20 @@ +from datetime import datetime +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.tracking_location import TrackingLocation +from pydantic import Field + + +class CarrierDetail(EasyPostObject): + alternate_identifier: Optional[str] = Field(None, alias="alternate_identifier") + container_type: Optional[str] = Field(None, alias="container_type") + destination_location: Optional[str] = Field(None, alias="destination_location") + destination_tracking_location: Optional[TrackingLocation] = Field(None, alias="destination_tracking_location") + est_delivery_date_local: Optional[str] = Field(None, alias="est_delivery_date_local") + est_delivery_time_local: Optional[str] = Field(None, alias="est_delivery_time_local") + guaranteed_delivery_date: Optional[datetime] = Field(None, alias="guaranteed_delivery_date") + initial_delivery_attempt: Optional[datetime] = Field(None, alias="initial_delivery_attempt") + origin_location: Optional[str] = Field(None, alias="origin_location") + origin_tracking_location: Optional[TrackingLocation] = Field(None, alias="origin_tracking_location") + service: Optional[str] = Field(None, alias="service") diff --git a/easypost/models/carrier_fields.py b/easypost/models/carrier_fields.py new file mode 100644 index 00000000..10575855 --- /dev/null +++ b/easypost/models/carrier_fields.py @@ -0,0 +1,18 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class CarrierField(EasyPostObject): + key: Optional[str] = Field(None, alias="key") + visibility: Optional[str] = Field(None, alias="visibility") + label: Optional[str] = Field(None, alias="label") + value: Optional[str] = Field(None, alias="value") + + +class CarrierFields(EasyPostObject): + credentials: Optional[CarrierField] = Field(None, alias="credentials") + test_credentials: Optional[CarrierField] = Field(None, alias="test_credentials") + auto_link: Optional[bool] = Field(None, alias="auto_link") + custom_workflow: Optional[bool] = Field(None, alias="custom_workflow") diff --git a/easypost/models/carrier_metadata.py b/easypost/models/carrier_metadata.py new file mode 100644 index 00000000..1cc810bf --- /dev/null +++ b/easypost/models/carrier_metadata.py @@ -0,0 +1,62 @@ +import enum +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class CarrierMetadataType(enum.Enum): + ServiceLevels = "service_levels" + PredefinedPackages = "predefined_packages" + ShipmentOptions = "shipment_options" + SupportedFeatures = "supported_features" + + +class PredefinedPackage(EasyPostObject): + carrier: Optional[str] = Field(None, alias="carrier") + description: Optional[str] = Field(None, alias="description") + dimensions: Optional[List[str]] = Field(None, alias="dimensions") + human_readable: Optional[str] = Field(None, alias="human_readable") + max_weight: Optional[str] = Field(None, alias="max_weight") + name: Optional[str] = Field(None, alias="name") + + +class ServiceLevel(EasyPostObject): + carrier: Optional[str] = Field(None, alias="carrier") + description: Optional[str] = Field(None, alias="description") + dimensions: Optional[List[str]] = Field(None, alias="dimensions") + human_readable: Optional[str] = Field(None, alias="human_readable") + max_weight: Optional[str] = Field(None, alias="max_weight") + name: Optional[str] = Field(None, alias="name") + + +class ShipmentOption(EasyPostObject): + carrier: Optional[str] = Field(None, alias="carrier") + deprecated: Optional[bool] = Field(None, alias="deprecated") + description: Optional[str] = Field(None, alias="description") + human_readable: Optional[str] = Field(None, alias="human_readable") + name: Optional[str] = Field(None, alias="name") + type: Optional[str] = Field(None, alias="type") + + +class SupportedFeature(EasyPostObject): + carrier: Optional[str] = Field(None, alias="carrier") + description: Optional[str] = Field(None, alias="description") + name: Optional[str] = Field(None, alias="name") + supported: Optional[bool] = Field(None, alias="supported") + + +class Carrier(EasyPostObject): + name: Optional[str] = Field(None, alias="name") + human_readable: Optional[str] = Field(None, alias="human_readable") + predefined_packages: Optional[List[PredefinedPackage]] = Field(None, alias="predefined_packages") + service_levels: Optional[List[ServiceLevel]] = Field(None, alias="service_levels") + shipment_options: Optional[List[ShipmentOption]] = Field(None, alias="shipment_options") + supported_features: Optional[List[SupportedFeature]] = Field(None, alias="supported_features") + + +class CarrierMetadata(EasyPostObject): + carriers: Optional[List[Carrier]] = Field(None, alias="carriers") diff --git a/easypost/models/carrier_type.py b/easypost/models/carrier_type.py new file mode 100644 index 00000000..95145e2e --- /dev/null +++ b/easypost/models/carrier_type.py @@ -0,0 +1,14 @@ +from typing import ( + Dict, + Optional, +) + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class CarrierType(EasyPostObject): + fields: Optional[Dict[str, any]] = Field(None, alias="fields") + logo: Optional[str] = Field(None, alias="logo") + readable: Optional[str] = Field(None, alias="readable") + type: Optional[str] = Field(None, alias="type") diff --git a/easypost/models/customs_info.py b/easypost/models/customs_info.py index 9b7ebd98..ff51ffe5 100644 --- a/easypost/models/customs_info.py +++ b/easypost/models/customs_info.py @@ -1,5 +1,21 @@ +from typing import ( + List, + Optional, +) + from easypost.easypost_object import EasyPostObject +from easypost.models.customs_item import CustomsItem +from pydantic import Field class CustomsInfo(EasyPostObject): - pass + contents_explanation: Optional[str] = Field(None, alias="contents_explanation") + contents_type: Optional[str] = Field(None, alias="contents_type") + customs_certify: Optional[bool] = Field(None, alias="customs_certify") + customs_items: Optional[List[CustomsItem]] = Field(None, alias="customs_items") + customs_signer: Optional[str] = Field(None, alias="customs_signer") + declaration: Optional[str] = Field(None, alias="declaration") + eel_pfc: Optional[str] = Field(None, alias="eel_pfc") + non_delivery_option: Optional[str] = Field(None, alias="non_delivery_option") + restriction_comments: Optional[str] = Field(None, alias="restriction_comments") + restriction_type: Optional[str] = Field(None, alias="restriction_type") diff --git a/easypost/models/customs_item.py b/easypost/models/customs_item.py index 6b29b123..72f6238d 100644 --- a/easypost/models/customs_item.py +++ b/easypost/models/customs_item.py @@ -1,5 +1,15 @@ +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class CustomsItem(EasyPostObject): - pass + code: Optional[str] = Field(None, alias="code") + currency: Optional[str] = Field(None, alias="currency") + description: Optional[str] = Field(None, alias="description") + hs_tariff_number: Optional[str] = Field(None, alias="hs_tariff_number") + origin_country: Optional[str] = Field(None, alias="origin_country") + quantity: Optional[int] = Field(None, alias="quantity") + value: Optional[float] = Field(None, alias="value") + weight: Optional[float] = Field(None, alias="weight") diff --git a/easypost/models/end_shipper.py b/easypost/models/end_shipper.py index 7968f43d..2a18f6c8 100644 --- a/easypost/models/end_shipper.py +++ b/easypost/models/end_shipper.py @@ -1,5 +1,29 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from pydantic import Field class EndShipper(EasyPostObject): - pass + city: Optional[str] = Field(None, alias="city") + company: Optional[str] = Field(None, alias="company") + country: Optional[str] = Field(None, alias="country") + email: Optional[str] = Field(None, alias="email") + error: Optional[str] = Field(None, alias="error") + message: Optional[str] = Field(None, alias="message") + name: Optional[str] = Field(None, alias="name") + phone: Optional[str] = Field(None, alias="phone") + state: Optional[str] = Field(None, alias="state") + street1: Optional[str] = Field(None, alias="street1") + street2: Optional[str] = Field(None, alias="street2") + zip: Optional[str] = Field(None, alias="zip") + + +class EndShipperCollection(PaginatedCollection): + end_shippers: List[EndShipper] = Field([], alias="end_shippers") diff --git a/easypost/models/error.py b/easypost/models/error.py new file mode 100644 index 00000000..437f44d0 --- /dev/null +++ b/easypost/models/error.py @@ -0,0 +1,43 @@ +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Error(EasyPostObject): + code: Optional[str] = Field(None, alias="code") + errors: Optional[List["Error"]] = Field(None, alias="errors") + field: Optional[str] = Field(None, alias="field") + suggestion: Optional[str] = Field(None, alias="suggestion") + raw_message: Optional[object] = Field(None, alias="message") + + @property + def message(self): + # This parses the message from the API response and returns it as a string regardless of what type it + # actually is. + messages = self.collect_error_messages(self.raw_message, []) + if len(messages) == 0: + return None + elif len(messages) == 1: + return messages[0] + else: + return ", ".join(messages) + + @staticmethod + def _collect_error_messages(element, collected_messages): + if element is None: + return collected_messages + elif isinstance(element, list): + for item in element: + collected_messages = Error.collect_error_messages(item, collected_messages) + elif isinstance(element, dict): + for value in element.values(): + collected_messages = Error.collect_error_messages(value, collected_messages) + else: + as_string = str(element) + if as_string.strip(): + collected_messages.append(as_string) + return collected_messages diff --git a/easypost/models/event.py b/easypost/models/event.py index afe441b2..58555501 100644 --- a/easypost/models/event.py +++ b/easypost/models/event.py @@ -1,5 +1,25 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + Dict, + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from pydantic import Field class Event(EasyPostObject): - pass + completed_urls: Optional[List[str]] = Field(None, alias="completed_urls") + description: Optional[str] = Field(None, alias="description") + pending_urls: Optional[List[str]] = Field(None, alias="pending_urls") + previous_attributes: Optional[Dict[str, object]] = Field(None, alias="previous_attributes") + result: Optional[Dict[str, object]] = Field(None, alias="result") + status: Optional[str] = Field(None, alias="status") + user_id: Optional[str] = Field(None, alias="user_id") + + +class EventCollection(PaginatedCollection): + events: List[Event] = Field([], alias="events") diff --git a/easypost/models/fee.py b/easypost/models/fee.py new file mode 100644 index 00000000..d88d48f7 --- /dev/null +++ b/easypost/models/fee.py @@ -0,0 +1,11 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Fee(EasyPostObject): + amount: Optional[float] = Field(None, alias="amount") + charged: Optional[bool] = Field(None, alias="charged") + refunded: Optional[bool] = Field(None, alias="refunded") + type: Optional[str] = Field(None, alias="type") diff --git a/easypost/models/form.py b/easypost/models/form.py new file mode 100644 index 00000000..d0e57bc3 --- /dev/null +++ b/easypost/models/form.py @@ -0,0 +1,10 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Form(EasyPostObject): + form_type: Optional[str] = Field(None, alias="form_type") + form_url: Optional[str] = Field(None, alias="form_url") + submitted_electronically: Optional[bool] = Field(None, alias="submitted_electronically") diff --git a/easypost/models/insurance.py b/easypost/models/insurance.py index 3392cb3d..9c15aff2 100644 --- a/easypost/models/insurance.py +++ b/easypost/models/insurance.py @@ -1,5 +1,32 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.address import Address +from easypost.models.fee import Fee +from easypost.models.tracker import Tracker +from pydantic import Field class Insurance(EasyPostObject): - pass + amount: Optional[str] = Field(None, alias="amount") + from_address: Optional[Address] = Field(None, alias="from_address") + messages: Optional[List[str]] = Field(None, alias="messages") + provider: Optional[str] = Field(None, alias="provider") + provider_id: Optional[str] = Field(None, alias="provider_id") + reference: Optional[str] = Field(None, alias="reference") + shipment_id: Optional[str] = Field(None, alias="shipment_id") + status: Optional[str] = Field(None, alias="status") + to_address: Optional[Address] = Field(None, alias="to_address") + tracker: Optional[Tracker] = Field(None, alias="tracker") + tracking_code: Optional[str] = Field(None, alias="tracking_code") + fee: Optional[Fee] = Field(None, alias="fee") + + +class InsuranceCollection(PaginatedCollection): + insurances: List[Insurance] = Field([], alias="insurances") diff --git a/easypost/models/message.py b/easypost/models/message.py new file mode 100644 index 00000000..6c0e9389 --- /dev/null +++ b/easypost/models/message.py @@ -0,0 +1,11 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Message(EasyPostObject): + carrier: Optional[str] = Field(None, alias="carrier") + carrier_account_id: Optional[str] = Field(None, alias="carrier_account_id") + text: Optional[str] = Field(None, alias="text") + type: Optional[str] = Field(None, alias="type") diff --git a/easypost/models/options.py b/easypost/models/options.py new file mode 100644 index 00000000..c61b0167 --- /dev/null +++ b/easypost/models/options.py @@ -0,0 +1,111 @@ +from datetime import datetime +from typing import ( + Dict, + List, + Optional, +) + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class Options(EasyPostObject): + additional_handling: Optional[bool] = Field(None, alias="additional_handling") + address_validation_level: Optional[str] = Field(None, alias="address_validation_level") + alcohol: Optional[bool] = Field(None, alias="alcohol") + billing_ref: Optional[str] = Field(None, alias="billing_ref") + bill_receiver_account: Optional[str] = Field(None, alias="bill_receiver_account") # Obsolete. Use Payment instead. + bill_receiver_postal_code: Optional[str] = Field( + None, alias="bill_receiver_postal_code" + ) # Obsolete. Use Payment instead. + bill_third_party_account: Optional[str] = Field( + None, alias="bill_third_party_account" + ) # Obsolete. Use Payment instead. + bill_third_party_country: Optional[str] = Field( + None, alias="bill_third_party_country" + ) # Obsolete. Use Payment instead. + bill_third_party_postal_code: Optional[str] = Field( + None, alias="bill_third_party_postal_code" + ) # Obsolete. Use Payment instead. + by_drone: Optional[bool] = Field(None, alias="by_drone") + carrier_insurance_amount: Optional[str] = Field(None, alias="carrier_insurance_amount") + carrier_notification_email: Optional[str] = Field(None, alias="carrier_notification_email") + carrier_notification_sms: Optional[str] = Field(None, alias="carrier_notification_sms") + certified_mail: Optional[bool] = Field(None, alias="certified_mail") + cod_address_id: Optional[str] = Field(None, alias="cod_address_id") + cod_amount: Optional[str] = Field(None, alias="cod_amount") + cod_method: Optional[str] = Field(None, alias="cod_method") + commercial_invoice_format: Optional[str] = Field(None, alias="commercial_invoice_format") + commercial_invoice_letterhead: Optional[str] = Field(None, alias="commercial_invoice_letterhead") + commercial_invoice_signature: Optional[str] = Field(None, alias="commercial_invoice_signature") + commercial_invoice_size: Optional[str] = Field(None, alias="commercial_invoice_size") + content_description: Optional[str] = Field(None, alias="content_description") + cost_center: Optional[str] = Field(None, alias="cost_center") + currency: Optional[str] = Field(None, alias="currency") + customs_broker_address_id: Optional[str] = Field(None, alias="customs_broker_address_id") + customs_include_shipping: Optional[str] = Field(None, alias="customs_include_shipping") + declared_value: Optional[float] = Field(None, alias="declared_value") + delivered_duty_paid: Optional[bool] = Field(None, alias="delivered_duty_paid") + delivery_confirmation: Optional[str] = Field(None, alias="delivery_confirmation") + delivery_time_preference: Optional[str] = Field(None, alias="delivery_time_preference") + delivery_min_datetime: Optional[str] = Field(None, alias="delivery_min_datetime") + delivery_max_datetime: Optional[str] = Field(None, alias="delivery_max_datetime") + dropoff_max_datetime: Optional[datetime] = Field(None, alias="dropoff_max_datetime") + dropoff_type: Optional[str] = Field(None, alias="dropoff_type") + dry_ice: Optional[bool] = Field(None, alias="dry_ice") + dry_ice_medical: Optional[str] = Field(None, alias="dry_ice_medical") + dry_ice_weight: Optional[str] = Field(None, alias="dry_ice_weight") + duty_payment: Optional[Dict[str, object]] = Field(None, alias="duty_payment") + duty_payment_account: Optional[str] = Field(None, alias="duty_payment_account") + endorsement: Optional[str] = Field(None, alias="endorsement") + end_shipper_id: Optional[str] = Field(None, alias="end_shipper_id") + freight_charge: Optional[str] = Field(None, alias="freight_charge") + group: Optional[str] = Field(None, alias="group") + handling_instructions: Optional[str] = Field(None, alias="handling_instructions") + hazmat: Optional[str] = Field(None, alias="hazmat") + hold_for_pickup: Optional[bool] = Field(None, alias="hold_for_pickup") + image_format: Optional[str] = Field(None, alias="image_format") + importer_address_id: Optional[str] = Field(None, alias="importer_address_id") + import_federal_tax_id: Optional[str] = Field(None, alias="import_federal_tax_id") + import_state_tax_id: Optional[str] = Field(None, alias="import_state_tax_id") + incoterm: Optional[str] = Field(None, alias="incoterm") + invoice_number: Optional[str] = Field(None, alias="invoice_number") + label_date: Optional[datetime] = Field(None, alias="label_date") + label_format: Optional[str] = Field(None, alias="label_format") + label_size: Optional[str] = Field(None, alias="label_size") + license_number: Optional[str] = Field(None, alias="license_number") + machinable: Optional[str] = Field(None, alias="machinable") + neutral_delivery: Optional[bool] = Field(None, alias="neutral_delivery") + non_contract: Optional[bool] = Field(None, alias="non_contract") + overlabel_construct_code: Optional[str] = Field(None, alias="overlabel_construct_code") + overlabel_original_tracking_number: Optional[str] = Field(None, alias="overlabel_original_tracking_number") + parties_to_transaction_are_related: Optional[str] = Field(None, alias="parties_to_transaction_are_related") + payment: Optional[Dict[str, object]] = Field(None, alias="payment") + peel_and_return: Optional[bool] = Field(None, alias="peel_and_return") + pickup_max_datetime: Optional[datetime] = Field(None, alias="pickup_max_datetime") + pickup_min_datetime: Optional[datetime] = Field(None, alias="pickup_min_datetime") + po_sort: Optional[str] = Field(None, alias="po_sort") + postage_label_inline: Optional[bool] = Field(None, alias="postage_label_inline") + print_custom: Optional[List[Dict[str, object]]] = Field(None, alias="print_custom") + print_custom_1: Optional[str] = Field(None, alias="print_custom_1") + print_custom_1_barcode: Optional[bool] = Field(None, alias="print_custom_1_barcode") + print_custom_1_code: Optional[str] = Field(None, alias="print_custom_1_code") + print_custom_2: Optional[str] = Field(None, alias="print_custom_2") + print_custom_2_barcode: Optional[bool] = Field(None, alias="print_custom_2_barcode") + print_custom_2_code: Optional[str] = Field(None, alias="print_custom_2_code") + print_custom_3: Optional[str] = Field(None, alias="print_custom_3") + print_custom_3_barcode: Optional[bool] = Field(None, alias="print_custom_3_barcode") + print_custom_3_code: Optional[str] = Field(None, alias="print_custom_3_code") + print_rate: Optional[bool] = Field(None, alias="print_rate") + receiver_liquor_license: Optional[str] = Field(None, alias="receiver_liquor_license") + registered_mail: Optional[bool] = Field(None, alias="registered_mail") + registered_mail_amount: Optional[float] = Field(None, alias="registered_mail_amount") + return_receipt: Optional[bool] = Field(None, alias="return_receipt") + return_service: Optional[str] = Field(None, alias="return_service") + saturday_delivery: Optional[bool] = Field(None, alias="saturday_delivery") + settlement_method: Optional[str] = Field(None, alias="settlement_method") + smartpost_hub: Optional[str] = Field(None, alias="smartpost_hub") + smartpost_manifest: Optional[str] = Field(None, alias="smartpost_manifest") + special_rates_eligibility: Optional[str] = Field(None, alias="special_rates_eligibility") + suppress_etd: Optional[bool] = Field(None, alias="suppress_etd") + tax_id_expiration_date: Optional[str] = Field(None, alias="tax_id_expiration_date") diff --git a/easypost/models/order.py b/easypost/models/order.py index 538ed99b..e2e68cd0 100644 --- a/easypost/models/order.py +++ b/easypost/models/order.py @@ -4,11 +4,30 @@ ) from easypost.easypost_object import EasyPostObject +from easypost.models.address import Address +from easypost.models.carrier_account import CarrierAccount +from easypost.models.customs_info import CustomsInfo +from easypost.models.message import Message from easypost.models.rate import Rate +from easypost.models.shipment import Shipment from easypost.util import get_lowest_object_rate +from pydantic import Field class Order(EasyPostObject): + buyer_address: Optional[Address] = Field(None, alias="buyer_address") + carrier_accounts: Optional[List[CarrierAccount]] = Field(None, alias="carrier_accounts") + customs_info: Optional[CustomsInfo] = Field(None, alias="customs_info") + from_address: Optional[Address] = Field(None, alias="from_address") + is_return: Optional[bool] = Field(None, alias="is_return") + messages: Optional[List[Message]] = Field(None, alias="messages") + rates: Optional[List[Rate]] = Field(None, alias="rates") + reference: Optional[str] = Field(None, alias="reference") + return_address: Optional[Address] = Field(None, alias="return_address") + service: Optional[str] = Field(None, alias="service") + shipments: Optional[List[Shipment]] = Field(None, alias="shipments") + to_address: Optional[Address] = Field(None, alias="to_address") + def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate: """Get the lowest rate of this Order.""" lowest_rate = get_lowest_object_rate(self, carriers, services) diff --git a/easypost/models/parcel.py b/easypost/models/parcel.py index 3c17f701..3532ba68 100644 --- a/easypost/models/parcel.py +++ b/easypost/models/parcel.py @@ -1,5 +1,12 @@ +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class Parcel(EasyPostObject): - pass + height: Optional[float] = Field(None, alias="height") + length: Optional[float] = Field(None, alias="length") + predefined_package: Optional[str] = Field(None, alias="predefined_package") + weight: Optional[float] = Field(None, alias="weight") + width: Optional[float] = Field(None, alias="width") diff --git a/easypost/models/payload.py b/easypost/models/payload.py index d05268ee..e11b7b4b 100644 --- a/easypost/models/payload.py +++ b/easypost/models/payload.py @@ -1,5 +1,17 @@ +from typing import ( + Dict, + Optional, +) + from easypost.easypost_object import EasyPostObject +from pydantic import Field class Payload(EasyPostObject): - pass + request_url: Optional[str] = Field(None, alias="request_url") + request_headers: Optional[Dict[str, str]] = Field(None, alias="request_headers") + request_body: Optional[str] = Field(None, alias="request_body") + response_headers: Optional[Dict[str, str]] = Field(None, alias="response_headers") + response_body: Optional[str] = Field(None, alias="response_body") + response_code: Optional[int] = Field(None, alias="response_code") + total_time: Optional[float] = Field(None, alias="total_time") diff --git a/easypost/models/payment_method.py b/easypost/models/payment_method.py new file mode 100644 index 00000000..95917349 --- /dev/null +++ b/easypost/models/payment_method.py @@ -0,0 +1,43 @@ +import enum +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class PaymentMethodType(enum.Enum): + CreditCard = "CreditCard" + BankAccount = "BankAccount" + + +class PaymentMethod(EasyPostObject): + bank_name: Optional[str] = Field(None, alias="bank_name") + brand: Optional[str] = Field(None, alias="brand") + country: Optional[str] = Field(None, alias="country") + disabled_at: Optional[str] = Field(None, alias="disabled_at") + expiration_month: Optional[int] = Field(None, alias="exp_month") + expiration_year: Optional[int] = Field(None, alias="exp_year") + last4: Optional[str] = Field(None, alias="last4") + name: Optional[str] = Field(None, alias="name") + verified: Optional[bool] = Field(None, alias="verified") + + @property + def type(self): + _object = self.object + + if _object == "CreditCard": + return PaymentMethodType.CreditCard + elif _object == "BankAccount": + return PaymentMethodType.BankAccount + else: + return None + + @property + def endpoint(self): + _type = self.type + if _type == PaymentMethodType.CreditCard: + return "credit_cards" + elif _type == PaymentMethodType.BankAccount: + return "bank_accounts" + else: + return None diff --git a/easypost/models/payment_methods_summary.py b/easypost/models/payment_methods_summary.py new file mode 100644 index 00000000..0874ff75 --- /dev/null +++ b/easypost/models/payment_methods_summary.py @@ -0,0 +1,16 @@ +import enum +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.payment_method import PaymentMethod +from pydantic import Field + + +class PaymentMethodPriority(enum.Enum): + Primary = "primary" + Secondary = "secondary" + + +class PaymentMethodsSummary(EasyPostObject): + primary: Optional[PaymentMethod] = Field(None, alias="primary_payment_method") + secondary: Optional[PaymentMethod] = Field(None, alias="secondary_payment_method") diff --git a/easypost/models/pickup.py b/easypost/models/pickup.py index a54175b7..db229a3b 100644 --- a/easypost/models/pickup.py +++ b/easypost/models/pickup.py @@ -1,14 +1,33 @@ +from datetime import datetime from typing import ( List, Optional, ) from easypost.easypost_object import EasyPostObject +from easypost.models.address import Address +from easypost.models.carrier_account import CarrierAccount +from easypost.models.message import Message +from easypost.models.pickup_rate import PickupRate from easypost.models.rate import Rate from easypost.util import get_lowest_object_rate +from pydantic import Field class Pickup(EasyPostObject): + address: Optional[Address] = Field(None, alias="address") + carrier_accounts: Optional[List[CarrierAccount]] = Field(None, alias="carrier_accounts") + confirmation: Optional[str] = Field(None, alias="confirmation") + instructions: Optional[str] = Field(None, alias="instructions") + is_account_address: Optional[bool] = Field(None, alias="is_account_address") + max_datetime: Optional[datetime] = Field(None, alias="max_datetime") + messages: Optional[List[Message]] = Field(None, alias="messages") + min_datetime: Optional[datetime] = Field(None, alias="min_datetime") + name: Optional[str] = Field(None, alias="name") + pickup_rates: Optional[List[PickupRate]] = Field(None, alias="pickup_rates") + reference: Optional[str] = Field(None, alias="reference") + status: Optional[str] = Field(None, alias="status") + def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate: """Get the lowest rate of this Pickup.""" lowest_rate = get_lowest_object_rate(self, carriers, services, "pickup_rates") diff --git a/easypost/models/pickup_rate.py b/easypost/models/pickup_rate.py index 0d99dbe7..a21a68e1 100644 --- a/easypost/models/pickup_rate.py +++ b/easypost/models/pickup_rate.py @@ -1,5 +1,8 @@ +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class PickupRate(EasyPostObject): - pass + pickup_id: Optional[str] = Field(None, alias="pickup_id") diff --git a/easypost/models/postage_label.py b/easypost/models/postage_label.py index 1ca23195..8c2ca745 100644 --- a/easypost/models/postage_label.py +++ b/easypost/models/postage_label.py @@ -1,5 +1,20 @@ +from datetime import datetime +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class PostageLabel(EasyPostObject): - pass + date_advance: Optional[int] = Field(None, alias="date_advance") + integrated_form: Optional[str] = Field(None, alias="integrated_form") + label_date: Optional[datetime] = Field(None, alias="label_date") + label_epl2_url: Optional[str] = Field(None, alias="label_epl2_url") + label_file: Optional[str] = Field(None, alias="label_file") + label_file_type: Optional[str] = Field(None, alias="label_file_type") + label_pdf_url: Optional[str] = Field(None, alias="label_pdf_url") + label_resolution: Optional[int] = Field(None, alias="label_resolution") + label_size: Optional[str] = Field(None, alias="label_size") + label_type: Optional[str] = Field(None, alias="label_type") + label_url: Optional[str] = Field(None, alias="label_url") + label_zpl_url: Optional[str] = Field(None, alias="label_zpl_url") diff --git a/easypost/models/rate.py b/easypost/models/rate.py index a9c44cca..12baf332 100644 --- a/easypost/models/rate.py +++ b/easypost/models/rate.py @@ -1,5 +1,23 @@ +from datetime import datetime +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class Rate(EasyPostObject): - pass + billing_type: Optional[str] = Field(None, alias="billing_type") + carrier: Optional[str] = Field(None, alias="carrier") + carrier_account_id: Optional[str] = Field(None, alias="carrier_account_id") + currency: Optional[str] = Field(None, alias="currency") + delivery_date: Optional[datetime] = Field(None, alias="delivery_date") + delivery_date_guaranteed: Optional[bool] = Field(None, alias="delivery_date_guaranteed") + delivery_days: Optional[int] = Field(None, alias="delivery_days") + est_delivery_days: Optional[int] = Field(None, alias="est_delivery_days") + list_currency: Optional[str] = Field(None, alias="list_currency") + list_rate: Optional[str] = Field(None, alias="list_rate") + price: Optional[str] = Field(None, alias="price") + retail_currency: Optional[str] = Field(None, alias="retail_currency") + retail_rate: Optional[str] = Field(None, alias="retail_rate") + service: Optional[str] = Field(None, alias="service") + shipment_id: Optional[str] = Field(None, alias="shipment_id") diff --git a/easypost/models/rate_with_estimated_delivery_date.py b/easypost/models/rate_with_estimated_delivery_date.py new file mode 100644 index 00000000..99d0464c --- /dev/null +++ b/easypost/models/rate_with_estimated_delivery_date.py @@ -0,0 +1,18 @@ +from datetime import datetime +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.rate import Rate +from easypost.models.time_in_transit import TimeInTransit +from pydantic import Field + + +class TimeInTransitDetails(EasyPostObject): + days_in_transit: Optional[TimeInTransit] = Field(None, alias="days_in_transit") + estimated_delivery_date: Optional[datetime] = Field(None, alias="easypost_estimated_delivery_date") + planned_ship_date: Optional[datetime] = Field(None, alias="planned_ship_date") + + +class RateWithEstimatedDeliveryDate(EasyPostObject): + rate: Optional[Rate] = Field(None, alias="rate") + time_in_transit_details: Optional[TimeInTransitDetails] = Field(None, alias="easypost_time_in_transit_data") diff --git a/easypost/models/referral_customer.py b/easypost/models/referral_customer.py new file mode 100644 index 00000000..fb3ef652 --- /dev/null +++ b/easypost/models/referral_customer.py @@ -0,0 +1,35 @@ +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.api_key import ApiKey +from easypost.models.user import User +from pydantic import Field + + +class ReferralCustomer(EasyPostObject): + api_keys: Optional[List[ApiKey]] = Field(None, alias="api_keys") + balance: Optional[str] = Field(None, alias="balance") + children: Optional[List[User]] = Field(None, alias="children") + email: Optional[str] = Field(None, alias="email") + name: Optional[str] = Field(None, alias="name") + parent_id: Optional[str] = Field(None, alias="parent_id") + password: Optional[str] = Field(None, alias="password") + password_confirmation: Optional[str] = Field(None, alias="password_confirmation") + phone_number: Optional[str] = Field(None, alias="phone_number") + price_per_shipment: Optional[str] = Field(None, alias="price_per_shipment") + recharge_amount: Optional[str] = Field(None, alias="recharge_amount") + recharge_threshold: Optional[str] = Field(None, alias="recharge_threshold") + secondary_recharge_amount: Optional[str] = Field(None, alias="secondary_recharge_amount") + convenience_fee_rate: Optional[str] = Field(None, alias="cc_fee_rate") + insurance_fee_rate: Optional[str] = Field(None, alias="insurance_fee_rate") + insurance_fee_minimum: Optional[str] = Field(None, alias="insurance_fee_minimum") + + +class ReferralCustomerCollection(PaginatedCollection): + referral_customers: List[ReferralCustomer] = Field([], alias="referral_customers") diff --git a/easypost/models/refund.py b/easypost/models/refund.py index 4a15ffa3..fb45ae20 100644 --- a/easypost/models/refund.py +++ b/easypost/models/refund.py @@ -1,5 +1,22 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from pydantic import Field class Refund(EasyPostObject): - pass + carrier: Optional[str] = Field(None, alias="carrier") + confirmation_number: Optional[str] = Field(None, alias="confirmation_number") + shipment_id: Optional[str] = Field(None, alias="shipment_id") + status: Optional[str] = Field(None, alias="status") + tracking_code: Optional[str] = Field(None, alias="tracking_code") + + +class RefundCollection(PaginatedCollection): + refunds: List[Refund] = Field([], alias="refunds") diff --git a/easypost/models/report.py b/easypost/models/report.py index 263ad9ce..b19e69ca 100644 --- a/easypost/models/report.py +++ b/easypost/models/report.py @@ -1,5 +1,24 @@ -from easypost.easypost_object import EasyPostObject +from datetime import datetime +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from pydantic import Field class Report(EasyPostObject): - pass + end_date: Optional[datetime] = Field(None, alias="end_date") + include_children: Optional[bool] = Field(None, alias="include_children") + start_date: Optional[datetime] = Field(None, alias="start_date") + status: Optional[str] = Field(None, alias="status") + url: Optional[str] = Field(None, alias="url") + url_expiration: Optional[datetime] = Field(None, alias="url_expires_at") + + +class ReportCollection(PaginatedCollection): + reports: List[Report] = Field([], alias="reports") diff --git a/easypost/models/scan_form.py b/easypost/models/scan_form.py index 56c4a62a..8295001c 100644 --- a/easypost/models/scan_form.py +++ b/easypost/models/scan_form.py @@ -1,5 +1,25 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.address import Address +from pydantic import Field class ScanForm(EasyPostObject): - pass + address: Optional[Address] = Field(None, alias="address") + batch_id: Optional[str] = Field(None, alias="batch_id") + form_file_type: Optional[str] = Field(None, alias="form_file_type") + form_url: Optional[str] = Field(None, alias="form_url") + message: Optional[str] = Field(None, alias="message") + status: Optional[str] = Field(None, alias="status") + tracking_codes: Optional[List[str]] = Field(None, alias="tracking_codes") + + +class ScanFormCollection(PaginatedCollection): + scan_forms: List[ScanForm] = Field([], alias="scan_forms") diff --git a/easypost/models/shipment.py b/easypost/models/shipment.py index c8f9bef3..c28df65f 100644 --- a/easypost/models/shipment.py +++ b/easypost/models/shipment.py @@ -3,14 +3,64 @@ Optional, ) -from easypost.easypost_object import EasyPostObject +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.address import Address +from easypost.models.carrier_account import CarrierAccount +from easypost.models.customs_info import CustomsInfo +from easypost.models.fee import Fee +from easypost.models.form import Form +from easypost.models.message import Message +from easypost.models.options import Options +from easypost.models.parcel import Parcel +from easypost.models.postage_label import PostageLabel from easypost.models.rate import Rate +from easypost.models.scan_form import ScanForm +from easypost.models.tax_identifier import TaxIdentifier +from easypost.models.tracker import Tracker from easypost.util import get_lowest_object_rate +from pydantic import Field class Shipment(EasyPostObject): + batch_id: Optional[str] = Field(None, alias="batch_id") + batch_message: Optional[str] = Field(None, alias="batch_message") + batch_status: Optional[str] = Field(None, alias="batch_status") + buyer_address: Optional[Address] = Field(None, alias="buyer_address") + carrier_accounts: Optional[List[CarrierAccount]] = Field(None, alias="carrier_accounts") + customs_info: Optional[CustomsInfo] = Field(None, alias="customs_info") + fees: Optional[List[Fee]] = Field(None, alias="fees") + forms: Optional[List[Form]] = Field(None, alias="forms") + from_address: Optional[Address] = Field(None, alias="from_address") + insurance: Optional[str] = Field(None, alias="insurance") + is_return: Optional[bool] = Field(None, alias="is_return") + messages: Optional[List[Message]] = Field(None, alias="messages") + options: Optional[Options] = Field(None, alias="options") + order_id: Optional[str] = Field(None, alias="order_id") + parcel: Optional[Parcel] = Field(None, alias="parcel") + postage_label: Optional[PostageLabel] = Field(None, alias="postage_label") + rates: Optional[List[Rate]] = Field(None, alias="rates") + reference: Optional[str] = Field(None, alias="reference") + refund_status: Optional[str] = Field(None, alias="refund_status") + return_address: Optional[Address] = Field(None, alias="return_address") + scan_form: Optional[ScanForm] = Field(None, alias="scan_form") + selected_rate: Optional[Rate] = Field(None, alias="selected_rate") + service: Optional[str] = Field(None, alias="service") + status: Optional[str] = Field(None, alias="status") + tax_identifiers: Optional[List[TaxIdentifier]] = Field(None, alias="tax_identifiers") + to_address: Optional[Address] = Field(None, alias="to_address") + tracker: Optional[Tracker] = Field(None, alias="tracker") + tracking_code: Optional[str] = Field(None, alias="tracking_code") + usps_zone: Optional[str] = Field(None, alias="usps_zone") + def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate: """Get the lowest rate of this shipment.""" lowest_rate = get_lowest_object_rate(self, carriers, services) return lowest_rate + + +class ShipmentCollection(PaginatedCollection): + shipments: List[Shipment] = Field([], alias="shipments") diff --git a/easypost/models/smart_rate.py b/easypost/models/smart_rate.py new file mode 100644 index 00000000..10147469 --- /dev/null +++ b/easypost/models/smart_rate.py @@ -0,0 +1,25 @@ +from datetime import datetime +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.time_in_transit import TimeInTransit +from pydantic import Field + + +class SmartRate(EasyPostObject): + billing_type: Optional[str] = Field(None, alias="billing_type") + carrier: Optional[str] = Field(None, alias="carrier") + carrier_account_id: Optional[str] = Field(None, alias="carrier_account_id") + currency: Optional[str] = Field(None, alias="currency") + delivery_date: Optional[datetime] = Field(None, alias="delivery_date") + delivery_date_guaranteed: Optional[bool] = Field(None, alias="delivery_date_guaranteed") + delivery_days: Optional[int] = Field(None, alias="delivery_days") + est_delivery_days: Optional[int] = Field(None, alias="est_delivery_days") + list_currency: Optional[str] = Field(None, alias="list_currency") + list_rate: Optional[str] = Field(None, alias="list_rate") + rate: Optional[float] = Field(None, alias="rate") + retail_currency: Optional[str] = Field(None, alias="retail_currency") + retail_rate: Optional[str] = Field(None, alias="retail_rate") + service: Optional[str] = Field(None, alias="service") + shipment_id: Optional[str] = Field(None, alias="shipment_id") + time_in_transit: Optional[TimeInTransit] = Field(None, alias="time_in_transit") diff --git a/easypost/models/smart_rate_accuracy.py b/easypost/models/smart_rate_accuracy.py new file mode 100644 index 00000000..d327dded --- /dev/null +++ b/easypost/models/smart_rate_accuracy.py @@ -0,0 +1,11 @@ +import enum + + +class SmartRateAccuracy(enum.Enum): + Percentile50 = ("percentile_50",) + Percentile75 = ("percentile_75",) + Percentile85 = ("percentile_85",) + Percentile90 = ("percentile_90",) + Percentile95 = ("percentile_95",) + Percentile97 = ("percentile_97",) + Percentile99 = "percentile_99" diff --git a/easypost/models/sort_direction.py b/easypost/models/sort_direction.py new file mode 100644 index 00000000..fa272c10 --- /dev/null +++ b/easypost/models/sort_direction.py @@ -0,0 +1,6 @@ +import enum + + +class SortDirection(enum.Enum): + Ascending = "asc" + Descending = "desc" diff --git a/easypost/models/tax_identifier.py b/easypost/models/tax_identifier.py new file mode 100644 index 00000000..b63d5dcb --- /dev/null +++ b/easypost/models/tax_identifier.py @@ -0,0 +1,11 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class TaxIdentifier(EasyPostObject): + entity: Optional[str] = Field(None, alias="entity") + issuing_country: Optional[str] = Field(None, alias="issuing_country") + tax_id: Optional[str] = Field(None, alias="tax_id") + tax_id_type: Optional[str] = Field(None, alias="tax_id_type") diff --git a/easypost/models/time_in_transit.py b/easypost/models/time_in_transit.py new file mode 100644 index 00000000..01325a00 --- /dev/null +++ b/easypost/models/time_in_transit.py @@ -0,0 +1,35 @@ +from typing import ( + Optional, + Union, +) + +from easypost.easypost_object import EasyPostObject +from easypost.models.smart_rate_accuracy import SmartRateAccuracy +from pydantic import Field + + +class TimeInTransit(EasyPostObject): + percentile_50: Optional[int] = Field(None, alias="percentile_50") + percentile_75: Optional[int] = Field(None, alias="percentile_75") + percentile_85: Optional[int] = Field(None, alias="percentile_85") + percentile_90: Optional[int] = Field(None, alias="percentile_90") + percentile_95: Optional[int] = Field(None, alias="percentile_95") + percentile_97: Optional[int] = Field(None, alias="percentile_97") + percentile_99: Optional[int] = Field(None, alias="percentile_99") + + def get_by_smart_rate_accuracy(self, smart_rate_accuracy: SmartRateAccuracy) -> Union[Optional[int], None]: + if smart_rate_accuracy == SmartRateAccuracy.Percentile50: + return self.percentile_50 + if smart_rate_accuracy == SmartRateAccuracy.Percentile75: + return self.percentile_75 + if smart_rate_accuracy == SmartRateAccuracy.Percentile85: + return self.percentile_85 + if smart_rate_accuracy == SmartRateAccuracy.Percentile90: + return self.percentile_90 + if smart_rate_accuracy == SmartRateAccuracy.Percentile95: + return self.percentile_95 + if smart_rate_accuracy == SmartRateAccuracy.Percentile97: + return self.percentile_97 + if smart_rate_accuracy == SmartRateAccuracy.Percentile99: + return self.percentile_99 + return None diff --git a/easypost/models/tracker.py b/easypost/models/tracker.py index bc09bdbf..2d533627 100644 --- a/easypost/models/tracker.py +++ b/easypost/models/tracker.py @@ -1,5 +1,34 @@ -from easypost.easypost_object import EasyPostObject +from datetime import datetime +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.carrier_detail import CarrierDetail +from easypost.models.fee import Fee +from easypost.models.tracking_detail import TrackingDetail +from pydantic import Field class Tracker(EasyPostObject): - pass + carrier: Optional[str] = Field(None, alias="carrier") + carrier_detail: Optional[CarrierDetail] = Field(None, alias="carrier_detail") + est_delivery_date: Optional[datetime] = Field(None, alias="est_delivery_date") + fees: Optional[List[Fee]] = Field(None, alias="fees") + public_url: Optional[str] = Field(None, alias="public_url") + shipment_id: Optional[str] = Field(None, alias="shipment_id") + signed_by: Optional[str] = Field(None, alias="signed_by") + status: Optional[str] = Field(None, alias="status") + status_detail: Optional[str] = Field(None, alias="status_detail") + tracking_code: Optional[str] = Field(None, alias="tracking_code") + tracking_details: Optional[List[TrackingDetail]] = Field(None, alias="tracking_details") + tracking_updated_at: Optional[datetime] = Field(None, alias="tracking_updated_at") + weight: Optional[float] = Field(None, alias="weight") + + +class TrackerCollection(PaginatedCollection): + trackers: list[Tracker] = Field([], alias="trackers") diff --git a/easypost/models/tracking_detail.py b/easypost/models/tracking_detail.py new file mode 100644 index 00000000..8dbe9940 --- /dev/null +++ b/easypost/models/tracking_detail.py @@ -0,0 +1,15 @@ +from datetime import datetime +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.tracking_location import TrackingLocation +from pydantic import Field + + +class TrackingDetail(EasyPostObject): + date_time: Optional[datetime] = Field(None, alias="datetime") + message: Optional[str] = Field(None, alias="message") + source: Optional[str] = Field(None, alias="source") + status: Optional[str] = Field(None, alias="status") + status_detail: Optional[str] = Field(None, alias="status_detail") + tracking_location: Optional[TrackingLocation] = Field(None, alias="tracking_location") diff --git a/easypost/models/tracking_location.py b/easypost/models/tracking_location.py new file mode 100644 index 00000000..80490b12 --- /dev/null +++ b/easypost/models/tracking_location.py @@ -0,0 +1,11 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class TrackingLocation(EasyPostObject): + city: Optional[str] = Field(None, alias="city") + country: Optional[str] = Field(None, alias="country") + state: Optional[str] = Field(None, alias="state") + zip: Optional[str] = Field(None, alias="zip") diff --git a/easypost/models/user.py b/easypost/models/user.py index 92e447a2..1391e5ee 100644 --- a/easypost/models/user.py +++ b/easypost/models/user.py @@ -1,5 +1,35 @@ -from easypost.easypost_object import EasyPostObject +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import ( + EasyPostObject, + PaginatedCollection, +) +from easypost.models.api_key import ApiKey +from pydantic import Field class User(EasyPostObject): - pass + api_keys: Optional[List[ApiKey]] = Field(None, alias="api_keys") + balance: Optional[str] = Field(None, alias="balance") + children: Optional[List["User"]] = Field(None, alias="children") + email: Optional[str] = Field(None, alias="email") + name: Optional[str] = Field(None, alias="name") + parent_id: Optional[str] = Field(None, alias="parent_id") + password: Optional[str] = Field(None, alias="password") + password_confirmation: Optional[str] = Field(None, alias="password_confirmation") + phone_number: Optional[str] = Field(None, alias="phone_number") + price_per_shipment: Optional[str] = Field(None, alias="price_per_shipment") + recharge_amount: Optional[str] = Field(None, alias="recharge_amount") + recharge_threshold: Optional[str] = Field(None, alias="recharge_threshold") + secondary_recharge_amount: Optional[str] = Field(None, alias="secondary_recharge_amount") + convenience_fee_rate: Optional[str] = Field(None, alias="cc_fee_rate") + insurance_fee_rate: Optional[str] = Field(None, alias="insurance_fee_rate") + insurance_fee_minimum: Optional[str] = Field(None, alias="insurance_fee_minimum") + verified: Optional[bool] = Field(None, alias="verified") + + +class ChildUserCollection(PaginatedCollection): + children: List[User] = Field([], alias="children") diff --git a/easypost/models/verification.py b/easypost/models/verification.py new file mode 100644 index 00000000..d592e71a --- /dev/null +++ b/easypost/models/verification.py @@ -0,0 +1,15 @@ +from typing import ( + List, + Optional, +) + +from easypost.easypost_object import EasyPostObject +from easypost.models.error import Error +from easypost.models.verification_details import VerificationDetails +from pydantic import Field + + +class Verification(EasyPostObject): + details: Optional[VerificationDetails] = Field(None, alias="details") + errors: Optional[List[Error]] = Field(None, alias="errors") + success: Optional[bool] = Field(None, alias="success") diff --git a/easypost/models/verification_details.py b/easypost/models/verification_details.py new file mode 100644 index 00000000..ce083ab5 --- /dev/null +++ b/easypost/models/verification_details.py @@ -0,0 +1,10 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from pydantic import Field + + +class VerificationDetails(EasyPostObject): + latitude: Optional[float] = Field(None, alias="latitude") + longitude: Optional[float] = Field(None, alias="longitude") + time_zone: Optional[str] = Field(None, alias="time_zone") diff --git a/easypost/models/verifications.py b/easypost/models/verifications.py new file mode 100644 index 00000000..c0f515b3 --- /dev/null +++ b/easypost/models/verifications.py @@ -0,0 +1,10 @@ +from typing import Optional + +from easypost.easypost_object import EasyPostObject +from easypost.models.verification import Verification +from pydantic import Field + + +class Verifications(EasyPostObject): + delivery: Optional[Verification] = Field(None, alias="delivery") + zip: Optional[Verification] = Field(None, alias="zip") diff --git a/easypost/models/webhook.py b/easypost/models/webhook.py index bf5771af..91744cb4 100644 --- a/easypost/models/webhook.py +++ b/easypost/models/webhook.py @@ -1,5 +1,10 @@ +from datetime import datetime +from typing import Optional + from easypost.easypost_object import EasyPostObject +from pydantic import Field class Webhook(EasyPostObject): - pass + disabled_at: Optional[datetime] = Field(None, alias="disabled_at") + url: Optional[str] = Field(None, alias="url") diff --git a/easypost/services/__init__.py b/easypost/services/__init__.py index d8a5f889..e69de29b 100644 --- a/easypost/services/__init__.py +++ b/easypost/services/__init__.py @@ -1,26 +0,0 @@ -# flake8: noqa -from easypost.services.address_service import AddressService -from easypost.services.api_key_service import ApiKeyService -from easypost.services.batch_service import BatchService -from easypost.services.beta_rate_service import BetaRateService -from easypost.services.beta_referral_customer_service import BetaReferralCustomerService -from easypost.services.billing_service import BillingService -from easypost.services.carrier_account_service import CarrierAccountService -from easypost.services.carrier_metadata_service import CarrierMetadataService -from easypost.services.customs_info_service import CustomsInfoService -from easypost.services.customs_item_service import CustomsItemService -from easypost.services.end_shipper_service import EndShipperService -from easypost.services.event_service import EventService -from easypost.services.insurance_service import InsuranceService -from easypost.services.order_service import OrderService -from easypost.services.parcel_service import ParcelService -from easypost.services.pickup_service import PickupService -from easypost.services.rate_service import RateService -from easypost.services.referral_customer_service import ReferralCustomerService -from easypost.services.refund_service import RefundService -from easypost.services.report_service import ReportService -from easypost.services.scan_form_service import ScanFormService -from easypost.services.shipment_service import ShipmentService -from easypost.services.tracker_service import TrackerService -from easypost.services.user_service import UserService -from easypost.services.webhook_service import WebhookService diff --git a/easypost/services/address_service.py b/easypost/services/address_service.py index 43f761e0..5b4f5cc5 100644 --- a/easypost/services/address_service.py +++ b/easypost/services/address_service.py @@ -4,19 +4,17 @@ Optional, ) -from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Address -from easypost.requestor import ( - RequestMethod, - Requestor, +from easypost.http import HttpMethod +from easypost.models.address import ( + Address, + AddressCollection, ) from easypost.services.base_service import BaseService class AddressService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Address.__name__ + super().__init__(client=client) def create( self, @@ -25,53 +23,55 @@ def create( **params, ) -> Address: """Create an Address.""" - url = self._class_url(self._model_class) - wrapped_params = {self._snakecase_name(self._model_class): params} # type: Dict[str, Any] + endpoint = "addresses" + method = HttpMethod.POST + wrapped_params = {"address": params} if verify: wrapped_params["verify"] = verify if verify_strict: wrapped_params["verify_strict"] = verify_strict - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + return self.request(klass=Address, method=method, endpoint=endpoint, params=wrapped_params) - return convert_to_easypost_object(response=response) - - def all(self, **params) -> Dict[str, Any]: + def all(self, **params) -> AddressCollection: """Retrieve a list of Addresses.""" - filters = { - "key": "addresses", - } + endpoint = "addresses" + method = HttpMethod.GET + + # TODO: Store the filters - return self._all_resources(self._model_class, filters, **params) + return self.request(klass=AddressCollection, method=method, endpoint=endpoint, params=params) - def retrieve(self, id) -> Address: + def retrieve(self, id: str) -> Address: """Retrieve an Address.""" - return self._retrieve_resource(self._model_class, id) + endpoint = f"addresses/{id}" + method = HttpMethod.GET + + return self.request(klass=Address, method=method, endpoint=endpoint) def create_and_verify(self, **params) -> Address: """Create and verify an Address in one call.""" - url = f"{self._class_url('address')}/create_and_verify" - wrapped_params = {self._snakecase_name(self._model_class): params} + endpoint = "addresses/create_and_verify" + method = HttpMethod.POST - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + wrapped_params = {"address": params} - return convert_to_easypost_object(response=response["address"]) + return self.request(klass=Address, method=method, endpoint=endpoint, params=wrapped_params, root_key="address") - def verify(self, id) -> Address: + def verify(self, id: str) -> Address: """Verify an already created Address.""" - url = f"{self._instance_url('address', id)}/verify" - - response = Requestor(self._client).request(method=RequestMethod.GET, url=url) + endpoint = f"addresses/{id}/verify" + method = HttpMethod.GET - return convert_to_easypost_object(response=response["address"]) + return self.request(klass=Address, method=method, endpoint=endpoint, root_key="address") def get_next_page( self, addresses: Dict[str, Any], page_size: int, optional_params: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + ) -> AddressCollection: """Retrieve the next page of the list Addresses response.""" self._check_has_next_page(collection=addresses) diff --git a/easypost/services/api_key_service.py b/easypost/services/api_key_service.py index 048ed861..3456d796 100644 --- a/easypost/services/api_key_service.py +++ b/easypost/services/api_key_service.py @@ -7,7 +7,8 @@ from easypost.constant import NO_USER_FOUND from easypost.easypost_object import convert_to_easypost_object from easypost.errors import FilteringError -from easypost.models import ApiKey +from easypost.http import HttpMethod +from easypost.models.api_key import ApiKey from easypost.requestor import ( RequestMethod, Requestor, @@ -17,16 +18,14 @@ class ApiKeyService(BaseService): def __init__(self, client): - self._client = client - self._model_class = ApiKey.__name__ + super().__init__(client=client) def all(self) -> Dict[str, Any]: """Retrieve a list of all API keys.""" - url = "/api_keys" + endpoint = "api_keys" + method = HttpMethod.GET - response = Requestor(self._client).request(method=RequestMethod.GET, url=url) - - return convert_to_easypost_object(response=response) + return self.request(klass=ApiKey, method=method, endpoint=endpoint) def retrieve_api_keys_for_user(self, id: str) -> List[ApiKey]: """Retrieve a list of API keys (works for the authenticated User or a child User).""" diff --git a/easypost/services/base_service.py b/easypost/services/base_service.py index 2e6c5c53..0d3bb50f 100644 --- a/easypost/services/base_service.py +++ b/easypost/services/base_service.py @@ -1,4 +1,3 @@ -import re from typing import ( Any, Dict, @@ -9,8 +8,10 @@ _FILTERS_KEY, NO_MORE_PAGES_ERROR, ) +from easypost.easypost_client import ApiVersion from easypost.easypost_object import convert_to_easypost_object from easypost.errors import EndOfPaginationError +from easypost.http import HttpMethod from easypost.requestor import ( RequestMethod, Requestor, @@ -20,24 +21,27 @@ class BaseService: """The base service that all other services inherit containing shared logic.""" - def __init__(self, client): - self._client = client - - def _snakecase_name(self, class_name: str) -> str: - """Return the class name as snake_case.""" - return re.sub(r"(? str: - """Generate a URL based on class name.""" - transformed_class_name = self._snakecase_name(class_name) - if transformed_class_name[-1:] in ("s", "h"): - return f"/{transformed_class_name}es" - else: - return f"/{transformed_class_name}s" - - def _instance_url(self, class_name: str, id: str) -> str: - """Generate an instance URL based on a class name and ID.""" - return f"{self._class_url(class_name)}/{id}" + def __init__(self, client: "EasyPostClient"): + self._client: "EasyPostClient" = client + + def request( + self, + klass: type, + method: HttpMethod, + endpoint: str, + params: Optional[Dict[str, Any]] = None, + root_key: Optional[str] = None, + override_api_version: Optional[ApiVersion] = None, + ) -> Any: + """Make a request to the EasyPost API.""" + return self._client.request( + klass=klass, + method=method, + endpoint=endpoint, + params=params, + root_key=root_key, + override_api_version=override_api_version, + ) def _create_resource(self, class_name: str, **params) -> Any: """Create an EasyPost object via the EasyPost API.""" diff --git a/easypost/services/batch_service.py b/easypost/services/batch_service.py index ac08efcd..ec993564 100644 --- a/easypost/services/batch_service.py +++ b/easypost/services/batch_service.py @@ -4,82 +4,82 @@ Optional, ) -from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Batch -from easypost.requestor import ( - RequestMethod, - Requestor, +from easypost.http import HttpMethod +from easypost.models.batch import ( + Batch, + BatchCollection, ) from easypost.services.base_service import BaseService class BatchService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Batch.__name__ + super().__init__(client=client) def create(self, **params) -> Batch: """Create a Batch.""" - return self._create_resource(self._model_class, **params) + endpoint = "batches" + method = HttpMethod.POST - def all(self, **params) -> Dict[str, Any]: + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) + + def all(self, **params) -> BatchCollection: """Retrieve a list of Batches.""" - filters = { - "key": "batches", - } + endpoint = "batches" + method = HttpMethod.GET + + # TODO: Store the filters - return self._all_resources(self._model_class, filters, **params) + return self.request(klass=BatchCollection, method=method, endpoint=endpoint, params=params) def retrieve(self, id: str) -> Batch: """Retrieve a Batch.""" - return self._retrieve_resource(self._model_class, id) + endpoint = f"batches/{id}" + method = HttpMethod.GET + + return self.request(klass=Batch, method=method, endpoint=endpoint) def buy(self, id: str, **params) -> Batch: """Buy a Batch.""" - url = f"{self._instance_url(self._model_class, id)}/buy" - - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) + endpoint = f"batches/{id}/buy" + method = HttpMethod.POST - return convert_to_easypost_object(response=response) + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) def label(self, id: str, **params) -> Batch: """Create a Batch label.""" - url = f"{self._instance_url(self._model_class, id)}/label" + endpoint = f"batches/{id}/label" + method = HttpMethod.POST - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) - - return convert_to_easypost_object(response=response) + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) def remove_shipments(self, id: str, **params) -> Batch: """Remove Shipments from a Batch.""" - url = f"{self._instance_url(self._model_class, id)}/remove_shipments" - - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) + endpoint = f"batches/{id}/remove_shipments" + method = HttpMethod.POST - return convert_to_easypost_object(response=response) + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) def add_shipments(self, id: str, **params) -> Batch: """Add Shipments to a Batch.""" - url = f"{self._instance_url(self._model_class, id)}/add_shipments" + endpoint = f"batches/{id}/add_shipments" + method = HttpMethod.POST - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) - - return convert_to_easypost_object(response=response) + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) def create_scan_form(self, id: str, **params) -> Batch: """Create a ScanForm for a Batch.""" - url = f"{self._instance_url(self._model_class, id)}/scan_form" - - response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) + endpoint = f"batches/{id}/scan_form" + method = HttpMethod.POST - return convert_to_easypost_object(response=response) + return self.request(klass=Batch, method=method, endpoint=endpoint, params=params) def get_next_page( self, batches: Dict[str, Any], page_size: int, optional_params: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + ) -> BatchCollection: """ Retrieve the next page of the list Batch response. diff --git a/easypost/services/beta_rate_service.py b/easypost/services/beta_rate_service.py index 16822353..c7fa5db2 100644 --- a/easypost/services/beta_rate_service.py +++ b/easypost/services/beta_rate_service.py @@ -3,8 +3,10 @@ Dict, ) +from easypost.easypost_client import ApiVersion from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Rate +from easypost.http import HttpMethod +from easypost.models.rate import Rate from easypost.requestor import ( RequestMethod, Requestor, @@ -14,14 +16,24 @@ class BetaRateService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Rate.__name__ + super().__init__(client=client) def retrieve_stateless_rates(self, **params) -> Dict[str, Any]: """Retrieves stateless rates by passing shipment data.""" - url = self._class_url(self._model_class) + endpoint = f"rates" + method = HttpMethod.POST + wrapped_params = {"shipment": params} + return self.request( + klass=Rate, + method=method, + endpoint=endpoint, + params=params, + root_key="rates", + override_api_version=ApiVersion.Beta, + ) + response = Requestor(self._client).request( method=RequestMethod.POST, url=url, diff --git a/easypost/services/billing_service.py b/easypost/services/billing_service.py index d580c39f..2cf86dfe 100644 --- a/easypost/services/billing_service.py +++ b/easypost/services/billing_service.py @@ -10,7 +10,6 @@ ) from easypost.easypost_object import convert_to_easypost_object from easypost.errors import InvalidObjectError -from easypost.models import Billing from easypost.requestor import ( RequestMethod, Requestor, @@ -20,8 +19,7 @@ class BillingService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Billing.__name__ + super().__init__(client=client) def fund_wallet(self, amount: str, priority: str = "primary") -> None: """Fund your EasyPost wallet by charging your primary or secondary payment method on file.""" diff --git a/easypost/services/carrier_account_service.py b/easypost/services/carrier_account_service.py index f78ab78e..a2aca257 100644 --- a/easypost/services/carrier_account_service.py +++ b/easypost/services/carrier_account_service.py @@ -11,7 +11,7 @@ ) from easypost.easypost_object import convert_to_easypost_object from easypost.errors import MissingParameterError -from easypost.models import CarrierAccount +from easypost.models.carrier_account import CarrierAccount from easypost.requestor import ( RequestMethod, Requestor, @@ -21,8 +21,7 @@ class CarrierAccountService(BaseService): def __init__(self, client): - self._client = client - self._model_class = CarrierAccount.__name__ + super().__init__(client=client) def create(self, **params) -> CarrierAccount: """Create a CarrierAccount.""" diff --git a/easypost/services/carrier_metadata_service.py b/easypost/services/carrier_metadata_service.py index 52377cd7..f72bfc70 100644 --- a/easypost/services/carrier_metadata_service.py +++ b/easypost/services/carrier_metadata_service.py @@ -15,7 +15,7 @@ class CarrierMetadataService(BaseService): def __init__(self, client): - self._client = client + super().__init__(client=client) def retrieve( self, diff --git a/easypost/services/customs_info_service.py b/easypost/services/customs_info_service.py index 65c994d5..22d032de 100644 --- a/easypost/services/customs_info_service.py +++ b/easypost/services/customs_info_service.py @@ -1,11 +1,10 @@ -from easypost.models import CustomsInfo +from easypost.models.customs_info import CustomsInfo from easypost.services.base_service import BaseService class CustomsInfoService(BaseService): def __init__(self, client): - self._client = client - self._model_class = CustomsInfo.__name__ + super().__init__(client=client) def create(self, **params) -> CustomsInfo: """Create a CustomsInfo.""" diff --git a/easypost/services/customs_item_service.py b/easypost/services/customs_item_service.py index a072f3d3..255bb81a 100644 --- a/easypost/services/customs_item_service.py +++ b/easypost/services/customs_item_service.py @@ -1,11 +1,10 @@ -from easypost.models import CustomsItem +from easypost.models.customs_item import CustomsItem from easypost.services.base_service import BaseService class CustomsItemService(BaseService): def __init__(self, client): - self._client = client - self._model_class = CustomsItem.__name__ + super().__init__(client=client) def create(self, **params) -> CustomsItem: """Create a CustomsItem.""" diff --git a/easypost/services/end_shipper_service.py b/easypost/services/end_shipper_service.py index edc565ef..3e5d0c2c 100644 --- a/easypost/services/end_shipper_service.py +++ b/easypost/services/end_shipper_service.py @@ -4,10 +4,8 @@ ) from easypost.easypost_object import convert_to_easypost_object -from easypost.models import ( - Address, - EndShipper, -) +from easypost.models.address import Address +from easypost.models.end_shipper import EndShipper from easypost.requestor import ( RequestMethod, Requestor, @@ -17,9 +15,7 @@ class EndShipperService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Address.__name__ - self._service_class = EndShipper.__name__ + super().__init__(client=client) def create(self, **params) -> Address: """Create an EndShipper.""" diff --git a/easypost/services/event_service.py b/easypost/services/event_service.py index 066ef177..fe62546d 100644 --- a/easypost/services/event_service.py +++ b/easypost/services/event_service.py @@ -5,10 +5,8 @@ ) from easypost.easypost_object import convert_to_easypost_object -from easypost.models import ( - Event, - Payload, -) +from easypost.models.event import Event +from easypost.models.payload import Payload from easypost.requestor import ( RequestMethod, Requestor, @@ -18,8 +16,7 @@ class EventService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Event.__name__ + super().__init__(client=client) def create(self, **params) -> Event: """Create an Event.""" diff --git a/easypost/services/insurance_service.py b/easypost/services/insurance_service.py index e442efce..6c6e2e7a 100644 --- a/easypost/services/insurance_service.py +++ b/easypost/services/insurance_service.py @@ -5,7 +5,7 @@ ) from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Insurance +from easypost.models.insurance import Insurance from easypost.requestor import ( RequestMethod, Requestor, @@ -15,8 +15,7 @@ class InsuranceService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Insurance.__name__ + super().__init__(client=client) def create(self, **params) -> Insurance: """Create an Insurance.""" diff --git a/easypost/services/order_service.py b/easypost/services/order_service.py index 0624b6ee..e62fa05b 100644 --- a/easypost/services/order_service.py +++ b/easypost/services/order_service.py @@ -1,5 +1,5 @@ from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Order +from easypost.models.order import Order from easypost.requestor import ( RequestMethod, Requestor, @@ -9,8 +9,7 @@ class OrderService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Order.__name__ + super().__init__(client=client) def create(self, **params) -> Order: """Create an Order.""" diff --git a/easypost/services/parcel_service.py b/easypost/services/parcel_service.py index 6b71781f..d93b1bbc 100644 --- a/easypost/services/parcel_service.py +++ b/easypost/services/parcel_service.py @@ -1,11 +1,10 @@ -from easypost.models import Parcel +from easypost.models.parcel import Parcel from easypost.services.base_service import BaseService class ParcelService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Parcel.__name__ + super().__init__(client=client) def create(self, **params) -> Parcel: """Create a Parcel.""" diff --git a/easypost/services/pickup_service.py b/easypost/services/pickup_service.py index 2102a1bf..36856284 100644 --- a/easypost/services/pickup_service.py +++ b/easypost/services/pickup_service.py @@ -5,7 +5,7 @@ ) from easypost.easypost_object import convert_to_easypost_object -from easypost.models import Pickup +from easypost.models.pickup import Pickup from easypost.requestor import ( RequestMethod, Requestor, @@ -15,8 +15,7 @@ class PickupService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Pickup.__name__ + super().__init__(client=client) def create(self, **params) -> Pickup: """Create a Pickup.""" diff --git a/easypost/services/rate_service.py b/easypost/services/rate_service.py index 00f8e1bf..88226aef 100644 --- a/easypost/services/rate_service.py +++ b/easypost/services/rate_service.py @@ -1,11 +1,10 @@ -from easypost.models import Rate +from easypost.models.rate import Rate from easypost.services.base_service import BaseService class RateService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Rate.__name__ + super().__init__(client=client) def retrieve(self, id: str) -> Rate: """Retrieve a Rate.""" diff --git a/easypost/services/referral_customer_service.py b/easypost/services/referral_customer_service.py index 30fe3918..8d3c617e 100644 --- a/easypost/services/referral_customer_service.py +++ b/easypost/services/referral_customer_service.py @@ -13,7 +13,7 @@ ) from easypost.easypost_object import convert_to_easypost_object from easypost.errors import ExternalApiError -from easypost.models import User +from easypost.models.user import User from easypost.requestor import ( RequestMethod, Requestor, @@ -23,8 +23,7 @@ class ReferralCustomerService(BaseService): def __init__(self, client): - self._client = client - self._model_class = User.__name__ + super().__init__(client=client) def create(self, **params) -> User: """Create a referral customer. diff --git a/easypost/services/refund_service.py b/easypost/services/refund_service.py index 16eef656..27857fc4 100644 --- a/easypost/services/refund_service.py +++ b/easypost/services/refund_service.py @@ -4,14 +4,13 @@ Optional, ) -from easypost.models import Refund +from easypost.models.refund import Refund from easypost.services.base_service import BaseService class RefundService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Refund.__name__ + super().__init__(client=client) def create(self, **params) -> Refund: """Create a Shipment Refund.""" diff --git a/easypost/services/report_service.py b/easypost/services/report_service.py index 4b2b8c49..9419167b 100644 --- a/easypost/services/report_service.py +++ b/easypost/services/report_service.py @@ -10,7 +10,7 @@ ) from easypost.easypost_object import convert_to_easypost_object from easypost.errors import MissingParameterError -from easypost.models import Report +from easypost.models.report import Report from easypost.requestor import ( RequestMethod, Requestor, @@ -20,8 +20,7 @@ class ReportService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Report.__name__ + super().__init__(client=client) def create(self, **params) -> Report: """Create a Report.""" diff --git a/easypost/services/scan_form_service.py b/easypost/services/scan_form_service.py index 505c364d..8e7d656a 100644 --- a/easypost/services/scan_form_service.py +++ b/easypost/services/scan_form_service.py @@ -4,14 +4,13 @@ Optional, ) -from easypost.models import ScanForm +from easypost.models.scan_form import ScanForm from easypost.services.base_service import BaseService class ScanFormService(BaseService): def __init__(self, client): - self._client = client - self._model_class = ScanForm.__name__ + super().__init__(client=client) def create(self, **params) -> ScanForm: """Create a ScanForm.""" diff --git a/easypost/services/shipment_service.py b/easypost/services/shipment_service.py index 409eb7b7..1d061b49 100644 --- a/easypost/services/shipment_service.py +++ b/easypost/services/shipment_service.py @@ -7,10 +7,8 @@ from easypost.constant import _FILTERS_KEY from easypost.easypost_object import convert_to_easypost_object -from easypost.models import ( - Rate, - Shipment, -) +from easypost.models.rate import Rate +from easypost.models.shipment import Shipment from easypost.requestor import ( RequestMethod, Requestor, @@ -21,8 +19,7 @@ class ShipmentService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Shipment.__name__ + super().__init__(client=client) def create(self, **params) -> Shipment: """Create a Shipment.""" diff --git a/easypost/services/tracker_service.py b/easypost/services/tracker_service.py index 777652be..23d2db68 100644 --- a/easypost/services/tracker_service.py +++ b/easypost/services/tracker_service.py @@ -6,7 +6,7 @@ ) from easypost.constant import _FILTERS_KEY -from easypost.models import Tracker +from easypost.models.tracker import Tracker from easypost.requestor import ( RequestMethod, Requestor, @@ -16,8 +16,7 @@ class TrackerService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Tracker.__name__ + super().__init__(client=client) def create(self, **params) -> Tracker: """Create a Tracker.""" diff --git a/easypost/services/user_service.py b/easypost/services/user_service.py index b61957b3..a229536b 100644 --- a/easypost/services/user_service.py +++ b/easypost/services/user_service.py @@ -7,10 +7,8 @@ from warnings import warn from easypost.easypost_object import convert_to_easypost_object -from easypost.models import ( - ApiKey, - User, -) +from easypost.models.api_key import ApiKey +from easypost.models.user import User from easypost.requestor import ( RequestMethod, Requestor, @@ -20,8 +18,7 @@ class UserService(BaseService): def __init__(self, client): - self._client = client - self._model_class = User.__name__ + super().__init__(client=client) def create(self, **params) -> User: """Create a User.""" diff --git a/easypost/services/webhook_service.py b/easypost/services/webhook_service.py index 2817e666..7647cacd 100644 --- a/easypost/services/webhook_service.py +++ b/easypost/services/webhook_service.py @@ -3,14 +3,13 @@ Dict, ) -from easypost.models import Webhook +from easypost.models.webhook import Webhook from easypost.services.base_service import BaseService class WebhookService(BaseService): def __init__(self, client): - self._client = client - self._model_class = Webhook.__name__ + super().__init__(client=client) def create(self, **params) -> Webhook: """Create a Webhook.""" diff --git a/tests/test_address.py b/tests/test_address.py index 61f69c10..de6d40ef 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -5,7 +5,7 @@ NO_MORE_PAGES_ERROR, ) from easypost.errors import ApiError -from easypost.models import Address +from easypost.models.address import Address @pytest.mark.vcr() @@ -26,13 +26,14 @@ def test_address_create_verify(incorrect_address, test_client): address = test_client.address.create(**incorrect_address) assert isinstance(address, Address) - assert hasattr(address.verifications, "delivery") is False + assert address.verifications.delivery is None # Creating with verify would make the address and perform verifications incorrect_address["verify"] = True address = test_client.address.create(**incorrect_address) assert isinstance(address, Address) + assert address.verifications.delivery is not None assert address.verifications.delivery.success is False @@ -61,13 +62,14 @@ def test_address_create_verify_array(incorrect_address, test_client): address = test_client.address.create(**incorrect_address) assert isinstance(address, Address) - assert hasattr(address.verifications, "delivery") is False + assert address.verifications.delivery is None # Creating with verify would make the address and perform verifications incorrect_address["verify"] = [True] address = test_client.address.create(**incorrect_address) assert isinstance(address, Address) + assert address.verifications.delivery is not None assert address.verifications.delivery.success is False @@ -94,10 +96,10 @@ def test_address_retrieve(ca_address_1, test_client): def test_address_all(page_size, test_client): addresses = test_client.address.all(page_size=page_size) - addresses_array = addresses["addresses"] + addresses_array = addresses.addresses assert len(addresses_array) <= page_size - assert addresses["has_more"] is not None + assert hasattr(addresses, "has_more") assert all(isinstance(address, Address) for address in addresses_array) diff --git a/tests/test_api_key.py b/tests/test_api_key.py index fa63fc2c..f193f1d3 100644 --- a/tests/test_api_key.py +++ b/tests/test_api_key.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import ApiKey +from easypost.models.api_key import ApiKey @pytest.mark.vcr() diff --git a/tests/test_batch.py b/tests/test_batch.py index c0d38b5e..0990f5ba 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -3,7 +3,7 @@ import time import pytest -from easypost.models import Batch +from easypost.models.batch import Batch @pytest.mark.vcr() diff --git a/tests/test_carrier_account.py b/tests/test_carrier_account.py index c6d19f9f..7e211485 100644 --- a/tests/test_carrier_account.py +++ b/tests/test_carrier_account.py @@ -1,6 +1,6 @@ import pytest from easypost.errors.api.api_error import ApiError -from easypost.models import CarrierAccount +from easypost.models.carrier_account import CarrierAccount @pytest.mark.vcr() diff --git a/tests/test_customs_info.py b/tests/test_customs_info.py index 6a7cf925..1728c2a7 100644 --- a/tests/test_customs_info.py +++ b/tests/test_customs_info.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import CustomsInfo +from easypost.models.customs_info import CustomsInfo @pytest.mark.vcr() diff --git a/tests/test_customs_item.py b/tests/test_customs_item.py index 15de1738..d3d6777c 100644 --- a/tests/test_customs_item.py +++ b/tests/test_customs_item.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import CustomsItem +from easypost.models.customs_item import CustomsItem @pytest.mark.vcr() diff --git a/tests/test_end_shipper.py b/tests/test_end_shipper.py index cd27936d..6a0d6c83 100644 --- a/tests/test_end_shipper.py +++ b/tests/test_end_shipper.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import EndShipper +from easypost.models.end_shipper import EndShipper @pytest.mark.vcr() diff --git a/tests/test_event.py b/tests/test_event.py index 5abf7c7f..dd9e032a 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -9,10 +9,8 @@ NO_MORE_PAGES_ERROR, ) from easypost.errors import ApiError -from easypost.models import ( - Event, - Payload, -) +from easypost.models.event import Event +from easypost.models.payload import Payload from easypost.util import receive_event diff --git a/tests/test_insurance.py b/tests/test_insurance.py index 98ca885a..5f2dd613 100644 --- a/tests/test_insurance.py +++ b/tests/test_insurance.py @@ -4,7 +4,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import Insurance +from easypost.models.insurance import Insurance @pytest.mark.vcr() diff --git a/tests/test_order.py b/tests/test_order.py index 33e60f46..facd2ce8 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -1,9 +1,7 @@ import pytest from easypost.errors import FilteringError -from easypost.models import ( - Order, - Rate, -) +from easypost.models.order import Order +from easypost.models.rate import Rate @pytest.mark.vcr() diff --git a/tests/test_parcel.py b/tests/test_parcel.py index 0faee13c..ff6ed137 100644 --- a/tests/test_parcel.py +++ b/tests/test_parcel.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import Parcel +from easypost.models.parcel import Parcel @pytest.mark.vcr() diff --git a/tests/test_pickup.py b/tests/test_pickup.py index 09200c1c..de61edd3 100644 --- a/tests/test_pickup.py +++ b/tests/test_pickup.py @@ -5,7 +5,7 @@ NO_MORE_PAGES_ERROR, ) from easypost.errors import FilteringError -from easypost.models import Pickup +from easypost.models.pickup import Pickup @pytest.mark.vcr() diff --git a/tests/test_rate.py b/tests/test_rate.py index cf482464..cf6cf939 100644 --- a/tests/test_rate.py +++ b/tests/test_rate.py @@ -1,5 +1,5 @@ import pytest -from easypost.models import Rate +from easypost.models.rate import Rate @pytest.mark.vcr() diff --git a/tests/test_referral_customer.py b/tests/test_referral_customer.py index 089ff785..cf730a15 100644 --- a/tests/test_referral_customer.py +++ b/tests/test_referral_customer.py @@ -7,7 +7,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import User +from easypost.models.user import User REFERRAL_CUSTOMER_PROD_API_KEY = os.getenv("REFERRAL_CUSTOMER_PROD_API_KEY", "123") diff --git a/tests/test_refund.py b/tests/test_refund.py index 359758ee..6d41c496 100644 --- a/tests/test_refund.py +++ b/tests/test_refund.py @@ -4,7 +4,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import Refund +from easypost.models.refund import Refund @pytest.mark.vcr() diff --git a/tests/test_report.py b/tests/test_report.py index 5aa61bd8..7faed17d 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -4,7 +4,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import Report +from easypost.models.report import Report @pytest.mark.vcr() diff --git a/tests/test_scan_form.py b/tests/test_scan_form.py index 09e7509e..402be280 100644 --- a/tests/test_scan_form.py +++ b/tests/test_scan_form.py @@ -4,7 +4,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import ScanForm +from easypost.models.scan_form import ScanForm @pytest.mark.vcr() diff --git a/tests/test_shipment.py b/tests/test_shipment.py index 396c8fa2..0b597aab 100644 --- a/tests/test_shipment.py +++ b/tests/test_shipment.py @@ -8,10 +8,8 @@ FilteringError, InvalidParameterError, ) -from easypost.models import ( - Rate, - Shipment, -) +from easypost.models.rate import Rate +from easypost.models.shipment import Shipment from easypost.util import get_lowest_smart_rate diff --git a/tests/test_tracker.py b/tests/test_tracker.py index 4fb11b0d..574655d7 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -4,7 +4,7 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import Tracker +from easypost.models.tracker import Tracker @pytest.mark.vcr() diff --git a/tests/test_user.py b/tests/test_user.py index 2608016d..9b4d55de 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -4,10 +4,8 @@ _TEST_FAILED_INTENTIONALLY_ERROR, NO_MORE_PAGES_ERROR, ) -from easypost.models import ( - Brand, - User, -) +from easypost.models.brand import Brand +from easypost.models.user import User @pytest.mark.vcr() diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 5a42d8d6..a13045f0 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -1,6 +1,6 @@ import pytest from easypost.errors import SignatureVerificationError -from easypost.models import Webhook +from easypost.models.webhook import Webhook from easypost.util import validate_webhook