-
-
Notifications
You must be signed in to change notification settings - Fork 150
Feat/add ninja van carrier integration #596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
14feaf9
52fdad4
abc057c
6333eef
78ea89c
0659066
19ef8b5
481f340
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
|
|
||
| # karrio.ninja_van | ||
|
|
||
| This package is a Ninja Van extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK. | ||
|
|
||
| ## Requirements | ||
|
|
||
| `Python 3.7+` | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| pip install karrio.ninja_van | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| import karrio | ||
| from karrio.mappers.ninja_van.settings import Settings | ||
|
|
||
|
|
||
| # Initialize a carrier gateway | ||
| ninja_van = karrio.gateway["ninja_van"].create( | ||
| Settings( | ||
| ... | ||
| ) | ||
| ) | ||
| ``` | ||
|
|
||
| Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| SCHEMAS=./schemas | ||
| LIB_MODULES=./karrio/schemas/ninja_van | ||
| 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/ninja_van \ | ||
| 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}/cancel_shipment_request.json" --out="${LIB_MODULES}/cancel_shipment_request.py" | ||
| quicktype --src="${SCHEMAS}/cancel_shipment_response.json" --out="${LIB_MODULES}/cancel_shipment_response.py" | ||
| quicktype --src="${SCHEMAS}/create_shipment_request.json" --out="${LIB_MODULES}/create_shipment_request.py" | ||
| quicktype --src="${SCHEMAS}/create_shipment_response.json" --out="${LIB_MODULES}/create_shipment_response.py" | ||
| quicktype --src="${SCHEMAS}/error_response.json" --out="${LIB_MODULES}/error_response.py" | ||
| quicktype --src="${SCHEMAS}/rate_request.json" --out="${LIB_MODULES}/rate_request.py" | ||
| quicktype --src="${SCHEMAS}/rate_response.json" --out="${LIB_MODULES}/rate_response.py" | ||
| quicktype --src="${SCHEMAS}/tracking_request.json" --out="${LIB_MODULES}/tracking_request.py" | ||
| quicktype --src="${SCHEMAS}/tracking_response.json" --out="${LIB_MODULES}/tracking_response.py" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
|
|
||
| from karrio.core.metadata import Metadata | ||
|
|
||
| from karrio.mappers.ninja_van.mapper import Mapper | ||
| from karrio.mappers.ninja_van.proxy import Proxy | ||
| from karrio.mappers.ninja_van.settings import Settings | ||
| import karrio.providers.ninja_van.units as units | ||
|
|
||
|
|
||
| METADATA = Metadata( | ||
| id="ninja_van", | ||
| label="Ninja Van", | ||
| # Integrations | ||
| Mapper=Mapper, | ||
| Proxy=Proxy, | ||
| Settings=Settings, | ||
| # Data Units | ||
| is_hub=False | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| """Karrio Ninja Van client mapper.""" | ||
|
|
||
| import typing | ||
| import karrio.lib as lib | ||
| import karrio.api.mapper as mapper | ||
| import karrio.core.models as models | ||
| import karrio.providers.ninja_van as provider | ||
| import karrio.mappers.ninja_van.settings as provider_settings | ||
|
|
||
|
|
||
| class Mapper(mapper.Mapper): | ||
| settings: provider_settings.Settings | ||
|
|
||
| def create_rate_request( | ||
| self, payload: models.RateRequest | ||
| ) -> lib.Serializable: | ||
| return provider.rate_request(payload, self.settings) | ||
|
|
||
| def create_tracking_request( | ||
| self, payload: models.TrackingRequest | ||
| ) -> lib.Serializable: | ||
| return provider.tracking_request(payload, self.settings) | ||
|
|
||
| def create_shipment_request( | ||
| self, payload: models.ShipmentRequest | ||
| ) -> lib.Serializable: | ||
| return provider.shipment_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_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_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) | ||
|
|
||
| def parse_tracking_response( | ||
| self, response: lib.Deserializable[str] | ||
| ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]: | ||
| return provider.parse_tracking_response(response, self.settings) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| """Karrio Ninja Van client proxy.""" | ||
|
|
||
| import karrio.lib as lib | ||
| import karrio.api.proxy as proxy | ||
| import logging | ||
| import json | ||
| from karrio.lib import Deserializable | ||
| import karrio.mappers.ninja_van.settings as provider_settings | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| class Proxy(proxy.Proxy): | ||
| settings: provider_settings.Settings | ||
|
|
||
| def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]: | ||
| response = lib.request( | ||
| url=f"{self.settings.server_url}/ID/1.0/public/price", | ||
| data=lib.to_json(request.serialize()), | ||
| trace=self.trace_as("json"), | ||
| method="POST", | ||
| headers={ | ||
| "Accept": "application/json", | ||
| "Content-type": "application/json", | ||
| "Authorization": f"Bearer {self.settings.access_token}", | ||
| }, | ||
| ) | ||
| return lib.Deserializable(response, lib.to_dict) | ||
|
|
||
| def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: | ||
| response = lib.request( | ||
| url=f"{self.settings.server_url}/{self.settings.account_country_code}/4.2/orders", | ||
| data=lib.to_json(request.serialize()), | ||
| trace=self.trace_as("json"), | ||
| method="POST", | ||
| headers={ | ||
| "Accept": "application/json", | ||
| "Content-type": "application/json", | ||
| "Authorization": f"Bearer {self.settings.access_token}", | ||
| }, | ||
| ) | ||
|
|
||
| return lib.Deserializable(response, lib.to_dict) | ||
|
|
||
| def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: | ||
| payload = request.serialize() | ||
| tracking_number = payload["options"]["tracking_number"] | ||
| response = lib.request( | ||
| url=f"{self.settings.server_url}/{self.settings.account_country_code}/2.2/orders/{tracking_number}", | ||
| data=lib.to_json(request.serialize()), | ||
| trace=self.trace_as("json"), | ||
| method="DELETE", | ||
| headers={ | ||
| "Accept": "application/json", | ||
| "Content-type": "application/json", | ||
| "Authorization": f"Bearer {self.settings.access_token}", | ||
| }, | ||
| ) | ||
| return lib.Deserializable(response, lib.to_dict) | ||
|
|
||
| def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]: | ||
| payload = request.serialize() | ||
| tracking_numbers = payload.get('tracking_numbers', []) | ||
| tracking_number = "&".join([f"tracking_number={tn}" for tn in tracking_numbers]) | ||
| response = lib.request( | ||
| url=f"{self.settings.server_url}/{self.settings.account_country_code}/1.0/orders/tracking-events?{tracking_number}", | ||
| data=lib.to_json(request.serialize()), | ||
| trace=self.trace_as("json"), | ||
| method="GET", | ||
| headers={ | ||
| "Accept": "application/json", | ||
| "Content-type": "application/json", | ||
| "Authorization": f"Bearer {self.settings.access_token}", | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| return lib.Deserializable(response, lib.to_dict) | ||
|
|
||
| def get_waybill(self, request: lib.Serializable) -> bytes: | ||
| payload = request.serialize() | ||
| tracking_number = payload.get('tracking_number') | ||
|
|
||
| if not tracking_number: | ||
| raise ValueError("A tracking number must be provided") | ||
|
|
||
| response = lib.request( | ||
| url=f"{self.settings.server_url}/{self.settings.country_code}/2.0/reports/waybill?tracking_number={tracking_number}", | ||
| data=None, # GET request should not have a body | ||
| trace=self.trace_as("json"), | ||
| method="GET", | ||
| headers={ | ||
| "Accept": "application/pdf", | ||
| "Authorization": f"Bearer {self.settings.access_token}", | ||
| }, | ||
| ) | ||
|
|
||
| # Ensure the response is successful and is a PDF | ||
| if response.headers.get('Content-Type') == 'application/pdf' and response.status_code == 200: | ||
| return response.content | ||
| else: | ||
| raise ValueError("Failed to retrieve PDF") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """Karrio Ninja Van client settings.""" | ||
|
|
||
| import attr | ||
| from karrio.providers.ninja_van.utils import Settings as BaseSettings | ||
|
|
||
|
|
||
| @attr.s(auto_attribs=True) | ||
| class Settings(BaseSettings): | ||
| """Ninja Van connection settings.""" | ||
|
|
||
| # required carrier specific properties | ||
| client_id: str = None | ||
| client_secret: str = None | ||
|
|
||
| # generic properties | ||
| id: str = None | ||
| test_mode: bool = False | ||
| carrier_id: str = "ninja_van" | ||
| account_country_code: str = "SG" | ||
| metadata: dict = {} | ||
| config: dict = {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
|
|
||
| from karrio.providers.ninja_van.utils import Settings | ||
| from karrio.providers.ninja_van.rate import parse_rate_response, rate_request | ||
| from karrio.providers.ninja_van.shipment import ( | ||
| parse_shipment_cancel_response, | ||
| parse_shipment_response, | ||
| shipment_cancel_request, | ||
| shipment_request, | ||
| ) | ||
| from karrio.providers.ninja_van.tracking import ( | ||
| parse_tracking_response, | ||
| tracking_request, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| """Karrio Ninja Van error parser.""" | ||
| import typing | ||
| import karrio.lib as lib | ||
| import karrio.core.models as models | ||
| import karrio.providers.ninja_van.utils as provider_utils | ||
|
|
||
|
|
||
| def parse_error_response( | ||
| response: typing.Union[typing.List[dict], dict], | ||
| settings: provider_utils.Settings, | ||
| **details, | ||
| ) -> typing.List[models.Message]: | ||
| responses = response if isinstance(response, list) else [response] | ||
| errors: typing.List[dict] = sum( | ||
| [ | ||
| result["error"]["details"] | ||
| if "error" in result and "details" in result["error"] | ||
| else [] | ||
| for result in responses | ||
| ], | ||
| [], | ||
| ) | ||
|
|
||
| messages: typing.List[models.Message] = [ | ||
| models.Message( | ||
| carrier_name=settings.carrier_name, | ||
| carrier_id=settings.carrier_id, | ||
| code=error.get("reason"), | ||
| message=error.get("message"), | ||
| details=details, | ||
| ) | ||
| for error in errors | ||
| ] | ||
|
|
||
| return messages |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import typing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.lib as lib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.core.units as units | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.core.models as models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.providers.ninja_van.error as error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.providers.ninja_van.utils as provider_utils | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import karrio.providers.ninja_van.units as provider_units | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def parse_rate_response( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _response: lib.Deserializable[dict], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings: provider_utils.Settings, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = _response.deserialize() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages = error.parse_error_response(response, settings) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rates = [_extract_details(rate, settings) for rate in response] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return rates, messages | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _extract_details( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: dict, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings: provider_utils.Settings, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> models.RateDetails: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rate = None # parse carrier rate type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return models.RateDetails( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| carrier_id=settings.carrier_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| carrier_name=settings.carrier_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service="", # extract service from rate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_charge=lib.to_money(0.0), # extract the rate total rate cost | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currency="", # extract the rate pricing currency | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| transit_days=0, # extract the rate transit days | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| meta=dict( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service_name="", # extract the rate service human readable name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def rate_request( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload: models.RateRequest, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings: provider_utils.Settings, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> lib.Serializable: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shipper = lib.to_address(payload.shipper) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recipient = lib.to_address(payload.recipient) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| packages = lib.to_packages(payload.parcels) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| services = lib.to_services(payload.services, provider_units.ShippingService) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options = lib.to_shipping_options( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| payload.options, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package_options=packages.options, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # map data to convert karrio model to ninja_van specific type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request = None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be implemented as well. Once you have the schema files, this is where you use them to create the request for the implementation. Here is another good example from Sendle karrio/modules/connectors/sendle/karrio/providers/sendle/rate.py Lines 97 to 119 in 12792cf
Notice the types generated by the schema are imported at the top
The request is pretty much a mirror of the schema file karrio/modules/connectors/sendle/schemas/product_request.json Lines 1 to 20 in 12792cf
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return lib.Serializable(request, lib.to_dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
|
|
||
| from karrio.providers.ninja_van.shipment.create import ( | ||
| parse_shipment_response, | ||
| shipment_request, | ||
| ) | ||
| from karrio.providers.ninja_van.shipment.cancel import ( | ||
| parse_shipment_cancel_response, | ||
| shipment_cancel_request, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be implemented. Since the Tariff API returns
you need to collect the data and forward to the Karrio RateDetails