Skip to content
Open
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
31 changes: 31 additions & 0 deletions modules/connectors/ninja_van/README.md
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
21 changes: 21 additions & 0 deletions modules/connectors/ninja_van/generate
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"
19 changes: 19 additions & 0 deletions modules/connectors/ninja_van/karrio/mappers/ninja_van/__init__.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
)
53 changes: 53 additions & 0 deletions modules/connectors/ninja_van/karrio/mappers/ninja_van/mapper.py
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)
101 changes: 101 additions & 0 deletions modules/connectors/ninja_van/karrio/mappers/ninja_van/proxy.py
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")
21 changes: 21 additions & 0 deletions modules/connectors/ninja_van/karrio/mappers/ninja_van/settings.py
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,
)
35 changes: 35 additions & 0 deletions modules/connectors/ninja_van/karrio/providers/ninja_van/error.py
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
58 changes: 58 additions & 0 deletions modules/connectors/ninja_van/karrio/providers/ninja_van/rate.py
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
),
)
Comment on lines +23 to +39
Copy link
Member

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

{
  "data": {
    "total_fee": 90000
  }
}

you need to collect the data and forward to the Karrio RateDetails



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
Copy link
Member

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 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

request = [
sendle.ProductRequestType(
sender_address_line1=shipper.address_line1,
sender_address_line2=shipper.address_line2,
sender_suburb=shipper.city,
sender_postcode=shipper.postal_code,
sender_country=shipper.country_code,
receiver_address_line1=recipient.address_line1,
receiver_address_line2=recipient.address_line2,
receiver_suburb=recipient.city,
receiver_postcode=recipient.postal_code,
receiver_country=recipient.country_code,
weight_value=package.weight.KG,
weight_units=units.WeightUnit.KG.name.lower(),
volume_value=package.volume.m3,
volume_units=units.VolumeUnit.m3.name,
length_value=package.length.CM,
width_value=package.width.CM,
height_value=package.height.CM,
dimension_units=units.DimensionUnit.CM.name.lower(),
)
for package in packages
]

Notice the types generated by the schema are imported at the top

import karrio.schemas.sendle.product_request as sendle
import karrio.schemas.sendle.product_response as rating

The request is pretty much a mirror of the schema file

{
"sender_address_line1": "",
"sender_address_line2": "",
"sender_suburb": "",
"sender_postcode": "",
"sender_country": "",
"receiver_address_line1": "",
"receiver_address_line2": "",
"receiver_suburb": "",
"receiver_postcode": "",
"receiver_country": "",
"weight_value": 0.0,
"weight_units": "kg",
"volume_value": "0.0",
"volume_units": "m3",
"length_value": 0.0,
"width_value": 0.0,
"height_value": 0.0,
"dimension_units": "cm"
}


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,
)
Loading