Skip to content

WIP Freightcom new REST API #766

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 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
25 changes: 17 additions & 8 deletions modules/connectors/freightcom/generate
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
SCHEMAS=./vendor/schemas
SCHEMAS=./schemas
LIB_MODULES=./karrio/schemas/freightcom
echo `pwd`
find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \;
touch "${LIB_MODULES}/__init__.py"

generateDS --no-namespace-defs -o "${LIB_MODULES}/quote_request.py" $SCHEMAS/quote_request.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/quote_reply.py" $SCHEMAS/quote_reply.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/shipping_request.py" $SCHEMAS/shipping_request.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/shipping_reply.py" $SCHEMAS/shipping_reply.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/error.py" $SCHEMAS/error.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/shipment_cancel_request.py" $SCHEMAS/shipment_cancel_request.xsd
generateDS --no-namespace-defs -o "${LIB_MODULES}/shipment_cancel_reply.py" $SCHEMAS/shipment_cancel_reply.xsd
quicktype() {
echo "Generating $1..."
docker run -it --rm --name quicktype -v $PWD:/app -e SCHEMAS=/app/schemas -e LIB_MODULES=/app/karrio/schemas/freightcom \
karrio/tools /quicktype/script/quicktype --no-uuids --no-date-times --no-enums --src-lang json --lang jstruct \
--all-properties-optional --type-as-suffix $@
}


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}/error_response.json" --out="${LIB_MODULES}/error_response.py"
quicktype --src="${SCHEMAS}/shipping_request.json" --out="${LIB_MODULES}/shipping_request.py"
quicktype --src="${SCHEMAS}/shipping_response.json" --out="${LIB_MODULES}/shipping_response.py"
quicktype --src="${SCHEMAS}/pickup_request.json" --out="${LIB_MODULES}/pickup_request.py"
quicktype --src="${SCHEMAS}/tracking_response.json" --out="${LIB_MODULES}/tracking_response.py"
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
# Data Units
options=units.ShippingOption,
services=units.ShippingService,
hub_carriers=units.CARRIER_IDS,
connection_configs = units.ConnectionConfig,
)
43 changes: 14 additions & 29 deletions modules/connectors/freightcom/karrio/mappers/freightcom/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,39 @@
from karrio.api.mapper import Mapper as BaseMapper
from karrio.mappers.freightcom.settings import Settings
from karrio.core.utils.serializable import Deserializable, Serializable
from karrio.core.models import (
RateRequest,
ShipmentRequest,
ShipmentDetails,
RateDetails,
Message,
ShipmentCancelRequest,
ConfirmationDetails,
)
from karrio.providers.freightcom import (
parse_quote_reply,
quote_request,
parse_shipping_reply,
shipping_request,
shipment_cancel_request,
parse_shipment_cancel_reply,
)
import karrio.core.models as models
import karrio.providers.freightcom as provider


class Mapper(BaseMapper):
settings: Settings

# Request Mappers

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

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

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

# Response Parsers

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

def parse_shipment_response(
self, response: Deserializable
) -> Tuple[ShipmentDetails, List[Message]]:
return parse_shipping_reply(response, self.settings)
) -> Tuple[models.ShipmentDetails, List[models.Message]]:
return provider.parse_shipment_response(response, self.settings)

def parse_cancel_shipment_response(
self, response: Deserializable
) -> Tuple[ConfirmationDetails, List[Message]]:
return parse_shipment_cancel_reply(response, self.settings)
) -> Tuple[models.ConfirmationDetails, List[models.Message]]:
return provider.parse_shipment_cancel_response(response, self.settings)
129 changes: 99 additions & 30 deletions modules/connectors/freightcom/karrio/mappers/freightcom/proxy.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,107 @@
from karrio.core.utils import request as http, XP
from karrio.api.proxy import Proxy as BaseProxy
"""Karrio Freightcom client proxy."""

import time
import karrio.lib as lib
import karrio.api.proxy as proxy
from karrio.mappers.freightcom.settings import Settings
from karrio.core.utils.serializable import Serializable, Deserializable

MAX_RETRIES = 10
POLL_INTERVAL = 2 # seconds

class Proxy(BaseProxy):
class Proxy(proxy.Proxy):
settings: Settings

def get_rates(self, request: Serializable) -> Deserializable:
response = http(
url=self.settings.server_url,
data=request.serialize(),
trace=self.trace_as("xml"),
method="POST",
headers={"Content-Type": "application/xml"},
def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
# Step 1: Submit rate request and get quote ID
response = self._send_request(
path="/rate", request=lib.Serializable(request.value, lib.to_json)
)

rate_id = lib.to_dict(response).get('request_id')
if not rate_id:
return lib.Deserializable(response, lib.to_dict)

# Step 2: Poll for rate results
for _ in range(MAX_RETRIES):
status_res = self._send_request(
path=f"/rate/{rate_id}",
method="GET"
)

status = lib.to_dict(status_res).get('status', {}).get('done', False)

if status: # Quote is complete
return lib.Deserializable(status_res, lib.to_dict)

time.sleep(POLL_INTERVAL)

# If we exceed max retries
return lib.Deserializable({
'message': 'Rate calculation timed out'
}, lib.to_dict)

def create_shipment(self, request: lib.Serializable) -> lib.Deserializable:

response = self._send_request(
path="/shipment", request=lib.Serializable(request.value, lib.to_json)
)

shipment_id = lib.to_dict(response).get('id')
if not shipment_id:
return lib.Deserializable(response, lib.to_dict)


# Step 2: retry because api return empty bytes if done to fast
time.sleep(1)
for _ in range(MAX_RETRIES):

shipment_response = self._send_request(path=f"/shipment/{shipment_id}", method="GET")
shipment_res = lib.failsafe(lambda :lib.to_dict(shipment_response)) or lib.decode(shipment_response)

if shipment_res: # is complete
return lib.Deserializable(shipment_res, lib.to_dict, request.ctx)

time.sleep(POLL_INTERVAL)

# If we exceed max retries
return lib.Deserializable({
'message': 'timed out'
}, lib.to_dict)


def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]:
response = self._send_request(path=f"/shipment/{request.serialize()}/tracking-events")

