Skip to content

Commit

Permalink
feat: introduce SAPIENT carrier HUB API
Browse files Browse the repository at this point in the history
  • Loading branch information
danh91 committed Aug 11, 2024
1 parent 95a0f5b commit 0343ba4
Show file tree
Hide file tree
Showing 41 changed files with 2,554 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ class Settings(provider_utils.Settings):
config: dict = {}

services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore

@property
def shipping_services(self) -> typing.List[models.ServiceLevel]:
if any(self.services or []):
return self.services

return provider_units.DEFAULT_SERVICES
7 changes: 7 additions & 0 deletions modules/connectors/dpdhl/karrio/mappers/dpdhl/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ class Settings(provider_utils.Settings, rating_proxy.RatingMixinSettings):
config: dict = {}

services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore

@property
def shipping_services(self) -> typing.List[models.ServiceLevel]:
if any(self.services or []):
return self.services

return provider_units.DEFAULT_SERVICES
7 changes: 7 additions & 0 deletions modules/connectors/geodis/karrio/mappers/geodis/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ class Settings(provider_utils.Settings):
config: dict = {}

services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore

@property
def shipping_services(self) -> typing.List[models.ServiceLevel]:
if any(self.services or []):
return self.services

return provider_units.DEFAULT_SERVICES
31 changes: 31 additions & 0 deletions modules/connectors/sapient/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# karrio.sapient

This package is a SAPIENT extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK.

## Requirements

`Python 3.7+`

## Installation

```bash
pip install karrio.sapient
```

## Usage

```python
import karrio
from karrio.mappers.sapient.settings import Settings


# Initialize a carrier gateway
sapient = karrio.gateway["sapient"].create(
Settings(
...
)
)
```

Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests
17 changes: 17 additions & 0 deletions modules/connectors/sapient/generate
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
SCHEMAS=./schemas
LIB_MODULES=./karrio/schemas/sapient
find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \;
touch "${LIB_MODULES}/__init__.py"

quicktype() {
echo "Generating $1..."
docker run -it --rm --name quicktype -v $PWD:/app -e SCHEMAS=/app/schemas -e LIB_MODULES=/app/karrio/schemas/sapient \
karrio/tools /quicktype/script/quicktype --no-uuids --no-date-times --no-enums --src-lang json --lang jstruct \
--no-nice-property-names --all-properties-optional --type-as-suffix $@
}

quicktype --src="${SCHEMAS}/error_response.json" --out="${LIB_MODULES}/error_response.py"
quicktype --src="${SCHEMAS}/pickup_request.json" --out="${LIB_MODULES}/pickup_request.py"
quicktype --src="${SCHEMAS}/pickup_response.json" --out="${LIB_MODULES}/pickup_response.py"
quicktype --src="${SCHEMAS}/shipment_requests.json" --out="${LIB_MODULES}/shipment_requests.py"
quicktype --src="${SCHEMAS}/shipment_response.json" --out="${LIB_MODULES}/shipment_response.py"
22 changes: 22 additions & 0 deletions modules/connectors/sapient/karrio/mappers/sapient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from karrio.core.metadata import Metadata

from karrio.mappers.sapient.mapper import Mapper
from karrio.mappers.sapient.proxy import Proxy
from karrio.mappers.sapient.settings import Settings
import karrio.providers.sapient.units as units
import karrio.providers.sapient.utils as utils


