karrio-seko 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,224 @@
1
+ """Karrio SEKO Logistics shipment API implementation."""
2
+
3
+ import karrio.schemas.seko.shipping_request as seko
4
+ import karrio.schemas.seko.shipping_response as shipping
5
+
6
+ import typing
7
+ import karrio.lib as lib
8
+ import karrio.core.units as units
9
+ import karrio.core.models as models
10
+ import karrio.providers.seko.error as error
11
+ import karrio.providers.seko.utils as provider_utils
12
+ import karrio.providers.seko.units as provider_units
13
+
14
+
15
+ def parse_shipment_response(
16
+ _response: lib.Deserializable[dict],
17
+ settings: provider_utils.Settings,
18
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
19
+ response = _response.deserialize()
20
+ print(_response.ctx)
21
+ messages = error.parse_error_response(response, settings)
22
+ shipment = lib.identity(
23
+ _extract_details(response, settings, ctx=_response.ctx)
24
+ if any(response.get("Consignments", []))
25
+ else None
26
+ )
27
+
28
+ return shipment, messages
29
+
30
+
31
+ def _extract_details(
32
+ data: dict,
33
+ settings: provider_utils.Settings,
34
+ ctx: dict = None,
35
+ ) -> models.ShipmentDetails:
36
+ details = lib.to_object(shipping.ShippingResponseType, data)
37
+ Connotes = [_.Connote for _ in details.Consignments]
38
+ TrackingUrls = [_.TrackingUrl for _ in details.Consignments]
39
+ ConsignmentIds = [_.ConsignmentId for _ in details.Consignments]
40
+ label_type = ctx.get("label_type")
41
+ label_format = ctx.get("label_format")
42
+
43
+ label = lib.bundle_base64(
44
+ sum([_["OutputFiles"][label_type] for _ in data["Consignments"]], []),
45
+ label_format,
46
+ )
47
+
48
+ return models.ShipmentDetails(
49
+ carrier_id=settings.carrier_id,
50
+ carrier_name=settings.carrier_name,
51
+ tracking_number=Connotes[0],
52
+ shipment_identifier=ConsignmentIds[0],
53
+ label_type=label_format,
54
+ docs=models.Documents(label=label),
55
+ meta=dict(
56
+ carrier_tracking_link=TrackingUrls[0],
57
+ seko_site_id=details.SiteId,
58
+ tracking_urls=TrackingUrls,
59
+ consignment_id=ConsignmentIds[0],
60
+ consignment_ids=ConsignmentIds,
61
+ seko_carrier_id=details.CarrierId,
62
+ seko_carrier_name=details.CarrierName,
63
+ seko_carrier_type=details.CarrierType,
64
+ rate_provider=details.CarrierName,
65
+ seko_invoice_response=details.InvoiceResponse,
66
+ ),
67
+ )
68
+
69
+
70
+ def shipment_request(
71
+ payload: models.ShipmentRequest,
72
+ settings: provider_utils.Settings,
73
+ ) -> lib.Serializable:
74
+ shipper = lib.to_address(payload.shipper)
75
+ recipient = lib.to_address(payload.recipient)
76
+ packages = lib.to_packages(payload.parcels)
77
+ service = provider_units.ShippingService.map(payload.service).value_or_key
78
+ options = lib.to_shipping_options(
79
+ payload.options,
80
+ package_options=packages.options,
81
+ initializer=provider_units.shipping_options_initializer,
82
+ )
83
+ customs = lib.to_customs_info(
84
+ payload.customs,
85
+ shipper=payload.shipper,
86
+ recipient=payload.recipient,
87
+ weight_unit=units.WeightUnit.KG.name,
88
+ option_type=provider_units.CustomsOption,
89
+ )
90
+ commodities: units.Products = lib.identity(
91
+ customs.commodities if payload.customs else packages.items
92
+ )
93
+ [label_format, label_type] = lib.identity(
94
+ provider_units.LabelType.map(payload.label_type).value
95
+ or provider_units.LabelType.PDF.value
96
+ )
97
+ commercial_invoice = lib.identity(
98
+ options.seko_invoice_data.state
99
+ or next((
100
+ _["doc_file"] for _ in options.doc_files.state or []
101
+ if _["doc_type"] == "commercial_invoice"
102
+ and _.get("doc_format", "PDF").lower() == "pdf"
103
+ ), None)
104
+ )
105
+
106
+ # map data to convert karrio model to seko specific type
107
+ request = seko.ShippingRequestType(
108
+ DeliveryReference=payload.reference,
109
+ Reference2=options.seko_reference_2.state,
110
+ Reference3=options.seko_reference_3.state,
111
+ Origin=seko.DestinationType(
112
+ Id=options.seko_origin_id.state,
113
+ Name=lib.identity(shipper.company_name or shipper.contact or "Shipper"),
114
+ Address=seko.AddressType(
115
+ BuildingName=None,
116
+ StreetAddress=shipper.street,
117
+ Suburb=shipper.city,
118
+ City=shipper.state_code or shipper.city,
119
+ PostCode=shipper.postal_code,
120
+ CountryCode=shipper.country_code,
121
+ ),
122
+ ContactPerson=shipper.contact,
123
+ PhoneNumber=shipper.phone_number or "000 000 0000",
124
+ Email=shipper.email or " ",
125
+ DeliveryInstructions=options.origin_instructions.state,
126
+ RecipientTaxId=shipper.tax_id,
127
+ SendTrackingEmail=None,
128
+ ),
129
+ Destination=seko.DestinationType(
130
+ Id=options.seko_destination_id.state,
131
+ Name=lib.identity(
132
+ recipient.company_name or recipient.contact or "Recipient"
133
+ ),
134
+ Address=seko.AddressType(
135
+ BuildingName=None,
136
+ StreetAddress=recipient.street,
137
+ Suburb=recipient.city,
138
+ City=recipient.state_code or recipient.city,
139
+ PostCode=recipient.postal_code,
140
+ CountryCode=recipient.country_code,
141
+ ),
142
+ ContactPerson=recipient.contact,
143
+ PhoneNumber=recipient.phone_number or "000 000 0000",
144
+ Email=recipient.email or " ",
145
+ DeliveryInstructions=options.destination_instructions.state,
146
+ RecipientTaxId=recipient.tax_id,
147
+ SendTrackingEmail=options.seko_send_tracking_email.state,
148
+ ),
149
+ DangerousGoods=None,
150
+ Commodities=[
151
+ seko.CommodityType(
152
+ Description=lib.identity(
153
+ lib.text(commodity.description or commodity.title, max=35) or "item"
154
+ ),
155
+ HarmonizedCode=commodity.hs_code or "0000.00.00",
156
+ Units=commodity.quantity,
157
+ UnitValue=commodity.value_amount,
158
+ UnitKg=commodity.weight,
159
+ Currency=commodity.value_currency,
160
+ Country=commodity.origin_country,
161
+ IsDG=None,
162
+ itemSKU=commodity.sku,
163
+ DangerousGoodsItem=None,
164
+ )
165
+ for commodity in commodities
166
+ ],
167
+ Packages=[
168
+ seko.PackageType(
169
+ Height=package.height.CM,
170
+ Length=package.length.CM,
171
+ Width=package.width.CM,
172
+ Kg=package.weight.KG,
173
+ Name=lib.text(package.description, max=50),
174
+ Type=lib.identity(
175
+ provider_units.PackagingType.map(package.packaging_type).value
176
+ ),
177
+ OverLabelBarcode=package.reference_number,
178
+ )
179
+ for package in packages
180
+ ],
181
+ issignaturerequired=options.seko_is_signature_required.state,
182
+ DutiesAndTaxesByReceiver=lib.identity(
183
+ customs.duty.paid_by == "recipient" if payload.customs else None
184
+ ),
185
+ ProductCategory=options.seko_product_category.state,
186
+ ShipType=options.seko_ship_type.state,
187
+ PrintToPrinter=lib.identity(
188
+ options.seko_print_to_printer.state
189
+ if options.seko_print_to_printer.state is not None
190
+ else True
191
+ ),
192
+ IncludeLineDetails=True,
193
+ Carrier=options.seko_carrier.state,
194
+ Service=service,
195
+ CostCentreName=settings.connection_config.cost_center.state,
196
+ CostCentreId=settings.connection_config.cost_center_id.state,
197
+ CodValue=options.cash_on_delivery.state,
198
+ TaxCollected=lib.identity(
199
+ options.seko_tax_collected.state
200
+ if options.seko_tax_collected.state is not None
201
+ else True
202
+ ),
203
+ AmountCollected=lib.to_money(options.seko_amount_collected.state),
204
+ CIFValue=options.seko_cif_value.state,
205
+ FreightValue=options.seko_freight_value.state,
206
+ SendLabel="Y" if options.seko_send_label.state else None,
207
+ LabelBranding=settings.connection_config.label_branding.state,
208
+ InvoiceData=commercial_invoice,
209
+ TaxIds=[
210
+ seko.TaxIDType(
211
+ IdType=option.code,
212
+ IdNumber=option.state,
213
+ )
214
+ for key, option in customs.options.items()
215
+ if key in provider_units.CustomsOption and option.state is not None
216
+ ],
217
+ Outputs=[label_type],
218
+ )
219
+
220
+ return lib.Serializable(
221
+ request,
222
+ lib.to_dict,
223
+ dict(label_type=label_type, label_format=label_format),
224
+ )
@@ -0,0 +1,95 @@
1
+ """Karrio SEKO Logistics tracking API implementation."""
2
+
3
+ import karrio.schemas.seko.tracking_response as tracking
4
+
5
+ import typing
6
+ import karrio.lib as lib
7
+ import karrio.core.units as units
8
+ import karrio.core.models as models
9
+ import karrio.providers.seko.error as error
10
+ import karrio.providers.seko.utils as provider_utils
11
+ import karrio.providers.seko.units as provider_units
12
+
13
+
14
+ def parse_tracking_response(
15
+ _response: lib.Deserializable[typing.List[dict]],
16
+ settings: provider_utils.Settings,
17
+ ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]:
18
+ responses = _response.deserialize()
19
+
20
+ messages: typing.List[models.Message] = error.parse_error_response(
21
+ responses, settings
22
+ )
23
+ tracking_details = [
24
+ _extract_details(_, settings)
25
+ for _ in (responses if isinstance(responses, list) else [responses])
26
+ if any(_.get("Events", []))
27
+ ]
28
+
29
+ return tracking_details, messages
30
+
31
+
32
+ def _extract_details(
33
+ data: dict,
34
+ settings: provider_utils.Settings,
35
+ ) -> models.TrackingDetails:
36
+ details = lib.to_object(tracking.TrackingResponseElementType, data)
37
+ events = list(reversed(details.Events))
38
+ latest_status = lib.identity(
39
+ events[0].OmniCode if any(events) else getattr(details, "Status", None)
40
+ )
41
+ status = next(
42
+ (
43
+ status.name
44
+ for status in list(provider_units.TrackingStatus)
45
+ if latest_status in status.value
46
+ ),
47
+ provider_units.TrackingStatus.in_transit.name,
48
+ )
49
+
50
+ return models.TrackingDetails(
51
+ carrier_id=settings.carrier_id,
52
+ carrier_name=settings.carrier_name,
53
+ tracking_number=details.ConsignmentNo,
54
+ events=[
55
+ models.TrackingEvent(
56
+ date=lib.fdate(
57
+ event.EventDT,
58
+ try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
59
+ ),
60
+ description=event.Description,
61
+ code=event.OmniCode or event.Code,
62
+ time=lib.flocaltime(
63
+ event.EventDT,
64
+ try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
65
+ ),
66
+ location=event.Location,
67
+ )
68
+ for event in events
69
+ ],
70
+ delivered=status == "delivered",
71
+ status=status,
72
+ info=models.TrackingInfo(
73
+ carrier_tracking_link=details.Tracking,
74
+ expected_delivery=lib.fdate(
75
+ details.Delivered,
76
+ try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
77
+ ),
78
+ shipping_date=lib.fdate(
79
+ details.Picked,
80
+ try_formats=["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"],
81
+ ),
82
+ ),
83
+ meta=dict(reference=details.Reference1),
84
+ )
85
+
86
+
87
+ def tracking_request(
88
+ payload: models.TrackingRequest,
89
+ settings: provider_utils.Settings,
90
+ ) -> lib.Serializable:
91
+
92
+ # map data to convert karrio model to seko specific type
93
+ request = payload.tracking_numbers
94
+
95
+ return lib.Serializable(request, lib.to_dict)
@@ -0,0 +1,239 @@
1
+ import karrio.lib as lib
2
+ import karrio.core.units as units
3
+
4
+
5
+ class LabelType(lib.Enum):
6
+ LABEL_PDF = ("PDF", "LABEL_PDF")
7
+ LABEL_PNG_100X150 = ("PNG", "LABEL_PNG_100X150")
8
+ LABEL_PNG_100X175 = ("PNG", "LABEL_PNG_100X175")
9
+ LABEL_PDF_100X175 = ("PDF", "LABEL_PDF_100X175")
10
+ LABEL_PDF_100X150 = ("PDF", "LABEL_PDF_100X150")
11
+ LABEL_ZPL_100X175 = ("ZPL", "LABEL_ZPL_100X175")
12
+ LABEL_ZPL_100X150 = ("ZPL", "LABEL_ZPL_100X150")
13
+
14
+ """ Unified Label type mapping """
15
+ PDF = LABEL_PDF_100X150
16
+ ZPL = LABEL_ZPL_100X150
17
+ PNG = LABEL_PNG_100X150
18
+
19
+
20
+ class PackagingType(lib.StrEnum):
21
+ """Carrier specific packaging type"""
22
+
23
+ Bag = "Bag"
24
+ Box = "Box"
25
+ Carton = "Carton"
26
+ Container = "Container"
27
+ Crate = "Crate"
28
+ Envelope = "Envelope"
29
+ Pail = "Pail"
30
+ Pallet = "Pallet"
31
+ Satchel = "Satchel"
32
+ Tube = "Tube"
33
+ Custom = "Custom"
34
+
35
+ """ Unified Packaging type mapping """
36
+ envelope = Envelope
37
+ pak = Satchel
38
+ tube = Tube
39
+ pallet = Pallet
40
+ small_box = Box
41
+ medium_box = Carton
42
+ your_packaging = Custom
43
+
44
+
45
+ class ShippingService(lib.StrEnum):
46
+ """Carrier specific services"""
47
+
48
+ seko_ecommerce_standard_tracked = "eCommerce Standard Tracked"
49
+ seko_ecommerce_express_tracked = "eCommerce Express Tracked"
50
+ seko_domestic_express = "Domestic Express"
51
+ seko_domestic_standard = "Domestic Standard"
52
+ seko_domestic_large_parcel = "Domestic Large Parcel"
53
+
54
+
55
+ class ShippingOption(lib.Enum):
56
+ """Carrier specific options"""
57
+
58
+ seko_carrier = lib.OptionEnum("Carrier")
59
+ seko_ship_type = lib.OptionEnum("ShipType")
60
+ seko_package_id = lib.OptionEnum("PackageId")
61
+ seko_destination_id = lib.OptionEnum("DestinationId")
62
+ seko_product_category = lib.OptionEnum("ProductCategory")
63
+ origin_instructions = lib.OptionEnum("OriginInstructions")
64
+ destination_instructions = lib.OptionEnum("DestinationInstructions")
65
+ seko_is_saturday_delivery = lib.OptionEnum("IsSaturdayDelivery", bool)
66
+ seko_is_signature_required = lib.OptionEnum("IsSignatureRequired", bool)
67
+ seko_send_tracking_email = lib.OptionEnum("SendTrackingEmail", bool)
68
+ seko_amount_collected = lib.OptionEnum("AmountCollected", float)
69
+ seko_tax_collected = lib.OptionEnum("TaxCollected", bool)
70
+ seko_cod_amount = lib.OptionEnum("CODAmount", float)
71
+ seko_reference_2 = lib.OptionEnum("Reference2")
72
+ seko_reference_3 = lib.OptionEnum("Reference3")
73
+ seko_invoice_data = lib.OptionEnum("InvoiceData")
74
+ seko_origin_id = lib.OptionEnum("OriginId", int)
75
+ seko_print_to_printer = lib.OptionEnum("PrintToPrinter", bool)
76
+ seko_cif_value = lib.OptionEnum("CIFValue", float)
77
+ seko_freight_value = lib.OptionEnum("FreightValue", float)
78
+ seko_send_label = lib.OptionEnum("SendLabel", bool)
79
+
80
+ """ Unified Option type mapping """
81
+ saturday_delivery = seko_is_saturday_delivery
82
+ signature_required = seko_is_signature_required
83
+ email_notification = seko_send_tracking_email
84
+ doc_files = lib.OptionEnum("doc_files", lib.to_dict)
85
+ doc_references = lib.OptionEnum("doc_references", lib.to_dict)
86
+
87
+
88
+ def shipping_options_initializer(
89
+ options: dict,
90
+ package_options: units.ShippingOptions = None,
91
+ ) -> units.ShippingOptions:
92
+ """
93
+ Apply default values to the given options.
94
+ """
95
+
96
+ if package_options is not None:
97
+ options.update(package_options.content)
98
+
99
+ def items_filter(key: str) -> bool:
100
+ return key in ShippingOption # type: ignore
101
+
102
+ return units.ShippingOptions(options, ShippingOption, items_filter=items_filter)
103
+
104
+
105
+ class CustomsOption(lib.Enum):
106
+ XIEORINumber = lib.OptionEnum("XIEORINumber")
107
+ IOSSNUMBER = lib.OptionEnum("IOSSNUMBER")
108
+ GBEORINUMBER = lib.OptionEnum("GBEORINUMBER")
109
+ VOECNUMBER = lib.OptionEnum("VOECNUMBER")
110
+ VATNUMBER = lib.OptionEnum("VATNUMBER")
111
+ VENDORID = lib.OptionEnum("VENDORID")
112
+ NZIRDNUMBER = lib.OptionEnum("NZIRDNUMBER")
113
+ SWISS_VAT = lib.OptionEnum("SWISS VAT")
114
+ OVRNUMBER = lib.OptionEnum("OVRNUMBER")
115
+ EUEORINumber = lib.OptionEnum("EUEORINumber")
116
+ EUVATNumber = lib.OptionEnum("EUVATNumber")
117
+ LVGRegistrationNumber = lib.OptionEnum("LVGRegistrationNumber")
118
+
119
+ """ Unified Customs Identifier type mapping """
120
+
121
+ ioss = IOSSNUMBER
122
+ nip_number = VATNUMBER
123
+ eori_number = EUEORINumber
124
+
125
+
126
+ class TrackingStatus(lib.Enum):
127
+ pending = [
128
+ "OP-1", # Pending
129
+ "OP-8", # Manifest Received by Carrier
130
+ "OP-9", # Not yet received by carrier
131
+ "OP-11", # Received by carrier – no manifest sent
132
+ "OP-12", # Collection request received by carrier
133
+ ]
134
+ on_hold = [
135
+ "OP-26", # Held by carrier
136
+ "OP-2", # Held at Export Hub
137
+ "OP-6", # Customs held for inspection and clearance
138
+ "OP-49", # Held by Delivery Courier
139
+ "OP-70", # Parcel Blocked
140
+ "OP-87", # Aged Parcel - High Value Unpaid
141
+ "OP-88", # Held at Export Hub - Additional Payment Required
142
+ "OP-41", # Incorrect details declared by sender
143
+ "OP-36", # Delivery arranged with receiver
144
+ "OP-39", # Package repacked
145
+ "OP-44", # Selected for redelivery
146
+ "OP-46", # Customer Enquiry lodged
147
+ "OP-52", # Parcel Redirection Requested
148
+ "OP-53", # Parcel Redirected
149
+ "OP-91", # Parcel Blocked - Declared LIT
150
+ ]
151
+ delivered = [
152
+ "OP-71", # Delivered in part
153
+ "OP-72", # Delivered
154
+ "OP-73", # Delivered to neighbour
155
+ "OP-74", # Delivered - Authority to Leave / Safe Drop
156
+ "OP-75", # Delivered - Parcel Collected
157
+ "OP-76", # Delivered to locker/collection point
158
+ "OP-77", # Delivered to alternate delivery point
159
+ ]
160
+ in_transit = [
161
+ "OP-18", # In transit
162
+ "OP-20", # Sub-contractor update
163
+ "OP-22", # Received by Sub-contractor
164
+ "OP-3", # Processed through Export Hub
165
+ "OP-4", # International transit to destination country
166
+ "OP-5", # Customs cleared
167
+ "OP-47", # Processed through Sorting Facility
168
+ "OP-50", # Parcel arrived to courier processing facility
169
+ "OP-51", # Parcel departed courier processing facility
170
+ "OP-78", # Flight Arrived
171
+ "OP-79", # InTransit
172
+ "OP-80", # Reshipped
173
+ "OP-81", # Flight Departed
174
+ "OP-7", # Picked up by Delivery Courier
175
+ "OP-10", # Received by carrier
176
+ "OP-14", # Parcel received and accepted
177
+ "OP-31", # Information
178
+ "OP-32", # Information
179
+ "OP-33", # Collected from sender
180
+ "OP-48", # With Delivery Courier
181
+ "OP-82", # Inbound freight received
182
+ "OP-83", # Delivery exception
183
+ "OP-84", # Recipient not available
184
+ "OP-89", # Collected from sender
185
+ "OP-54", # Transferred to Collection Point
186
+ "OP-56", # Transferred to delivery provider
187
+ ]
188
+ delivery_failed = [
189
+ "OP-24", # Attempted Delivery - Receiver carded
190
+ "OP-27", # Attempted Delivery - Customer not known at address
191
+ "OP-28", # Attempted Delivery - Refused by customer
192
+ "OP-29", # Return to sender
193
+ "OP-30", # Non delivery
194
+ "OP-37", # Attempted Delivery - No access to receivers address
195
+ "OP-38", # Attempted Delivery - Customer Identification failed
196
+ "OP-45", # Attempted delivery
197
+ "OP-55", # Attempted Delivery - Returned to Sender
198
+ "OP-15", # Parcel lost
199
+ "OP-17", # Parcel Damaged
200
+ "OP-23", # Invalid / Insufficient Address
201
+ "OP-86", # Attempted delivery
202
+ "OP-40", # Package disposed
203
+ "OP-92", # Amazon RTS - DESTROY
204
+ "OP-43", # Not collected from store
205
+ ]
206
+ delivery_delayed = [
207
+ "OP-16", # Parcel Delayed
208
+ "OP-13", # Misdirected
209
+ "OP-35", # Mis sorted by carrier
210
+ ]
211
+ out_for_delivery = [
212
+ "OP-21", # Out for delivery
213
+ ]
214
+ ready_for_pickup = [
215
+ "OP-19", # Awaiting Collection
216
+ "OP-25", # Customer to collect from carrier
217
+ "OP-42", # Awaiting collection
218
+ ]
219
+ cancelled = [
220
+ "OP-34", # Cancelled
221
+ "OP-67", # RTS - Cancelled Order
222
+ ]
223
+ return_to_sender = [
224
+ "OP-57", # RTS Received - Authorised Return
225
+ "OP-58", # RTS Received - Cancelled Order
226
+ "OP-59", # RTS Received - Card Left, Never Collected
227
+ "OP-60", # RTS - Fraudulant
228
+ "OP-61", # RTS Received - Invalid or Insufficient Address
229
+ "OP-62", # RTS Received- No Reason Given
230
+ "OP-63", # RTS - High Value Rejected
231
+ "OP-64", # RTS Received - Refused
232
+ "OP-65", # RTS Received - Unclaimed
233
+ "OP-66", # Return to Sender
234
+ "OP-68", # RTS Received
235
+ "OP-69", # RTS Received - Damaged Parcel
236
+ "OP-85", # RTS - In transit
237
+ "OP-90", # Return processed
238
+ "OP-94", # RTS consolidated
239
+ ]
@@ -0,0 +1,114 @@
1
+ import base64
2
+ import datetime
3
+ import karrio.lib as lib
4
+ import karrio.core as core
5
+ import karrio.core.errors as errors
6
+
7
+
8
+ class Settings(core.Settings):
9
+ """SEKO Logistics connection settings."""
10
+
11
+ # Add carrier specific api connection properties here
12
+ access_key: str
13
+
14
+ @property
15
+ def carrier_name(self):
16
+ return "seko"
17
+
18
+ @property
19
+ def server_url(self):
20
+ return (
21
+ "https://staging.omniparcel.com"
22
+ if self.test_mode
23
+ else "https://api.omniparcel.com"
24
+ )
25
+
26
+ # """uncomment the following code block to expose a carrier tracking url."""
27
+ # @property
28
+ # def tracking_url(self):
29
+ # return "https://www.carrier.com/tracking?tracking-id={}"
30
+
31
+ # """uncomment the following code block to implement the Basic auth."""
32
+ # @property
33
+ # def authorization(self):
34
+ # pair = "%s:%s" % (self.username, self.password)
35
+ # return base64.b64encode(pair.encode("utf-8")).decode("ascii")
36
+
37
+ @property
38
+ def connection_config(self) -> lib.units.Options:
39
+ return lib.to_connection_config(
40
+ self.config or {},
41
+ option_type=ConnectionConfig,
42
+ )
43
+
44
+
45
+ # """uncomment the following code block to implement the oauth login."""
46
+ # @property
47
+ # def access_token(self):
48
+ # """Retrieve the access_token using the client_id|client_secret pair
49
+ # or collect it from the cache if an unexpired access_token exist.
50
+ # """
51
+ # cache_key = f"{self.carrier_name}|{self.client_id}|{self.client_secret}"
52
+ # now = datetime.datetime.now() + datetime.timedelta(minutes=30)
53
+
54
+ # auth = self.connection_cache.get(cache_key) or {}
55
+ # token = auth.get("access_token")
56
+ # expiry = lib.to_date(auth.get("expiry"), current_format="%Y-%m-%d %H:%M:%S")
57
+
58
+ # if token is not None and expiry is not None and expiry > now:
59
+ # return token
60
+
61
+ # self.connection_cache.set(cache_key, lambda: login(self))
62
+ # new_auth = self.connection_cache.get(cache_key)
63
+
64
+ # return new_auth["access_token"]
65
+
66
+ # """uncomment the following code block to implement the oauth login."""
67
+ # def login(settings: Settings):
68
+ # import karrio.providers.seko.error as error
69
+
70
+ # result = lib.request(
71
+ # url=f"{settings.server_url}/oauth/token",
72
+ # method="POST",
73
+ # headers={"content-Type": "application/x-www-form-urlencoded"},
74
+ # data=lib.to_query_string(
75
+ # dict(
76
+ # grant_type="client_credentials",
77
+ # client_id=settings.client_id,
78
+ # client_secret=settings.client_secret,
79
+ # )
80
+ # ),
81
+ # )
82
+
83
+ # response = lib.to_dict(result)
84
+ # messages = error.parse_error_response(response, settings)
85
+
86
+ # if any(messages):
87
+ # raise errors.ParsedMessagesError(messages)
88
+
89
+ # expiry = datetime.datetime.now() + datetime.timedelta(
90
+ # seconds=float(response.get("expires_in", 0))
91
+ # )
92
+ # return {**response, "expiry": lib.fdatetime(expiry)}
93
+
94
+
95
+ class ConnectionConfig(lib.Enum):
96
+ """SEKO Logistics connection configuration."""
97
+
98
+ currency = lib.OptionEnum("currency", str)
99
+ cost_center = lib.OptionEnum("cost_center", str)
100
+ cost_center_id = lib.OptionEnum("cost_center_id", str)
101
+ shipping_options = lib.OptionEnum("shipping_options", list)
102
+ shipping_services = lib.OptionEnum("shipping_services", list)
103
+
104
+
105
+ def parse_error_response(response):
106
+ """Parse the error response from the SAPIENT API."""
107
+ content = lib.failsafe(lambda: lib.decode(response.read()))
108
+
109
+ if any(content or ""):
110
+ return content
111
+
112
+ return lib.to_json(
113
+ dict(Errors=[dict(code=str(response.code), Message=response.reason)])
114
+ )
File without changes
@@ -0,0 +1,11 @@
1
+ import attr
2
+ import jstruct
3
+ import typing
4
+
5
+
6
+ @attr.s(auto_attribs=True)
7
+ class ErrorResponseType:
8
+ Property: typing.Optional[str] = None
9
+ Message: typing.Optional[str] = None
10
+ Key: typing.Optional[str] = None
11
+ Value: typing.Optional[str] = None