Skip to main content

Extension

Custom Carrier

The question we are most asked is: How to add a custom carrier?

We have experimented and studied approximately ~100 shipping carriers API/web services to design the karrio structure as it is.

The good news is that making it easy to add a custom carrier fits perfectly with karrio' vision.

caution

To integrate a custom carrier at this stage, you will need:

  • A minimal knowledge of Python
  • Write code to extend the abstract classes
  • Use the plugin/extension structure available
  • Read and Understand the carrier' API documentation

Steps to integrate a custom carrier

  1. Generate Python data types from the carrier API schemas

  2. Create a karrio extension package

info

The package naming convention for extensions is karrio.[carrier_name]


Extension anatomy

Considering the vision we aimed to achieve with karrio, the codebase has been modularized with a clear separation of concerns to decouple the carrier integration from the interface abstraction. Additionally, each carrier integration is done in an isolated self-contained Python package.

As a result, we have a very modular ecosystem where one can only select the carrier integrations of interest without carrying the whole codebase.

Most importantly, this flexibility allows the integration of additional carrier services under the karrio umbrella.

info

karrio makes shipping API integration easy for a single carrier and in a multi-carrier scenario, the value is exponential.

Module convention

Two modules are required to create a karrio extension.

karrio.mappers.[carrier_name]"

This is where the karrio abstract classes are implemented. Also, the Metadata require to identified the extension is also provided there.

on runtime, karrio retrieves all mappers by going trought the karrio.mappers modules

karrio.providers.[carrier_name]"

This is where the mapping between karrio Unified API data is mapped on the carrier data type corresponding requests

extension signature

The Mapper is the cornerstone ofkarrio 's abstraction. A Metadata declared at karrio.mappers.[carrier_name].__init__ specifies the integration classes required to define a karrio compatible extension.

from karrio.core.metadata import Metadata

from karrio.mappers.[carrier_name].mapper import Mapper
from karrio.mappers.[carrier_name].proxy import Proxy
from karrio.mappers.[carrier_name].settings import Settings
import karrio.providers.[carrier_name].units as units


METADATA = Metadata(
id="[carrier_name]", # e.g: "ddp_uk"
label="[Carrier Name]", # e.g: "DDP UK"

# Integrations
Mapper=Mapper,
Proxy=Proxy,
Settings=Settings,

# Data Units (Optional...)
options=units.OptionCode, # Enum of Shipping options supported by the carrier
package_presets=units.PackagePresets, # Enum of parcel presets/templates
services=units.ServiceType, # Enum of Shipping services supported by the carrier

is_hub=False # True if the carrier is a hub like (EasyPost, Shippo, Postmen...)
)

file structure

The carrier extension package folder structure looks like this

extensions/[carrier_name]/
├── karrio
│ ├── mappers
│ │ └── [carrier_name]
│ │ ├── __init__.py
│ │ ├── mapper.py
│ │ ├── proxy.py
│ │ └── settings.py
│ └── providers
│ └── [carrier_name]
│ ├── __init__.py
│ ├── address.py
│ ├── error.py
│ ├── pickup
│ │ ├── __init__.py
│ │ ├── cancel.py
│ │ ├── create.py
│ │ └── update.py
│ ├── rate.py
│ ├── shipment
│ │ ├── __init__.py
│ │ ├── cancel.py
│ │ └── create.py
│ ├── tracking.py
│ ├── units.py
│ └── utils.py
└── setup.py
info

Note that pickup and shipment modules are directories since there are often many sub to integrate such as create, cancel...

warning

We will eventually create a script to bootstrap the extension package.


Mappers implementation

The mapper function implementations consists of instantiating carrier specific request data types assigning

# Import karrio unified API models
from karrio.core.models import PickupRequest

# Import requirements from the DHL generated data types library (py-dhl)
from pydhl.book_pickup_global_req_3_0 import BookPURequest, MetaData
from pydhl.pickupdatatypes_global_3_0 import (
Requestor,
Place,
Pickup,
WeightSeg,
RequestorContact,
)

def pickup_request(payload: PickupRequest, settings: Settings) -> Serializable[BookPURequest]:
weight = 0.00 # Total weight calculated from the sum of `payload.parcels[].weights`
# ...
request = BookPURequest(
Request=settings.Request(
MetaData=MetaData(SoftwareName="XMLPI", SoftwareVersion=3.0)
),
schemaVersion=3.0,
RegionCode="AM",
Requestor=Requestor(
AccountNumber=settings.account_number,
AccountType="D",
RequestorContact=RequestorContact(
PersonName=payload.address.person_name,
Phone=payload.address.phone_number,
PhoneExtension=None,
),
CompanyName=payload.address.company_name,
),
Place=Place(
City=payload.address.city,
StateCode=payload.address.state_code,
PostalCode=payload.address.postal_code,
CompanyName=payload.address.company_name,
CountryCode=payload.address.country_code,
PackageLocation=payload.package_location,
LocationType="R" if payload.address.residential else "B",
Address1=payload.address.address_line1,
Address2=payload.address.address_line2,
),
PickupContact=RequestorContact(
PersonName=payload.address.person_name, Phone=payload.address.phone_number
),
Pickup=Pickup(
Pieces=len(payload.parcels),
PickupDate=payload.pickup_date,
ReadyByTime=f"{payload.ready_time}:00",
CloseTime=f"{payload.closing_time}:00",
SpecialInstructions=[payload.instruction],
RemotePickupFlag="Y",
weight=WeightSeg(
Weight=sum(p.weight for p in payload.parcel),
WeightUnit="LB"
),
),
ShipmentDetails=None,
ConsigneeDetails=None,
)

return Serializable(request)
info

The mapping function instantiates the carrier data types like a tree to allow a global view and simplify the mental relation between the code and the formatted data output schema.

Generated schema data types

To keep to robustness and simplify the maintenance of the codebase, In karrio, we use Python data types reflecting the schemas of carriers we want to integrate. That said, defining every schema object's structure can be tedious, long and unproductive. Therefore, code generators are used to generate Python data types based of the schema format definition.

  • For XML and SOAP services, generateDs is used to generate .xsd files into Python data types
info

Check the generate.sh file to see how genereDs is used.

  • For JSON services, quicktype is used to generate .json files into Python data types. We then use jstruct as a replacement for python dataclass to add automated nested object instantiation.