karrio-boxknight 2025.5rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from karrio.mappers.boxknight.mapper import Mapper
2
+ from karrio.mappers.boxknight.proxy import Proxy
3
+ from karrio.mappers.boxknight.settings import Settings
@@ -0,0 +1,50 @@
1
+ """Karrio BoxKnight client mapper."""
2
+
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.api.mapper as mapper
6
+ import karrio.core.models as models
7
+ import karrio.providers.boxknight as provider
8
+ import karrio.mappers.boxknight.settings as provider_settings
9
+
10
+
11
+ class Mapper(mapper.Mapper):
12
+ settings: provider_settings.Settings
13
+
14
+ def create_rate_request(self, payload: models.RateRequest) -> lib.Serializable:
15
+ return provider.rate_request(payload, self.settings)
16
+
17
+ def create_tracking_request(
18
+ self, payload: models.TrackingRequest
19
+ ) -> lib.Serializable:
20
+ return provider.tracking_request(payload, self.settings)
21
+
22
+ def create_shipment_request(
23
+ self, payload: models.ShipmentRequest
24
+ ) -> lib.Serializable:
25
+ return provider.shipment_request(payload, self.settings)
26
+
27
+ def create_cancel_shipment_request(
28
+ self, payload: models.ShipmentCancelRequest
29
+ ) -> lib.Serializable:
30
+ return provider.shipment_cancel_request(payload, self.settings)
31
+
32
+ def parse_cancel_shipment_response(
33
+ self, response: lib.Deserializable
34
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
35
+ return provider.parse_shipment_cancel_response(response, self.settings)
36
+
37
+ def parse_rate_response(
38
+ self, response: lib.Deserializable
39
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
40
+ return provider.parse_rate_response(response, self.settings)
41
+
42
+ def parse_shipment_response(
43
+ self, response: lib.Deserializable
44
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
45
+ return provider.parse_shipment_response(response, self.settings)
46
+
47
+ def parse_tracking_response(
48
+ self, response: lib.Deserializable
49
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
50
+ return provider.parse_tracking_response(response, self.settings)
@@ -0,0 +1,82 @@
1
+ """Karrio BoxKnight client proxy."""
2
+
3
+ import typing
4
+ import base64
5
+ import karrio.lib as lib
6
+ import karrio.api.proxy as proxy
7
+ import karrio.mappers.boxknight.settings as provider_settings
8
+
9
+
10
+ class Proxy(proxy.Proxy):
11
+ settings: provider_settings.Settings
12
+
13
+ def get_rates(self, request: lib.Serializable) -> lib.Deserializable:
14
+ response = lib.request(
15
+ url=f"{self.settings.server_url}/rates",
16
+ data=request.serialize(),
17
+ trace=self.trace_as("json"),
18
+ method="POST",
19
+ headers={"Authorization": self.settings.auth_token},
20
+ )
21
+
22
+ return lib.Deserializable(response, lib.to_dict)
23
+
24
+ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable:
25
+ payload = request.serialize()
26
+ result = lib.to_dict(
27
+ lib.request(
28
+ url=f"{self.settings.server_url}/orders",
29
+ data=payload["order"],
30
+ trace=self.trace_as("json"),
31
+ method="POST",
32
+ headers={"Authorization": self.settings.auth_token},
33
+ )
34
+ )
35
+
36
+ response = (
37
+ dict(
38
+ order_id=result["id"],
39
+ label_type=payload["label_type"],
40
+ service=payload["order"]["service"],
41
+ label=lib.request(
42
+ url=f"{self.settings.server_url}/labels/{result['id']}?format={payload['label_type']}",
43
+ decoder=lambda b: base64.encodebytes(b).decode("utf-8"),
44
+ headers={"Authorization": self.settings.auth_token},
45
+ trace=self.trace_as("json"),
46
+ ),
47
+ )
48
+ if result.get("error") is None
49
+ else result
50
+ )
51
+
52
+ return lib.Deserializable(response)
53
+
54
+ def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable:
55
+ response = lib.request(
56
+ url=f"{self.settings.server_url}/orders/{request.serialize()['order_id']}",
57
+ trace=self.trace_as("json"),
58
+ method="DELETE",
59
+ headers={"Authorization": self.settings.auth_token},
60
+ )
61
+
62
+ return lib.Deserializable(response, lib.to_dict)
63
+
64
+ def get_tracking(self, requests: lib.Serializable) -> lib.Deserializable:
65
+ track = lambda data: (
66
+ data["order_id"],
67
+ lib.request(
68
+ url=f"{self.settings.server_url}/orders/{data['order_id']}",
69
+ trace=self.trace_as("json"),
70
+ method="GET",
71
+ headers={"Authorization": self.settings.auth_token},
72
+ ),
73
+ )
74
+
75
+ responses: typing.List[typing.Tuple[str, str]] = lib.run_asynchronously(
76
+ track, requests.serialize()
77
+ )
78
+
79
+ return lib.Deserializable(
80
+ responses,
81
+ lambda response: [(key, lib.to_dict(res)) for key, res in response],
82
+ )
@@ -0,0 +1,23 @@
1
+ """Karrio BoxKnight client settings."""
2
+
3
+ import attr
4
+ import jstruct
5
+ import karrio.lib as lib
6
+ import karrio.providers.boxknight.utils as provider_utils
7
+
8
+
9
+ @attr.s(auto_attribs=True)
10
+ class Settings(provider_utils.Settings):
11
+ """BoxKnight connection settings."""
12
+
13
+ # required carrier specific properties
14
+ username: str
15
+ password: str
16
+
17
+ # generic properties
18
+ id: str = None
19
+ test_mode: bool = False
20
+ carrier_id: str = "boxknight"
21
+ account_country_code: str = "CA"
22
+ metadata: dict = {}
23
+ config: dict = {}
@@ -0,0 +1,22 @@
1
+ import karrio.core.metadata as metadata
2
+ import karrio.mappers.boxknight as mappers
3
+ import karrio.providers.boxknight.units as units
4
+
5
+
6
+ METADATA = metadata.PluginMetadata(
7
+ status="beta",
8
+ id="boxknight",
9
+ label="BoxKnight",
10
+ # Integrations
11
+ Mapper=mappers.Mapper,
12
+ Proxy=mappers.Proxy,
13
+ Settings=mappers.Settings,
14
+ # Data Units
15
+ is_hub=False,
16
+ services=units.ShippingService,
17
+ options=units.ShippingOption,
18
+ # New fields
19
+ website="https://www.boxknight.com/",
20
+ documentation="https://www.docs.boxknight.com/",
21
+ description="Specializes in same-day delivery at affordable prices for e-commerce retailers. Our mission is to get packages to your customers when they are actually home and as quickly as possible.",
22
+ )
@@ -0,0 +1,13 @@
1
+
2
+ from karrio.providers.boxknight.utils import Settings
3
+ from karrio.providers.boxknight.rate import parse_rate_response, rate_request
4
+ from karrio.providers.boxknight.shipment import (
5
+ parse_shipment_cancel_response,
6
+ parse_shipment_response,
7
+ shipment_cancel_request,
8
+ shipment_request,
9
+ )
10
+ from karrio.providers.boxknight.tracking import (
11
+ parse_tracking_response,
12
+ tracking_request,
13
+ )
@@ -0,0 +1,29 @@
1
+ import karrio.schemas.boxknight.error as boxknight
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.models as models
5
+ import karrio.providers.boxknight.utils as provider_utils
6
+
7
+
8
+ def parse_error_response(
9
+ response: typing.Union[dict, typing.List[dict]],
10
+ settings: provider_utils.Settings,
11
+ **kwargs,
12
+ ) -> typing.List[models.Message]:
13
+ responses = response if isinstance(response, list) else [response]
14
+ errors = [
15
+ lib.to_object(boxknight.Error, res)
16
+ for res in responses
17
+ if res.get("error") is not None
18
+ ]
19
+
20
+ return [
21
+ models.Message(
22
+ carrier_id=settings.carrier_id,
23
+ carrier_name=settings.carrier_name,
24
+ code=None,
25
+ message=error.error,
26
+ details={**kwargs},
27
+ )
28
+ for error in errors
29
+ ]
@@ -0,0 +1,91 @@
1
+ import karrio.schemas.boxknight.rate_request as boxknight
2
+ import karrio.schemas.boxknight.rate_response as rating
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.units as units
6
+ import karrio.core.models as models
7
+ import karrio.core.errors as errors
8
+ import karrio.providers.boxknight.error as error
9
+ import karrio.providers.boxknight.utils as provider_utils
10
+ import karrio.providers.boxknight.units as provider_units
11
+
12
+
13
+ def parse_rate_response(
14
+ _response: lib.Deserializable[typing.Union[dict, typing.List[dict]]],
15
+ settings: provider_utils.Settings,
16
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
17
+ response = _response.deserialize()
18
+ messages = error.parse_error_response(response, settings)
19
+ rates = [
20
+ _extract_details(rate, settings)
21
+ for rate in response
22
+ if isinstance(response, list)
23
+ ]
24
+
25
+ return rates, messages
26
+
27
+
28
+ def _extract_details(
29
+ data: dict,
30
+ settings: provider_utils.Settings,
31
+ ) -> models.RateDetails:
32
+ rate = lib.to_object(rating.Rate, data)
33
+ service = provider_units.ShippingService.map(rate.service)
34
+ transit_days = (
35
+ lib.to_date(rate.estimateTo, "%Y-%m-%d")
36
+ - lib.to_date(rate.estimateFrom, "%Y-%m-%d")
37
+ ).days
38
+
39
+ return models.RateDetails(
40
+ carrier_id=settings.carrier_id,
41
+ carrier_name=settings.carrier_name,
42
+ service=service.name_or_key,
43
+ total_charge=lib.to_money(rate.price),
44
+ currency=units.Currency.CAD.name,
45
+ transit_days=transit_days if transit_days > 0 else 1,
46
+ meta=dict(service_name=rate.name),
47
+ )
48
+
49
+
50
+ def rate_request(
51
+ payload: models.RateRequest,
52
+ settings: provider_utils.Settings,
53
+ ) -> lib.Serializable:
54
+ if (
55
+ payload.shipper.country_code is not None
56
+ and payload.shipper.country_code != units.Country.CA.name
57
+ ):
58
+ raise errors.OriginNotServicedError(payload.shipper.country_code)
59
+
60
+ if (
61
+ payload.recipient.country_code is not None
62
+ and payload.recipient.country_code != units.Country.CA.name
63
+ ):
64
+ raise errors.DestinationNotServicedError(payload.recipient.country_code)
65
+
66
+ packages = lib.to_packages(payload.parcels)
67
+
68
+ request = boxknight.RateRequest(
69
+ postalCode=payload.recipient.postal_code,
70
+ originPostalCode=payload.shipper.postal_code,
71
+ packages=[
72
+ boxknight.Package(
73
+ refNumber=package.parcel.reference_number or str(idx),
74
+ weightOptions=boxknight.WeightOptions(
75
+ weight=package.weight.value,
76
+ unit=package.weight_unit.value.lower(),
77
+ ),
78
+ sizeOptions=boxknight.SizeOptions(
79
+ length=package.length.value,
80
+ width=package.width.value,
81
+ height=package.height.value,
82
+ unit=provider_units.DimensionUnit.map(
83
+ package.dimension_unit.name
84
+ ).value,
85
+ ),
86
+ )
87
+ for idx, package in enumerate(packages, start=1)
88
+ ],
89
+ )
90
+
91
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,9 @@
1
+
2
+ from karrio.providers.boxknight.shipment.create import (
3
+ parse_shipment_response,
4
+ shipment_request,
5
+ )
6
+ from karrio.providers.boxknight.shipment.cancel import (
7
+ parse_shipment_cancel_response,
8
+ shipment_cancel_request,
9
+ )
@@ -0,0 +1,37 @@
1
+ import typing
2
+ import karrio.lib as lib
3
+ import karrio.core.models as models
4
+ import karrio.providers.boxknight.error as error
5
+ import karrio.providers.boxknight.utils as provider_utils
6
+ import karrio.providers.boxknight.units as provider_units
7
+
8
+
9
+ def parse_shipment_cancel_response(
10
+ _response: lib.Deserializable[dict],
11
+ settings: provider_utils.Settings,
12
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
13
+ response = _response.deserialize()
14
+ messages = error.parse_error_response(response, settings)
15
+ success = response.get("error") is None
16
+
17
+ confirmation = (
18
+ models.ConfirmationDetails(
19
+ carrier_id=settings.carrier_id,
20
+ carrier_name=settings.carrier_name,
21
+ operation="Cancel Shipment",
22
+ success=success,
23
+ )
24
+ if success
25
+ else None
26
+ )
27
+
28
+ return confirmation, messages
29
+
30
+
31
+ def shipment_cancel_request(
32
+ payload: models.ShipmentCancelRequest,
33
+ settings: provider_utils.Settings,
34
+ ) -> lib.Serializable:
35
+ request = dict(order_id=payload.shipment_identifier)
36
+
37
+ return lib.Serializable(request)
@@ -0,0 +1,134 @@
1
+ import karrio.schemas.boxknight.order_request as boxknight
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.units as units
5
+ import karrio.core.models as models
6
+ import karrio.core.errors as errors
7
+ import karrio.providers.boxknight.error as error
8
+ import karrio.providers.boxknight.utils as provider_utils
9
+ import karrio.providers.boxknight.units as provider_units
10
+
11
+
12
+ def parse_shipment_response(
13
+ _response: lib.Deserializable[dict],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
16
+ response = _response.deserialize()
17
+ messages = error.parse_error_response(response, settings)
18
+ shipment = (
19
+ _extract_details(response, settings) if response.get("error") is None else None
20
+ )
21
+
22
+ return shipment, messages
23
+
24
+
25
+ def _extract_details(
26
+ data: dict,
27
+ settings: provider_utils.Settings,
28
+ ) -> models.ShipmentDetails:
29
+ return models.ShipmentDetails(
30
+ carrier_id=settings.carrier_id,
31
+ carrier_name=settings.carrier_name,
32
+ tracking_number=data["order_id"],
33
+ shipment_identifier=data["order_id"],
34
+ label_type=data["label_type"],
35
+ docs=models.Documents(label=data["label"]),
36
+ meta=dict(service_name=data.get("service")),
37
+ )
38
+
39
+
40
+ def shipment_request(
41
+ payload: models.ShipmentRequest,
42
+ settings: provider_utils.Settings,
43
+ ) -> lib.Serializable:
44
+ if (
45
+ payload.shipper.country_code is not None
46
+ and payload.shipper.country_code != units.Country.CA.name
47
+ ):
48
+ raise errors.OriginNotServicedError(payload.shipper.country_code)
49
+
50
+ if (
51
+ payload.recipient.country_code is not None
52
+ and payload.recipient.country_code != units.Country.CA.name
53
+ ):
54
+ raise errors.DestinationNotServicedError(payload.recipient.country_code)
55
+
56
+ shipper = lib.to_address(payload.shipper)
57
+ recipient = lib.to_address(payload.recipient)
58
+ packages = lib.to_packages(payload.parcels)
59
+ service = provider_units.ShippingService.map(payload.service).value_or_key
60
+ options = lib.to_shipping_options(
61
+ payload.options,
62
+ package_options=packages.options,
63
+ initializer=provider_units.shipping_options_initializer,
64
+ )
65
+
66
+ request = dict(
67
+ order=boxknight.OrderRequest(
68
+ recipient=(
69
+ boxknight.Recipient(
70
+ name=recipient.contact,
71
+ phone=recipient.phone_number,
72
+ notes=None,
73
+ email=recipient.email,
74
+ )
75
+ if any([recipient.contact, recipient.phone_number, recipient.email])
76
+ else None
77
+ ),
78
+ recipientAddress=boxknight.Address(
79
+ street=recipient.address_line1,
80
+ unit=recipient.address_line2,
81
+ city=recipient.city,
82
+ province=units.CountryState.CA.value[recipient.state_code].value,
83
+ country=units.Country.CA.value,
84
+ postalCode=recipient.postal_code,
85
+ isBusinessAddress=recipient.residential is False,
86
+ ),
87
+ originAddress=boxknight.Address(
88
+ street=shipper.address_line1,
89
+ unit=shipper.address_line2,
90
+ city=shipper.city,
91
+ province=units.CountryState.CA.value[shipper.state_code].value,
92
+ country=units.Country.CA.value,
93
+ postalCode=shipper.postal_code,
94
+ isBusinessAddress=shipper.residential is False,
95
+ ),
96
+ packageCount=len(packages),
97
+ service=service,
98
+ notes=options.boxknight_notes.state,
99
+ refNumber=payload.reference,
100
+ merchantDisplayName=(
101
+ shipper.company_name or options.boxknight_merchant_display_name.state
102
+ ),
103
+ signatureRequired=options.boxknight_signature_required.state,
104
+ packages=[
105
+ boxknight.Package(
106
+ refNumber=package.parcel.reference_number or str(idx),
107
+ weightOptions=boxknight.WeightOptions(
108
+ weight=package.weight.value,
109
+ unit=package.weight_unit.value.lower(),
110
+ ),
111
+ sizeOptions=boxknight.SizeOptions(
112
+ length=package.length.value,
113
+ width=package.width.value,
114
+ height=package.height.value,
115
+ unit=provider_units.DimensionUnit.map(
116
+ package.dimension_unit.value
117
+ ).value,
118
+ ),
119
+ )
120
+ for idx, package in enumerate(packages, start=1)
121
+ ],
122
+ ),
123
+ label_type=units.LabelType.map(
124
+ payload.label_type or "PDF"
125
+ ).value_or_key.lower(),
126
+ )
127
+
128
+ return lib.Serializable(
129
+ request,
130
+ lambda _: {
131
+ "order": lib.to_dict(_["order"]),
132
+ "label_type": _["label_type"],
133
+ },
134
+ )
@@ -0,0 +1,83 @@
1
+ import datetime
2
+ import karrio.schemas.boxknight.tracking_response as boxknight
3
+ import typing
4
+ import karrio.lib as lib
5
+ import karrio.core.units as units
6
+ import karrio.core.models as models
7
+ import karrio.providers.boxknight.error as error
8
+ import karrio.providers.boxknight.utils as provider_utils
9
+ import karrio.providers.boxknight.units as provider_units
10
+
11
+
12
+ def parse_tracking_response(
13
+ _responses: lib.Deserializable[typing.List[typing.Tuple[str, dict]]],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
16
+ responses = _responses.deserialize()
17
+ messages: typing.List[models.Message] = sum(
18
+ [
19
+ error.parse_error_response(res, settings, tracking_number=number)
20
+ for number, res in responses
21
+ if res.get("error") is not None
22
+ ],
23
+ start=[],
24
+ )
25
+ tracking_details = [
26
+ _extract_details(res, settings)
27
+ for _, res in responses
28
+ if res.get("error") is None
29
+ ]
30
+
31
+ return tracking_details, messages
32
+
33
+
34
+ def _extract_details(
35
+ data: dict,
36
+ settings: provider_utils.Settings,
37
+ ) -> models.TrackingDetails:
38
+ order = lib.to_object(boxknight.TrackingResponse, data)
39
+ delivered = order.orderStatus == "DELIVERY_COMPLETED"
40
+
41
+ return models.TrackingDetails(
42
+ carrier_id=settings.carrier_id,
43
+ carrier_name=settings.carrier_name,
44
+ tracking_number=order.id,
45
+ events=[
46
+ models.TrackingEvent(
47
+ description=order.orderStatus,
48
+ code=order.orderStatus,
49
+ date=lib.fdate(datetime.datetime.now(), "%Y-%m-%dT%H:%M:%S.%fZ"),
50
+ time=lib.flocaltime(datetime.datetime.now(), "%Y-%m-%dT%H:%M:%S.%fZ"),
51
+ )
52
+ ],
53
+ delivered=delivered,
54
+ meta=dict(reference=order.refNumber),
55
+ info=models.TrackingInfo(
56
+ carrier_tracking_link=settings.tracking_url.format(order.id),
57
+ customer_name=getattr(order.recipient, "name", None),
58
+ shipment_package_count=order.packageCount,
59
+ shipment_service=order.service,
60
+ shipment_origin_postal_code=getattr(
61
+ order.originAddress, "postalCode", None
62
+ ),
63
+ shipment_origin_country=units.Country.map(
64
+ getattr(order.originAddress, "country", None)
65
+ ).name,
66
+ shipment_destination_postal_code=getattr(
67
+ order.recipientAddress, "postalCode", None
68
+ ),
69
+ shipment_destination_country=units.Country.map(
70
+ getattr(order.recipientAddress, "country", None)
71
+ ).name,
72
+ shipping_date=lib.fdate(order.createdAt, "%Y-%m-%dT%H:%M:%S.%fZ"),
73
+ ),
74
+ )
75
+
76
+
77
+ def tracking_request(
78
+ payload: models.TrackingRequest,
79
+ settings: provider_utils.Settings,
80
+ ) -> lib.Serializable:
81
+ request = [dict(order_id=number) for number in payload.tracking_numbers]
82
+
83
+ return lib.Serializable(request)
@@ -0,0 +1,65 @@
1
+ import karrio.lib as lib
2
+ import karrio.core.units as units
3
+
4
+
5
+ class DimensionUnit(lib.StrEnum):
6
+ CM = "cm"
7
+ IN = "inch"
8
+
9
+
10
+ class PackagingType(lib.StrEnum):
11
+ """Carrier specific packaging type"""
12
+
13
+ PACKAGE = "PACKAGE"
14
+
15
+ """ Unified Packaging type mapping """
16
+ envelope = PACKAGE
17
+ pak = PACKAGE
18
+ tube = PACKAGE
19
+ pallet = PACKAGE
20
+ small_box = PACKAGE
21
+ medium_box = PACKAGE
22
+ your_packaging = PACKAGE
23
+
24
+
25
+ class ShippingService(lib.StrEnum):
26
+ """Carrier specific services"""
27
+
28
+ boxknight_sameday = "SAMEDAY"
29
+ boxknight_nextday = "NEXTDAY"
30
+ boxknight_scheduled = "SCHEDULED"
31
+
32
+
33
+ class ShippingOption(lib.Enum):
34
+ """Carrier specific options"""
35
+
36
+ boxknight_signature_required = lib.OptionEnum("signatureRequired", bool)
37
+ boxknight_merchant_display_name = lib.OptionEnum("merchantDisplayName")
38
+ boxknight_notes = lib.OptionEnum("notes")
39
+
40
+ signature_required = boxknight_signature_required
41
+
42
+
43
+ def shipping_options_initializer(
44
+ options: dict,
45
+ package_options: units.ShippingOptions = None,
46
+ ) -> units.ShippingOptions:
47
+ """
48
+ Apply default values to the given options.
49
+ """
50
+
51
+ if package_options is not None:
52
+ options.update(package_options.content)
53
+
54
+ def items_filter(key: str) -> bool:
55
+ return key in ShippingOption # type: ignore
56
+
57
+ return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
58
+
59
+
60
+ class TrackingStatus(lib.Enum):
61
+ on_hold = ["CREATED", "GEOCODED"]
62
+ delivered = ["DELIVERY_COMPLETED"]
63
+ in_transit = ["DELIVERY_ASSIGNED", "PICKUP_EN_ROUTE"]
64
+ delivery_delayed = ["delivery_delayed"]
65
+ out_for_delivery = ["PICKUP_COMPLETED", "DELIVERY_EN_ROUTE"]
@@ -0,0 +1,60 @@
1
+ import jstruct
2
+ import karrio.lib as lib
3
+ import karrio.core as core
4
+ import karrio.core.errors as errors
5
+
6
+
7
+ class Settings(core.Settings):
8
+ """BoxKnight connection settings."""
9
+
10
+ username: str
11
+ password: str
12
+
13
+ @property
14
+ def carrier_name(self):
15
+ return "boxknight"
16
+
17
+ @property
18
+ def server_url(self):
19
+ return "https://api.boxknight.com/v1"
20
+
21
+ @property
22
+ def tracking_url(self):
23
+ return "https://www.tracking.boxknight.com/tracking?trackingNo={}"
24
+
25
+ @property
26
+ def auth_token(self):
27
+ """Retrieve the auth token using the username|passwword pair
28
+ or collect it from the cache if an unexpired token exist.
29
+ """
30
+ cache_key = f"{self.carrier_name}|{self.username}|{self.password}"
31
+ auth = self.connection_cache.get(cache_key) or {}
32
+ token = auth.get("token")
33
+
34
+ if token is not None:
35
+ return token
36
+
37
+ self.connection_cache.set(cache_key, lambda: authenticate(self))
38
+ new_auth = self.connection_cache.get(cache_key)
39
+
40
+ if any(self.depot or "") is False:
41
+ self.depot = new_auth["depot"]
42
+
43
+ return new_auth["token"]
44
+
45
+
46
+ def authenticate(settings: Settings):
47
+ import karrio.providers.boxknight.error as error
48
+
49
+ result = lib.request(
50
+ url=f"{settings.server_url}/soap/services/LoginService/V2_1",
51
+ data=dict(username=settings.username, password=settings.password),
52
+ method="POST",
53
+ )
54
+ response = lib.to_dict(result)
55
+ messages = error.parse_error_response(response, settings)
56
+
57
+ if any(messages):
58
+ raise errors.ParsedMessagesError(messages=messages)
59
+
60
+ return dict(token=response["token"])
File without changes
@@ -0,0 +1,8 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class Error:
8
+ error: typing.Optional[str] = None
@@ -0,0 +1,57 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class Address:
8
+ street: typing.Optional[str] = None
9
+ unit: typing.Optional[str] = None
10
+ city: typing.Optional[str] = None
11
+ province: typing.Optional[str] = None
12
+ country: typing.Optional[str] = None
13
+ postalCode: typing.Optional[str] = None
14
+ isBusinessAddress: typing.Optional[bool] = None
15
+
16
+
17
+ @attr.s(auto_attribs=True)
18
+ class SizeOptions:
19
+ length: typing.Optional[float] = None
20
+ width: typing.Optional[float] = None
21
+ height: typing.Optional[float] = None
22
+ unit: typing.Optional[str] = None
23
+
24
+
25
+ @attr.s(auto_attribs=True)
26
+ class WeightOptions:
27
+ weight: typing.Optional[float] = None
28
+ unit: typing.Optional[str] = None
29
+
30
+
31
+ @attr.s(auto_attribs=True)
32
+ class Package:
33
+ refNumber: typing.Optional[int] = None
34
+ weightOptions: typing.Optional[WeightOptions] = jstruct.JStruct[WeightOptions]
35
+ sizeOptions: typing.Optional[SizeOptions] = jstruct.JStruct[SizeOptions]
36
+
37
+
38
+ @attr.s(auto_attribs=True)
39
+ class Recipient:
40
+ name: typing.Optional[str] = None
41
+ phone: typing.Optional[str] = None
42
+ notes: typing.Optional[str] = None
43
+ email: typing.Optional[str] = None
44
+
45
+
46
+ @attr.s(auto_attribs=True)
47
+ class OrderRequest:
48
+ recipient: typing.Optional[Recipient] = jstruct.JStruct[Recipient]
49
+ recipientAddress: typing.Optional[Address] = jstruct.JStruct[Address]
50
+ originAddress: typing.Optional[Address] = jstruct.JStruct[Address]
51
+ packageCount: typing.Optional[int] = None
52
+ service: typing.Optional[str] = None
53
+ notes: typing.Optional[str] = None
54
+ refNumber: typing.Optional[str] = None
55
+ merchantDisplayName: typing.Optional[str] = None
56
+ signatureRequired: typing.Optional[bool] = None
57
+ packages: typing.Optional[typing.List[Package]] = jstruct.JList[Package]
@@ -0,0 +1,31 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class SizeOptions:
8
+ length: typing.Optional[float] = None
9
+ width: typing.Optional[float] = None
10
+ height: typing.Optional[float] = None
11
+ unit: typing.Optional[str] = None
12
+
13
+
14
+ @attr.s(auto_attribs=True)
15
+ class WeightOptions:
16
+ weight: typing.Optional[float] = None
17
+ unit: typing.Optional[str] = None
18
+
19
+
20
+ @attr.s(auto_attribs=True)
21
+ class Package:
22
+ refNumber: typing.Optional[int] = None
23
+ weightOptions: typing.Optional[WeightOptions] = jstruct.JStruct[WeightOptions]
24
+ sizeOptions: typing.Optional[SizeOptions] = jstruct.JStruct[SizeOptions]
25
+
26
+
27
+ @attr.s(auto_attribs=True)
28
+ class RateRequest:
29
+ postalCode: typing.Optional[str] = None
30
+ originPostalCode: typing.Optional[str] = None
31
+ packages: typing.Optional[typing.List[Package]] = jstruct.JList[Package]
@@ -0,0 +1,19 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class Rate:
8
+ price: typing.Optional[int] = None
9
+ service: typing.Optional[str] = None
10
+ name: typing.Optional[str] = None
11
+ description: typing.Optional[str] = None
12
+ estimateDay: typing.Optional[str] = None
13
+ estimateFrom: typing.Optional[str] = None
14
+ estimateTo: typing.Optional[str] = None
15
+
16
+
17
+ @attr.s(auto_attribs=True)
18
+ class RateResponse:
19
+ rates: typing.Optional[typing.List[Rate]] = jstruct.JList[Rate]
@@ -0,0 +1,53 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class Location:
8
+ lat: typing.Optional[float] = None
9
+ lng: typing.Optional[float] = None
10
+
11
+
12
+ @attr.s(auto_attribs=True)
13
+ class Address:
14
+ number: typing.Optional[int] = None
15
+ street: typing.Optional[str] = None
16
+ city: typing.Optional[str] = None
17
+ province: typing.Optional[str] = None
18
+ country: typing.Optional[str] = None
19
+ postalCode: typing.Optional[str] = None
20
+ sublocality: typing.Optional[str] = None
21
+ location: typing.Optional[Location] = jstruct.JStruct[Location]
22
+
23
+
24
+ @attr.s(auto_attribs=True)
25
+ class Recipient:
26
+ name: typing.Optional[str] = None
27
+ phone: typing.Optional[str] = None
28
+ notes: typing.Optional[str] = None
29
+ email: typing.Optional[str] = None
30
+
31
+
32
+ @attr.s(auto_attribs=True)
33
+ class TrackingResponse:
34
+ id: typing.Optional[str] = None
35
+ createdAt: typing.Optional[str] = None
36
+ createdBy: typing.Optional[str] = None
37
+ merchantId: typing.Optional[str] = None
38
+ orderStatus: typing.Optional[str] = None
39
+ scanningRequired: typing.Optional[bool] = None
40
+ validAddress: typing.Optional[bool] = None
41
+ labelUrl: typing.Optional[str] = None
42
+ pdfLabelUrl: typing.Optional[str] = None
43
+ recipient: typing.Optional[Recipient] = jstruct.JStruct[Recipient]
44
+ recipientAddress: typing.Optional[Address] = jstruct.JStruct[Address]
45
+ originAddress: typing.Optional[Address] = jstruct.JStruct[Address]
46
+ packageCount: typing.Optional[int] = None
47
+ signatureRequired: typing.Optional[bool] = None
48
+ service: typing.Optional[str] = None
49
+ notes: typing.Optional[str] = None
50
+ refNumber: typing.Optional[str] = None
51
+ completeAfter: typing.Optional[int] = None
52
+ completeBefore: typing.Optional[int] = None
53
+ merchantDisplayName: typing.Optional[str] = None
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: karrio_boxknight
3
+ Version: 2025.5rc1
4
+ Summary: Karrio - BoxKnight Shipping Extension
5
+ Author-email: karrio <hello@karrio.io>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/karrioapi/karrio
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: karrio
14
+
15
+
16
+ # karrio.boxknight
17
+
18
+ This package is a BoxKnight extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK.
19
+
20
+ ## Requirements
21
+
22
+ `Python 3.7+`
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install karrio.boxknight
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ import karrio.sdk as karrio
34
+ from karrio.mappers.boxknight.settings import Settings
35
+
36
+
37
+ # Initialize a carrier gateway
38
+ boxknight = karrio.gateway["boxknight"].create(
39
+ Settings(
40
+ ...
41
+ )
42
+ )
43
+ ```
44
+
45
+ Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests
@@ -0,0 +1,25 @@
1
+ karrio/mappers/boxknight/__init__.py,sha256=SALvjoaukPIeMxCGQIM9aUjjsIz7BY0UUIx5-k9eJxY,155
2
+ karrio/mappers/boxknight/mapper.py,sha256=HFHxpy2VDrzHtV6ZYzqWBKw47eMkXbxv4ia2QJhdGr0,1947
3
+ karrio/mappers/boxknight/proxy.py,sha256=4OHf2vM2hx8u4ri_ZDS0Oc7sIVPv6PhenxxOpVl9UMU,2870
4
+ karrio/mappers/boxknight/settings.py,sha256=Ilqa1vgwpah5o-1M6pt_ftb5WpMG7pO-VLuWMCEGyHY,532
5
+ karrio/plugins/boxknight/__init__.py,sha256=pJmgB7-NH5Gl-bAgBogK0OogPS0BA7nuZeWXWq9dQRs,756
6
+ karrio/providers/boxknight/__init__.py,sha256=0WwIjVf2pRM4uE9NrqZ_PSdzmTTeeGjZfdWdfsh0tSY,404
7
+ karrio/providers/boxknight/error.py,sha256=W6R_Q1fwRw-kW1p9XuLrRitmT4suGB3eW6UNszAViss,819
8
+ karrio/providers/boxknight/rate.py,sha256=Vh3OqiY-IHpf--bffMQuyrtC4VIabl3FENz-rnvrS_Q,3130
9
+ karrio/providers/boxknight/tracking.py,sha256=4KzbOB-VOx6ZItvCface3YL-k2IGJtp1EFtR43NO5No,2982
10
+ karrio/providers/boxknight/units.py,sha256=JIgfBxvYcX-QpSC9G-h762tChA6HGDDV7E2VEUV_RRg,1676
11
+ karrio/providers/boxknight/utils.py,sha256=UCWT4utryNAAajgQXJ2MMv7kNpdwketRJNwr3k83d-w,1667
12
+ karrio/providers/boxknight/shipment/__init__.py,sha256=AKoH2eQcuZuJhyJNh7oGcM5_9qh-pUPrnAa4RBgZDbU,235
13
+ karrio/providers/boxknight/shipment/cancel.py,sha256=Od0CUtJyvcdufb-WAhbJ6emWPFS61mCXZX5eMV5CA68,1118
14
+ karrio/providers/boxknight/shipment/create.py,sha256=QwJIWb7rQLzZwSHAvE9LdmZPw5FSriVzuPve6GbNw_Y,5001
15
+ karrio/schemas/boxknight/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ karrio/schemas/boxknight/error.py,sha256=P2l807AVgrtoxDoLWJcshp0gmO3giul7j_CecNpVk3Q,122
17
+ karrio/schemas/boxknight/order_request.py,sha256=YKoMUvxPTtQVO5vRb1Oa5afu-wLD3ktkMA2AgsMts9g,1785
18
+ karrio/schemas/boxknight/rate_request.py,sha256=rdmfzMK-2zpuQzu8gahHUHo0gfM2R60uL6C17ijVtQo,848
19
+ karrio/schemas/boxknight/rate_response.py,sha256=mYg7ThzQ7qzD9bp2_PVmVT996lbUsC2UAeVT8hH7hBU,497
20
+ karrio/schemas/boxknight/tracking_response.py,sha256=f3r-edDsF1_AFZjJ11PSv8eM0b-YVdTIZWar-Ta045s,1801
21
+ karrio_boxknight-2025.5rc1.dist-info/METADATA,sha256=o5nmgmAFQlzm24jKazpxANnuWhaXphLRm-hOaM0vz7E,1008
22
+ karrio_boxknight-2025.5rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
+ karrio_boxknight-2025.5rc1.dist-info/entry_points.txt,sha256=YnqTcqXFHHm7AAutRMn78415tSbWLShqgQKNgsl6Ows,63
24
+ karrio_boxknight-2025.5rc1.dist-info/top_level.txt,sha256=FZCY8Nwft8oEGHdl--xku8P3TrnOxu5dETEU_fWpRSM,20
25
+ karrio_boxknight-2025.5rc1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [karrio.plugins]
2
+ boxknight = karrio.plugins.boxknight:METADATA
@@ -0,0 +1,3 @@
1
+ dist
2
+ karrio
3
+ schemas