Skip to content
Closed
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
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)
128 changes: 98 additions & 30 deletions modules/connectors/freightcom/karrio/mappers/freightcom/proxy.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,106 @@
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)
Comment thread
danh91 marked this conversation as resolved.

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)

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,16 @@
"""Karrio freightcom connection settings."""

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


@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

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