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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ jobs:
- name: Lint
run: make lint
run-tests:
# TODO: Use `latest` once we drop support for Python 3.7
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
strategy:
matrix:
pythonversion: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
pythonversion: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

## Next Release

- Drops support for Python 3.7 and 3.8
- Fixes the payload wrapping for updating a webhook
- Removes deprecated `user.all_api_keys` and `user.api_keys`, use `api_key.all` and `api_key.retrieve_api_keys_for_user` respectively
- Bumps all dev dependencies

## v9.5.0 (2024-10-24)

Expand Down
25 changes: 14 additions & 11 deletions easypost/easypost_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
import re
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] = {
EASYPOST_OBJECT_ID_PREFIX_TO_CLASS_NAME_MAP: dict[str, Any] = {
"adr": "Address",
"ak": "ApiKey",
"batch": "Batch",
Expand Down Expand Up @@ -44,7 +42,7 @@
"user": "User",
}

OBJECT_CLASS_NAME_OVERRIDES: Dict[str, Any] = {
OBJECT_CLASS_NAME_OVERRIDES: dict[str, Any] = {
"CashFlowReport": "Report",
"PaymentLogReport": "Report",
"RefundReport": "Report",
Expand All @@ -55,7 +53,7 @@


def convert_to_easypost_object(
response: Dict[str, Any],
response: dict[str, Any],
parent: object = None,
name: Optional[str] = None,
):
Expand Down Expand Up @@ -147,17 +145,17 @@ def __setitem__(self, k, v) -> None:
setattr(self, k, v)

@property
def keys(self) -> List[str]:
def keys(self) -> list[str]:
return self._values.keys()

@property
def values(self) -> List[Any]:
def values(self) -> list[Any]:
return self._values.keys()

@classmethod
def construct_from(
cls,
values: Dict[str, Any],
values: dict[str, Any],
parent: object = None,
name: Optional[str] = None,
) -> object:
Expand All @@ -167,7 +165,7 @@ def construct_from(

return instance

def convert_each_value(self, values: Dict[str, Any]) -> None:
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:
Expand All @@ -186,7 +184,12 @@ def __repr__(self) -> str:

json_string = json.dumps(obj=self.to_dict(), sort_keys=True, indent=2, cls=EasyPostObjectEncoder)

return "<%s%s at %s> JSON: %s" % (type(self).__name__, type_string, hex(id(self)), json_string)
return "<%s%s at %s> JSON: %s" % (
type(self).__name__,
type_string,
hex(id(self)),
json_string,
)

def __str__(self) -> str:
return self.to_json(indent=2)
Expand All @@ -200,7 +203,7 @@ 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]:
def to_dict(self) -> dict[str, Any]:
"""Convert current object to a dict."""

def _serialize(o):
Expand Down
12 changes: 5 additions & 7 deletions easypost/errors/api/api_error.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import json
from typing import (
Any,
Dict,
List,
Optional,
Union,
)
Expand All @@ -16,15 +14,15 @@ class ApiError(EasyPostError):
def __init__(
self,
message: Union[
Dict[str, Any], list, str
dict[str, Any], list, str
], # message should be a string but can sometimes incorrectly come back as a list or object
errors: Optional[List[str]] = None,
errors: Optional[list[str]] = None,
code: Optional[str] = None,
http_status: Optional[int] = None,
http_body: Optional[Union[str, bytes]] = None,
):
super().__init__(message) # type: ignore
message_list: List[str] = []
message_list: list[str] = []
self._traverse_json_element(message, message_list)
self.message = ", ".join(message_list)
self.errors = errors
Expand Down Expand Up @@ -54,8 +52,8 @@ def __init__(

def _traverse_json_element(
self,
error_message: Optional[Union[Dict[str, Any], list, str]],
messages_list: List[str],
error_message: Optional[Union[dict[str, Any], list, str]],
messages_list: list[str],
) -> None:
"""Recursively traverses a JSON object or array and extracts error messages
as strings. Adds the extracted messages to the specified messages_list array.
Expand Down
3 changes: 1 addition & 2 deletions easypost/models/order.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import (
List,
Optional,
)

Expand All @@ -9,7 +8,7 @@


class Order(EasyPostObject):
def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate:
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)

Expand Down
3 changes: 1 addition & 2 deletions easypost/models/pickup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import (
List,
Optional,
)

Expand All @@ -9,7 +8,7 @@


class Pickup(EasyPostObject):
def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate:
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")

Expand Down
3 changes: 1 addition & 2 deletions easypost/models/shipment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import (
List,
Optional,
)

Expand All @@ -9,7 +8,7 @@


class Shipment(EasyPostObject):
def lowest_rate(self, carriers: Optional[List[str]] = None, services: Optional[List[str]] = None) -> Rate:
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)

Expand Down
42 changes: 20 additions & 22 deletions easypost/requestor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from json import JSONDecodeError
from typing import (
Any,
Dict,
List,
Optional,
Tuple,
Union,
Expand Down Expand Up @@ -49,7 +47,7 @@
)


STATUS_CODE_TO_ERROR_MAPPING: Dict[int, Any] = {
STATUS_CODE_TO_ERROR_MAPPING: dict[int, Any] = {
400: BadRequestError,
401: UnauthorizedError,
402: PaymentError,
Expand Down Expand Up @@ -78,7 +76,7 @@ def __init__(self, client):
self._client = client

@classmethod
def _objects_to_ids(cls, param: Dict[str, Any]) -> Dict[str, Any]:
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.
"""
Expand All @@ -97,10 +95,10 @@ def _objects_to_ids(cls, param: Dict[str, Any]) -> Dict[str, Any]:

@staticmethod
def form_encode_params(
data: Dict[str, Any],
parent_keys: Optional[List[str]] = None,
parent_dict: Optional[Dict[str, Any]] = None,
) -> Dict:
data: dict[str, Any],
parent_keys: Optional[list[str]] = None,
parent_dict: Optional[dict[str, Any]] = None,
) -> dict:
"""Form-encode a multi-layer dictionary to a one-layer dictionary."""
result = parent_dict or {}
keys = parent_keys or []
Expand All @@ -116,7 +114,7 @@ def form_encode_params(
return result

@staticmethod
def _build_dict_key(keys: List[str]) -> str:
def _build_dict_key(keys: list[str]) -> str:
"""Build a dict key from a list of keys.
Example: [code, number] -> code[number]
"""
Expand All @@ -131,9 +129,9 @@ def request(
self,
method: RequestMethod,
url: str,
params: Optional[Dict[str, Any]] = None,
params: Optional[dict[str, Any]] = None,
beta: bool = False,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Make a request to the EasyPost API."""
if params is None:
params = {}
Expand All @@ -153,7 +151,7 @@ def request_raw(
self,
method: RequestMethod,
url: str,
params: Optional[Dict[str, Any]] = None,
params: Optional[dict[str, Any]] = None,
beta: bool = False,
) -> Tuple[str, int]:
"""Internal logic required to make a request to the EasyPost API."""
Expand Down Expand Up @@ -239,7 +237,7 @@ def request_raw(

return http_body, http_status

def interpret_response(self, http_body: str, http_status: int) -> Dict[str, Any]:
def interpret_response(self, http_body: str, http_status: int) -> dict[str, Any]:
"""Interpret the response body we receive from the API."""
if http_status == 204:
# HTTP 204 does not have any response body and we can just return here
Expand All @@ -259,9 +257,9 @@ def requests_request(
self,
method: RequestMethod,
abs_url: str,
headers: Dict[str, Any],
params: Dict[str, Any],
) -> Tuple[str, int, Dict[str, Any]]:
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 [RequestMethod.GET, RequestMethod.DELETE]:
url_params = params
Expand Down Expand Up @@ -299,9 +297,9 @@ def urlfetch_request(
self,
method: RequestMethod,
abs_url: str,
headers: Dict[str, Any],
params: Dict[str, Any],
) -> Tuple[str, int, Dict[str, Any]]:
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,
Expand Down Expand Up @@ -329,7 +327,7 @@ def urlfetch_request(

return result.content, result.status_code, result.headers

def handle_api_error(self, http_status: int, http_body: str, response: Dict[str, Any]) -> None:
def handle_api_error(self, http_status: int, http_body: str, response: dict[str, Any]) -> None:
"""Handles API errors returned from the EasyPost API."""
try:
error = response["error"]
Expand Down Expand Up @@ -357,7 +355,7 @@ def _utf8(self, value: Union[str, bytes]) -> str:
return value.decode(encoding="utf-8")
return value

def encode_url_params(self, params: Dict[str, Any], method: RequestMethod) -> Union[str, None]:
def encode_url_params(self, params: dict[str, Any], method: RequestMethod) -> Union[str, None]:
"""Encode params for a URL."""
if method not in [RequestMethod.GET, RequestMethod.DELETE]:
raise EasyPostError(INVALID_REQUEST_PARAMETERS_ERROR)
Expand All @@ -373,7 +371,7 @@ def encode_url_params(self, params: Dict[str, Any], method: RequestMethod) -> Un

return urlencode(query=converted_params)

def add_params_to_url(self, url: str, params: Dict[str, Any], method: RequestMethod) -> str:
def add_params_to_url(self, url: str, params: dict[str, Any], method: RequestMethod) -> str:
"""Add params to the URL."""
if method not in [RequestMethod.GET, RequestMethod.DELETE]:
raise EasyPostError(INVALID_REQUEST_PARAMETERS_ERROR)
Expand Down
11 changes: 5 additions & 6 deletions easypost/services/address_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import (
Any,
Dict,
Optional,
)

Expand All @@ -26,7 +25,7 @@ def create(
) -> Address:
"""Create an Address."""
url = self._class_url(self._model_class)
wrapped_params = {self._snakecase_name(self._model_class): params} # type: Dict[str, Any]
wrapped_params = {self._snakecase_name(self._model_class): params} # type: dict[str, Any]

if verify:
wrapped_params["verify"] = verify
Expand All @@ -37,7 +36,7 @@ def create(

return convert_to_easypost_object(response=response)

def all(self, **params) -> Dict[str, Any]:
def all(self, **params) -> dict[str, Any]:
"""Retrieve a list of Addresses."""
filters = {
"key": "addresses",
Expand Down Expand Up @@ -68,10 +67,10 @@ def verify(self, id) -> Address:

def get_next_page(
self,
addresses: Dict[str, Any],
addresses: dict[str, Any],
page_size: int,
optional_params: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
optional_params: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
"""Retrieve the next page of the list Addresses response."""
self._check_has_next_page(collection=addresses)

Expand Down
6 changes: 2 additions & 4 deletions easypost/services/api_key_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from typing import (
Any,
Dict,
List,
)

from easypost.constant import NO_USER_FOUND
Expand All @@ -20,15 +18,15 @@ def __init__(self, client):
self._client = client
self._model_class = ApiKey.__name__

def all(self) -> Dict[str, Any]:
def all(self) -> dict[str, Any]:
"""Retrieve a list of all API keys."""
url = "/api_keys"

response = Requestor(self._client).request(method=RequestMethod.GET, url=url)

return convert_to_easypost_object(response=response)

def retrieve_api_keys_for_user(self, id: str) -> List[ApiKey]:
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)."""
api_keys = self.all()

Expand Down
Loading