METADATA = Metadata(
id="sapient",
label="SAPIENT",
# Integrations
Mapper=Mapper,
Proxy=Proxy,
Settings=Settings,
# Data Units
is_hub=False,
# options=units.ShippingOption,
# services=units.ShippingService,
# connection_configs=utils.ConnectionConfig,
)
69 changes: 69 additions & 0 deletions modules/connectors/sapient/karrio/mappers/sapient/mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Karrio SAPIENT client mapper."""

import typing
import karrio.lib as lib
import karrio.api.mapper as mapper
import karrio.core.models as models
import karrio.providers.sapient as provider
import karrio.mappers.sapient.settings as provider_settings
import karrio.universal.providers.rating as universal_provider


class Mapper(mapper.Mapper):
settings: provider_settings.Settings

def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
return universal_provider.rate_request(payload, self.settings)

def create_shipment_request(
self, payload: models.ShipmentRequest
) -> lib.Serializable:
return provider.shipment_request(payload, self.settings)

def create_pickup_request(self, payload: models.PickupRequest) -> lib.Serializable:
return provider.pickup_request(payload, self.settings)

def create_pickup_update_request(
self, payload: models.PickupUpdateRequest
) -> lib.Serializable:
return provider.pickup_update_request(payload, self.settings)

def create_cancel_pickup_request(
self, payload: models.PickupCancelRequest
) -> lib.Serializable:
return provider.pickup_cancel_request(payload, self.settings)

def create_cancel_shipment_request(
self, payload: models.ShipmentCancelRequest
) -> lib.Serializable[str]:
return provider.shipment_cancel_request(payload, self.settings)

def parse_cancel_pickup_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
return provider.parse_pickup_cancel_response(response, self.settings)

def parse_cancel_shipment_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
return provider.parse_shipment_cancel_response(response, self.settings)

def parse_pickup_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
return provider.parse_pickup_response(response, self.settings)

def parse_pickup_update_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[models.PickupDetails, typing.List[models.Message]]:
return provider.parse_pickup_update_response(response, self.settings)

def parse_rate_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
return provider.parse_rate_response(response, self.settings)

def parse_shipment_response(
self, response: lib.Deserializable[str]
) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
return provider.parse_shipment_response(response, self.settings)
76 changes: 76 additions & 0 deletions modules/connectors/sapient/karrio/mappers/sapient/proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Karrio SAPIENT client proxy."""

import karrio.lib as lib
import karrio.api.proxy as proxy
import karrio.mappers.sapient.settings as provider_settings
import karrio.universal.mappers.rating_proxy as rating_proxy


class Proxy(rating_proxy.RatingMixinProxy, proxy.Proxy):
settings: provider_settings.Settings

def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]:
return super().get_rates(request)

def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = lib.request(
url=f"{self.settings.server_url}/v4/shipments/{request.ctx['carrier']}",
data=lib.to_json(request.serialize()),
trace=self.trace_as("json"),
method="POST",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.settings.access_token}",
},
)

return lib.Deserializable(response, lib.to_dict, request.ctx)

def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = lib.request(
url=f"{self.settings.server_url}/v4/shipments/status",
data=lib.to_json(request.serialize()),
trace=self.trace_as("json"),
method="PUT",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.settings.access_token}",
},
)

return lib.Deserializable(response, lib.to_dict)

def schedule_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = lib.request(
url=f"{self.settings.server_url}/v4/collections/{request.ctx['carrier']}/{request.ctx['shipmentId']}",
data=lib.to_json(request.serialize()),
trace=self.trace_as("json"),
method="POST",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.settings.access_token}",
},
)

return lib.Deserializable(response, lib.to_dict, request.ctx)

def modify_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = self.cancel_pickup(lib.Serializable(request.ctx))

if response.deserialize()["ok"]:
response = self.schedule_pickup(request)

return lib.Deserializable(response, lib.to_dict)

def cancel_pickup(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = lib.request(
url=f"{self.settings.server_url}/v4/collections/{request.serialize()['carrier']}/{request.serialize()['shipmentId']}/cancel",
trace=self.trace_as("json"),
method="PUT",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.settings.access_token}",
},
)