return lib.Deserializable(response, lib.to_dict)

# TODO: not sure how this can be a dynamic unit Enum, and cached for now i hard code the id in the ship request
def _get_payments_methods(self) -> lib.Deserializable[str]:
response = self._send_request(
path="/finance/payment-methods",
method="GET"
)
return Deserializable(response, XP.to_xml)

def create_shipment(self, request: Serializable) -> Deserializable:
response = http(
url=self.settings.server_url,
data=request.serialize(),
trace=self.trace_as("xml"),
method="POST",
headers={"Content-Type": "application/xml"},
return lib.Deserializable(response, lib.to_dict)

def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable:
response = self._send_request(
path=f"/shipment/{request.serialize()}", method="DELETE"
)
return Deserializable(response, XP.to_xml)

def cancel_shipment(self, request: Serializable) -> Deserializable:
response = http(
url=self.settings.server_url,
data=request.serialize(),
trace=self.trace_as("xml"),
method="POST",
headers={"Content-Type": "application/xml"},
return lib.Deserializable(response if any(response) else "{}", lib.to_dict)

def _send_request(
self, path: str, request: lib.Serializable = None, method: str = "POST"
) -> str:

data: dict = dict(data=request.serialize()) if request is not None else dict()
return lib.request(
**{
"url": f"{self.settings.server_url}{path}",
"trace": self.trace_as("json"),
"method": method,
"headers": {
"Content-Type": "application/json",
"Authorization": self.settings.api_key,
},
**data,
}
)
return Deserializable(response, XP.to_xml)
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Karrio freightcom connection settings."""

import attr
from karrio.providers.freightcom.utils import Settings as BaseSettings
import karrio.providers.freightcom.utils as provider_utils
from karrio.providers.freightcom.units import PaymentMethodType


@attr.s(auto_attribs=True)
class Settings(BaseSettings):
class Settings(provider_utils.Settings):
"""Freightcom connection settings."""
#carrier specific API connection properties here
api_key: str
payment_method_type: PaymentMethodType = "net_terms"

username: str
password: str

# generic properties
id: str = None
test_mode: bool = False
carrier_id: str = "freightcom"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from karrio.providers.freightcom.quote import parse_quote_reply, quote_request
from karrio.providers.freightcom.shipping import (
parse_shipping_reply,
shipping_request,

from karrio.providers.freightcom.rate import parse_rate_response, rate_request
from karrio.providers.freightcom.shipment import (
parse_shipment_response,
shipment_request,
parse_shipment_cancel_response,
shipment_cancel_request,
)
from karrio.providers.freightcom.void_shipment import shipment_cancel_request, parse_shipment_cancel_reply

# from karrio.providers.eshipper.tracking import (
# parse_tracking_response,
# tracking_request,
# )
55 changes: 22 additions & 33 deletions modules/connectors/freightcom/karrio/providers/freightcom/error.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,29 @@
from typing import List
from karrio.schemas.freightcom.error import ErrorType
from karrio.schemas.freightcom.quote_reply import CarrierErrorMessageType
from karrio.core.models import Message
from karrio.core.utils import Element, XP
from karrio.providers.freightcom.utils import Settings
import typing
import karrio.core.models as models
import karrio.providers.freightcom.utils as provider_utils

def parse_error_response(
response: dict,
settings: provider_utils.Settings,
**kwargs,
) -> typing.List[models.Message]:
responses = response if isinstance(response, list) else [response]

def parse_error_response(response: Element, settings: Settings) -> List[Message]:
errors = XP.find("Error", response, ErrorType)
carrier_errors = XP.find("CarrierErrorMessage", response, CarrierErrorMessageType)
errors = [
*[_ for _ in responses if _.get("message")],
]

return [
*[_extract_error(er, settings) for er in errors if er.Message != ""],
*[
_extract_carrier_error(er, settings)
for er in carrier_errors
if er.errorMessage0 != ""
],
models.Message(
carrier_id=settings.carrier_id,
carrier_name=settings.carrier_name,
message=error.get("message"),
details={
**kwargs,
**(error.get('data', {}))
},
)
for error in errors
]


def _extract_carrier_error(
error: CarrierErrorMessageType, settings: Settings
) -> Message:
return Message(
code="CarrierErrorMessage",
carrier_name=settings.carrier_name,
carrier_id=settings.carrier_id,
message=error.errorMessage0,
)


def _extract_error(error: ErrorType, settings: Settings) -> Message:
return Message(
code="Error",
carrier_name=settings.carrier_name,
carrier_id=settings.carrier_id,
message=error.Message,
)
Loading