return lib.Deserializable(response, lib.to_dict)
35 changes: 35 additions & 0 deletions modules/connectors/sapient/karrio/mappers/sapient/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Karrio SAPIENT client settings."""

import attr
import typing
import jstruct
import karrio.core.models as models
import karrio.providers.sapient.utils as provider_utils
import karrio.providers.sapient.units as provider_units


@attr.s(auto_attribs=True)
class Settings(provider_utils.Settings):
"""SAPIENT connection settings."""

# Add carrier specific API connection properties here
client_id: str
client_secret: str
shipping_account_id: str
carrier_code: str = "RM"

# generic properties
id: str = None
test_mode: bool = False
carrier_id: str = "sapient"
services: typing.List[models.ServiceLevel] = jstruct.JList[models.ServiceLevel, False, dict(default=provider_units.DEFAULT_SERVICES)] # type: ignore
account_country_code: str = "GB"
metadata: dict = {}
config: dict = {}

@property
def shipping_services(self) -> typing.List[models.ServiceLevel]:
if any(self.services or []):
return self.services

return provider_units.DEFAULT_SERVICES
18 changes: 18 additions & 0 deletions modules/connectors/sapient/karrio/providers/sapient/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Karrio SAPIENT provider imports."""

from karrio.providers.sapient.utils import Settings

from karrio.providers.sapient.shipment import (
parse_shipment_cancel_response,
parse_shipment_response,
shipment_cancel_request,
shipment_request,
)
from karrio.providers.sapient.pickup import (
parse_pickup_cancel_response,
parse_pickup_update_response,
parse_pickup_response,
pickup_update_request,
pickup_cancel_request,
pickup_request,
)
28 changes: 28 additions & 0 deletions modules/connectors/sapient/karrio/providers/sapient/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Karrio SAPIENT error parser."""

import typing
import karrio.lib as lib
import karrio.core.models as models
import karrio.providers.sapient.utils as provider_utils


def parse_error_response(
response: typing.Union[dict, typing.List[dict]],
settings: provider_utils.Settings,
**kwargs,
) -> typing.List[models.Message]:
responses = response if isinstance(response, list) else [response]
errors: typing.List[dict] = sum(
[_["Errors"] for _ in responses if "Errors" in _], []
)

return [
models.Message(
carrier_id=settings.carrier_id,
carrier_name=settings.carrier_name,
code=error.get("ErrorCode", "UNKNOWN"),
message=error.get("Message", "Unknown error"),
details={**kwargs, "Cause": error.get("Cause")},
)
for error in errors
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

from karrio.providers.sapient.pickup.create import parse_pickup_response, pickup_request
from karrio.providers.sapient.pickup.update import parse_pickup_update_response, pickup_update_request
from karrio.providers.sapient.pickup.cancel import parse_pickup_cancel_response, pickup_cancel_request
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import typing
import karrio.lib as lib
import karrio.core.units as units
import karrio.core.models as models
import karrio.providers.sapient.error as error
import karrio.providers.sapient.utils as provider_utils
import karrio.providers.sapient.units as provider_units


def parse_pickup_cancel_response(
_response: lib.Deserializable[dict],
settings: provider_utils.Settings,
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
response = _response.deserialize()
messages = error.parse_error_response(response, settings)
success = True # compute address validation success state

confirmation = (
models.ConfirmationDetails(
carrier_id=settings.carrier_id,
carrier_name=settings.carrier_name,
operation="Cancel Pickup",
success=success,
)
if success
else None
)

return confirmation, messages


def pickup_cancel_request(
payload: models.PickupCancelRequest,
settings: provider_utils.Settings,
) -> lib.Serializable:
options = lib.units.Options(
payload.options,
option_type=lib.units.create_enum(
"PickupOptions",
# fmt: off
{
"sapient_carrier": lib.OptionEnum("sapient_carrier"),
"sapient_shipment_id": lib.OptionEnum("sapient_shipment_id"),
},
# fmt: on
),
)

# map data to convert karrio model to sapient specific type
request = dict(
shipmentId=options.sapient_shipment_id.state,
carrier=options.sapient_carrier.state or settings.carrier_code,
)

return lib.Serializable(request, lib.to_dict)
Loading

0 comments on commit 0343ba4

Please sign in to comment.