karrio-canadapost 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.
Files changed (41) hide show
  1. karrio/mappers/canadapost/__init__.py +3 -0
  2. karrio/mappers/canadapost/mapper.py +88 -0
  3. karrio/mappers/canadapost/proxy.py +373 -0
  4. karrio/mappers/canadapost/settings.py +23 -0
  5. karrio/plugins/canadapost/__init__.py +23 -0
  6. karrio/providers/canadapost/__init__.py +25 -0
  7. karrio/providers/canadapost/error.py +42 -0
  8. karrio/providers/canadapost/manifest.py +127 -0
  9. karrio/providers/canadapost/pickup/__init__.py +3 -0
  10. karrio/providers/canadapost/pickup/cancel.py +33 -0
  11. karrio/providers/canadapost/pickup/create.py +217 -0
  12. karrio/providers/canadapost/pickup/update.py +55 -0
  13. karrio/providers/canadapost/rate.py +192 -0
  14. karrio/providers/canadapost/shipment/__init__.py +8 -0
  15. karrio/providers/canadapost/shipment/cancel.py +53 -0
  16. karrio/providers/canadapost/shipment/create.py +308 -0
  17. karrio/providers/canadapost/tracking.py +75 -0
  18. karrio/providers/canadapost/units.py +285 -0
  19. karrio/providers/canadapost/utils.py +92 -0
  20. karrio/schemas/canadapost/__init__.py +0 -0
  21. karrio/schemas/canadapost/authreturn.py +3389 -0
  22. karrio/schemas/canadapost/common.py +2037 -0
  23. karrio/schemas/canadapost/customerinfo.py +2307 -0
  24. karrio/schemas/canadapost/discovery.py +3016 -0
  25. karrio/schemas/canadapost/manifest.py +3704 -0
  26. karrio/schemas/canadapost/merchantregistration.py +1498 -0
  27. karrio/schemas/canadapost/messages.py +1431 -0
  28. karrio/schemas/canadapost/ncshipment.py +7231 -0
  29. karrio/schemas/canadapost/openreturn.py +2438 -0
  30. karrio/schemas/canadapost/pickup.py +1407 -0
  31. karrio/schemas/canadapost/pickuprequest.py +6794 -0
  32. karrio/schemas/canadapost/postoffice.py +2240 -0
  33. karrio/schemas/canadapost/rating.py +5308 -0
  34. karrio/schemas/canadapost/serviceinfo.py +1505 -0
  35. karrio/schemas/canadapost/shipment.py +9982 -0
  36. karrio/schemas/canadapost/track.py +3100 -0
  37. karrio_canadapost-2025.5rc1.dist-info/METADATA +44 -0
  38. karrio_canadapost-2025.5rc1.dist-info/RECORD +41 -0
  39. karrio_canadapost-2025.5rc1.dist-info/WHEEL +5 -0
  40. karrio_canadapost-2025.5rc1.dist-info/entry_points.txt +2 -0
  41. karrio_canadapost-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,127 @@
1
+ import karrio.schemas.canadapost.manifest as canadapost
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.models as models
5
+ import karrio.providers.canadapost.error as error
6
+ import karrio.providers.canadapost.utils as provider_utils
7
+ import karrio.providers.canadapost.units as provider_units
8
+
9
+
10
+ def parse_manifest_response(
11
+ _response: lib.Deserializable[lib.Element],
12
+ settings: provider_utils.Settings,
13
+ ) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
14
+ response = _response.deserialize()
15
+ links = lib.find_element("link", response)
16
+
17
+ messages = error.parse_error_response(response, settings)
18
+ details = (
19
+ _extract_details(links, settings, _response.ctx) if len(links) > 0 else None
20
+ )
21
+
22
+ return details, messages
23
+
24
+
25
+ def _extract_details(
26
+ links: typing.List[lib.Element],
27
+ settings: provider_utils.Settings,
28
+ ctx: dict = None,
29
+ ) -> models.ManifestDetails:
30
+ manifest = lib.bundle_base64(ctx["files"])
31
+
32
+ return models.ManifestDetails(
33
+ carrier_id=settings.carrier_id,
34
+ carrier_name=settings.carrier_id,
35
+ doc=models.ManifestDocument(manifest=manifest),
36
+ meta=dict(
37
+ group_ids=ctx["group_ids"],
38
+ links=[_.get("href") for _ in links],
39
+ ),
40
+ )
41
+
42
+
43
+ def manifest_request(
44
+ payload: models.ManifestRequest,
45
+ settings: provider_utils.Settings,
46
+ ) -> lib.Serializable:
47
+ address = lib.to_address(payload.address)
48
+ options = lib.units.Options(
49
+ payload.options,
50
+ option_type=lib.units.create_enum(
51
+ "ManifestOptions",
52
+ {
53
+ "group_ids": lib.OptionEnum("group_ids", list),
54
+ "shipments": lib.OptionEnum("shipments", lib.to_dict),
55
+ "method_of_payment": lib.OptionEnum("method_of_payment"),
56
+ "shipping_point_id": lib.OptionEnum("shipping_point_id"),
57
+ "excluded_shipments": lib.OptionEnum("excluded_shipments", list),
58
+ "detailed_manifests": lib.OptionEnum("detailed_manifests", bool),
59
+ "cpc_pickup_indicator": lib.OptionEnum("cpc_pickup_indicator", bool),
60
+ "requested_shipping_point": lib.OptionEnum("requested_shipping_point"),
61
+ },
62
+ ),
63
+ )
64
+ group_ids = lib.identity(
65
+ options.group_ids.state
66
+ or [
67
+ *set(
68
+ (
69
+ _.get("meta", {}).get("group_id")
70
+ for _ in (options.shipments.state or [])
71
+ if lib.text(_.get("meta", {}).get("group_id")) is not None
72
+ )
73
+ )
74
+ ]
75
+ )
76
+ retrieve_shipments = len(group_ids) == 0
77
+
78
+ request = canadapost.ShipmentTransmitSetType(
79
+ customer_request_id=None,
80
+ group_ids=canadapost.GroupIDListType(group_id=["[GROUP_IDS]"]),
81
+ cpc_pickup_indicator=options.cpc_pickup_indicator.state,
82
+ requested_shipping_point=provider_utils.format_ca_postal_code(
83
+ options.requested_shipping_point.state or address.postal_code
84
+ ),
85
+ shipping_point_id=options.shipping_point_id.state,
86
+ detailed_manifests=lib.identity(
87
+ True
88
+ if options.detailed_manifests.state is not False
89
+ else options.detailed_manifests.state
90
+ ),
91
+ method_of_payment=(options.method_of_payment.state or "Account"),
92
+ manifest_address=canadapost.ManifestAddressType(
93
+ manifest_name=address.contact,
94
+ manifest_company=address.company_name or address.contact or "N/A",
95
+ phone_number=address.phone_number or "000 000 0000",
96
+ address_details=canadapost.AddressDetailsType(
97
+ address_line_1=address.address_line1,
98
+ address_line_2=address.address_line2,
99
+ city=address.city,
100
+ prov_state=address.state_code,
101
+ country_code=address.country_code,
102
+ postal_zip_code=address.postal_code,
103
+ ),
104
+ ),
105
+ customer_reference=lib.text(payload.reference, max=12),
106
+ excluded_shipments=lib.identity(
107
+ canadapost.ExcludedShipmentsType(
108
+ shipment_id=options.excluded_shipments.state.slit(",")
109
+ )
110
+ if options.excluded_shipments.state
111
+ else None
112
+ ),
113
+ )
114
+
115
+ return lib.Serializable(
116
+ request,
117
+ lambda _: lib.to_xml(
118
+ request,
119
+ name_="transmit-set",
120
+ namespacedef_='xmlns="http://www.canadapost.ca/ws/manifest-v8"',
121
+ ),
122
+ dict(
123
+ group_ids=group_ids,
124
+ retrieve_shipments=retrieve_shipments,
125
+ shipment_identifiers=payload.shipment_identifiers,
126
+ ),
127
+ )
@@ -0,0 +1,3 @@
1
+ from karrio.providers.canadapost.pickup.create import parse_pickup_response, pickup_request
2
+ from karrio.providers.canadapost.pickup.update import parse_pickup_update_response, pickup_update_request
3
+ from karrio.providers.canadapost.pickup.cancel import parse_pickup_cancel_response, pickup_cancel_request
@@ -0,0 +1,33 @@
1
+ from typing import Tuple, List
2
+ from karrio.core.models import (
3
+ PickupCancelRequest,
4
+ Message,
5
+ ConfirmationDetails,
6
+ )
7
+ from karrio.core.utils import Serializable, Element
8
+ from karrio.providers.canadapost.error import parse_error_response
9
+ from karrio.providers.canadapost.utils import Settings
10
+ import karrio.lib as lib
11
+
12
+
13
+ def parse_pickup_cancel_response(
14
+ _response: lib.Deserializable[Element], settings: Settings
15
+ ) -> Tuple[ConfirmationDetails, List[Message]]:
16
+ response = _response.deserialize()
17
+ errors = parse_error_response(response, settings)
18
+ cancellation = (
19
+ ConfirmationDetails(
20
+ carrier_id=settings.carrier_id,
21
+ carrier_name=settings.carrier_name,
22
+ success=True,
23
+ operation="Cancel Pickup",
24
+ )
25
+ if len(errors) == 0
26
+ else None
27
+ )
28
+
29
+ return cancellation, errors
30
+
31
+
32
+ def pickup_cancel_request(payload: PickupCancelRequest, _) -> Serializable:
33
+ return Serializable(payload.confirmation_number)
@@ -0,0 +1,217 @@
1
+ from typing import Tuple, List, Union
2
+ from functools import partial
3
+ from karrio.schemas.canadapost.pickup import pickup_availability
4
+ from karrio.schemas.canadapost.pickuprequest import (
5
+ PickupRequestDetailsType,
6
+ PickupRequestUpdateDetailsType,
7
+ PickupLocationType,
8
+ AlternateAddressType,
9
+ ContactInfoType,
10
+ LocationDetailsType,
11
+ ItemsCharacteristicsType,
12
+ PickupTimesType,
13
+ OnDemandPickupTimeType,
14
+ PickupRequestPriceType,
15
+ PickupRequestHeaderType,
16
+ PickupTypeType as PickupType,
17
+ )
18
+ import karrio.lib as lib
19
+ from karrio.core.utils import (
20
+ Serializable,
21
+ Element,
22
+ Job,
23
+ Pipeline,
24
+ DF,
25
+ NF,
26
+ XP,
27
+ )
28
+ from karrio.core.models import (
29
+ PickupRequest,
30
+ PickupDetails,
31
+ Message,
32
+ ChargeDetails,
33
+ PickupUpdateRequest,
34
+ )
35
+ from karrio.core.units import Packages
36
+ from karrio.providers.canadapost.units import PackagePresets
37
+ from karrio.providers.canadapost.utils import Settings
38
+ from karrio.providers.canadapost.error import parse_error_response
39
+
40
+ PickupRequestDetails = Union[PickupRequestDetailsType, PickupRequestUpdateDetailsType]
41
+
42
+
43
+ def parse_pickup_response(
44
+ _response: lib.Deserializable[Element], settings: Settings
45
+ ) -> Tuple[PickupDetails, List[Message]]:
46
+ response = (
47
+ _response.deserialize() if hasattr(_response, "deserialize") else _response
48
+ )
49
+ pickup = (
50
+ _extract_pickup_details(response, settings)
51
+ if len(lib.find_element("pickup-request-header", response)) > 0
52
+ else None
53
+ )
54
+ return pickup, parse_error_response(response, settings)
55
+
56
+
57
+ def _extract_pickup_details(response: Element, settings: Settings) -> PickupDetails:
58
+ header = lib.find_element(
59
+ "pickup-request-header", response, PickupRequestHeaderType, first=True
60
+ )
61
+ price = lib.find_element(
62
+ "pickup-request-price", response, PickupRequestPriceType, first=True
63
+ )
64
+ price_amount = (
65
+ sum(
66
+ [
67
+ NF.decimal(price.hst_amount or 0.0),
68
+ NF.decimal(price.gst_amount or 0.0),
69
+ NF.decimal(price.due_amount or 0.0),
70
+ ],
71
+ 0.0,
72
+ )
73
+ if price is not None
74
+ else None
75
+ )
76
+
77
+ return PickupDetails(
78
+ carrier_id=settings.carrier_id,
79
+ carrier_name=settings.carrier_name,
80
+ confirmation_number=header.request_id,
81
+ pickup_date=DF.fdate(header.next_pickup_date),
82
+ pickup_charge=ChargeDetails(
83
+ name="Pickup fees", amount=NF.decimal(price_amount), currency="CAD"
84
+ )
85
+ if price is not None
86
+ else None,
87
+ )
88
+
89
+
90
+ def pickup_request(payload: PickupRequest, settings: Settings) -> Serializable:
91
+ request: Pipeline = Pipeline(
92
+ get_availability=lambda *_: _get_pickup_availability(payload),
93
+ create_pickup=partial(_create_pickup, payload=payload, settings=settings),
94
+ )
95
+ return Serializable(request)
96
+
97
+
98
+ def _create_pickup_request(
99
+ payload: PickupRequest, settings: Settings, update: bool = False
100
+ ) -> Serializable:
101
+ """
102
+ pickup_request create a serializable typed PickupRequestDetailsType
103
+
104
+ Options:
105
+ - five_ton_flag
106
+ - loading_dock_flag
107
+
108
+ :param update: bool
109
+ :param payload: PickupRequest
110
+ :param settings: Settings
111
+ :return: Serializable
112
+ """
113
+ RequestType = PickupRequestUpdateDetailsType if update else PickupRequestDetailsType
114
+ packages = Packages(payload.parcels, PackagePresets, required=["weight"])
115
+ heavy = any([p for p in packages if p.weight.KG > 23])
116
+ location_details = dict(
117
+ instruction=payload.instruction,
118
+ five_ton_flag=payload.options.get("five_ton_flag"),
119
+ loading_dock_flag=payload.options.get("loading_dock_flag"),
120
+ )
121
+ address = lib.to_address(payload.address)
122
+
123
+ request = RequestType(
124
+ customer_request_id=settings.customer_number,
125
+ pickup_type=PickupType.ON_DEMAND.value,
126
+ pickup_location=PickupLocationType(
127
+ business_address_flag=(not payload.address.residential),
128
+ alternate_address=AlternateAddressType(
129
+ company=address.company_name or "",
130
+ address_line_1=address.address_line,
131
+ city=address.city,
132
+ province=address.state_code,
133
+ postal_code=address.postal_code,
134
+ )
135
+ if payload.address
136
+ else None,
137
+ ),
138
+ contact_info=ContactInfoType(
139
+ contact_name=payload.address.person_name,
140
+ email=payload.address.email or "",
141
+ contact_phone=payload.address.phone_number,
142
+ telephone_ext=None,
143
+ receive_email_updates_flag=(payload.address.email is not None),
144
+ ),
145
+ location_details=(
146
+ LocationDetailsType(
147
+ five_ton_flag=location_details["five_ton_flag"],
148
+ loading_dock_flag=location_details["loading_dock_flag"],
149
+ pickup_instructions=location_details["instruction"],
150
+ )
151
+ if any(location_details.values())
152
+ else None
153
+ ),
154
+ items_characteristics=(
155
+ ItemsCharacteristicsType(
156
+ pww_flag=None,
157
+ priority_flag=None,
158
+ returns_flag=None,
159
+ heavy_item_flag=heavy,
160
+ )
161
+ if heavy
162
+ else None
163
+ ),
164
+ pickup_volume=f"{len(packages) or 1}",
165
+ pickup_times=PickupTimesType(
166
+ on_demand_pickup_time=OnDemandPickupTimeType(
167
+ date=payload.pickup_date,
168
+ preferred_time=payload.ready_time,
169
+ closing_time=payload.closing_time,
170
+ ),
171
+ scheduled_pickup_times=None,
172
+ ),
173
+ payment_info=None,
174
+ )
175
+ return Serializable(request, partial(_request_serializer, update=update))
176
+
177
+
178
+ def _get_pickup_availability(payload: PickupRequest):
179
+ return Job(
180
+ id="availability", data=(payload.address.postal_code or "").replace(" ", "")
181
+ )
182
+
183
+
184
+ def _create_pickup(
185
+ availability_response: str, payload: PickupRequest, settings: Settings
186
+ ):
187
+ availability = XP.to_object(pickup_availability, XP.to_xml(availability_response))
188
+ data = (
189
+ _create_pickup_request(payload, settings)
190
+ if availability.on_demand_tour
191
+ else None
192
+ )
193
+
194
+ return Job(id="create_pickup", data=data, fallback="" if data is None else "")
195
+
196
+
197
+ def _get_pickup(
198
+ update_response: str, payload: PickupUpdateRequest, settings: Settings
199
+ ) -> Job:
200
+ errors = parse_error_response(XP.to_xml(XP.bundle_xml([update_response])), settings)
201
+ data = (
202
+ None
203
+ if any(errors)
204
+ else f"/enab/{settings.customer_number}/pickuprequest/{payload.confirmation_number}/details"
205
+ )
206
+
207
+ return Job(
208
+ id="get_pickup", data=Serializable(data), fallback="" if data is None else ""
209
+ )
210
+
211
+
212
+ def _request_serializer(request: PickupRequestDetails, update: bool = False) -> str:
213
+ return XP.export(
214
+ request,
215
+ name_=("pickup-request-update" if update else "pickup-request-details"),
216
+ namespacedef_='xmlns="http://www.canadapost.ca/ws/pickuprequest"',
217
+ )
@@ -0,0 +1,55 @@
1
+ import karrio.lib as lib
2
+ from typing import cast, Tuple, List
3
+ from functools import partial
4
+ from karrio.core.utils import Job, Pipeline, Serializable, Element
5
+ from karrio.core.models import (
6
+ PickupRequest,
7
+ PickupUpdateRequest,
8
+ PickupDetails,
9
+ Message,
10
+ )
11
+
12
+ from karrio.providers.canadapost.utils import Settings
13
+ from karrio.providers.canadapost.pickup.create import (
14
+ parse_pickup_response,
15
+ _create_pickup_request,
16
+ _get_pickup,
17
+ )
18
+
19
+
20
+ def parse_pickup_update_response(
21
+ _response: lib.Deserializable[Element], settings: Settings
22
+ ) -> Tuple[PickupDetails, List[Message]]:
23
+ response = _response.deserialize()
24
+ return parse_pickup_response(response, settings)
25
+
26
+
27
+ def pickup_update_request(
28
+ payload: PickupUpdateRequest, settings: Settings
29
+ ) -> Serializable:
30
+ request: Pipeline = Pipeline(
31
+ update_pickup=lambda *_: _update_pickup(payload, settings),
32
+ get_pickup=partial(_get_pickup, payload=payload, settings=settings),
33
+ )
34
+ return Serializable(request)
35
+
36
+
37
+ def _update_pickup(payload: PickupUpdateRequest, settings: Settings) -> Job:
38
+ data = Serializable(
39
+ dict(
40
+ confirmation_number=payload.confirmation_number,
41
+ data=_create_pickup_request(
42
+ cast(PickupRequest, payload), settings, update=True
43
+ ),
44
+ ),
45
+ _update_request_serializer,
46
+ )
47
+ fallback = "" if data is None else ""
48
+
49
+ return Job(id="update_pickup", data=data, fallback=fallback)
50
+
51
+
52
+ def _update_request_serializer(request: dict) -> dict:
53
+ pickuprequest = request["confirmation_number"]
54
+ data = request["data"].serialize()
55
+ return dict(pickuprequest=pickuprequest, data=data)
@@ -0,0 +1,192 @@
1
+ import karrio.schemas.canadapost.rating as canadapost
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.units as units
5
+ import karrio.core.errors as errors
6
+ import karrio.core.models as models
7
+ import karrio.providers.canadapost.error as provider_error
8
+ import karrio.providers.canadapost.units as provider_units
9
+ import karrio.providers.canadapost.utils as provider_utils
10
+
11
+
12
+ def parse_rate_response(
13
+ _response: lib.Deserializable[lib.Element],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
16
+ responses = _response.deserialize()
17
+
18
+ package_rates: typing.List[typing.Tuple[str, typing.List[models.RateDetails]]] = [
19
+ (
20
+ f"{_}",
21
+ [
22
+ _extract_details(node, settings)
23
+ for node in lib.find_element("price-quote", response)
24
+ ],
25
+ )
26
+ for _, response in enumerate(responses, start=1)
27
+ ]
28
+
29
+ messages = provider_error.parse_error_response(responses, settings)
30
+ rates = lib.to_multi_piece_rates(package_rates)
31
+
32
+ return rates, messages
33
+
34
+
35
+ def _extract_details(
36
+ node: lib.Element, settings: provider_utils.Settings
37
+ ) -> models.RateDetails:
38
+ quote = lib.to_object(canadapost.price_quoteType, node)
39
+ service = provider_units.ServiceType.map(quote.service_code)
40
+
41
+ adjustments = getattr(quote.price_details.adjustments, "adjustment", [])
42
+ charges = [
43
+ ("Base charge", quote.price_details.base),
44
+ ("GST", quote.price_details.taxes.gst.valueOf_),
45
+ ("PST", quote.price_details.taxes.pst.valueOf_),
46
+ ("HST", quote.price_details.taxes.hst.valueOf_),
47
+ *((a.adjustment_name, a.adjustment_cost) for a in adjustments),
48
+ ]
49
+
50
+ return models.RateDetails(
51
+ carrier_name=settings.carrier_name,
52
+ carrier_id=settings.carrier_id,
53
+ currency=units.Currency.CAD.name,
54
+ transit_days=quote.service_standard.expected_transit_time,
55
+ service=service.name_or_key,
56
+ total_charge=lib.to_money(quote.price_details.due or 0),
57
+ extra_charges=[
58
+ models.ChargeDetails(
59
+ name=name,
60
+ currency=units.Currency.CAD.name,
61
+ amount=lib.to_money(amount),
62
+ )
63
+ for name, amount in charges
64
+ if amount
65
+ ],
66
+ meta=dict(service_name=(service.name or quote.service_name)),
67
+ )
68
+
69
+
70
+ def rate_request(
71
+ payload: models.RateRequest,
72
+ settings: provider_utils.Settings,
73
+ ) -> lib.Serializable:
74
+ """Create the appropriate Canada Post rate request depending on the destination
75
+
76
+ :param settings: Karrio carrier connection settings
77
+ :param payload: Karrio unified API rate request data
78
+ :return: a domestic or international Canada post compatible request
79
+ :raises: an OriginNotServicedError when origin country is not serviced by the carrier
80
+ """
81
+ if (
82
+ payload.shipper.country_code
83
+ and payload.shipper.country_code != units.Country.CA.name
84
+ ):
85
+ raise errors.OriginNotServicedError(payload.shipper.country_code)
86
+
87
+ services = lib.to_services(payload.services, provider_units.ServiceType)
88
+ options = lib.to_shipping_options(
89
+ payload.options,
90
+ initializer=provider_units.shipping_options_initializer,
91
+ )
92
+ packages = lib.to_packages(
93
+ payload.parcels,
94
+ provider_units.PackagePresets,
95
+ required=["weight"],
96
+ options=options,
97
+ package_option_type=provider_units.ShippingOption,
98
+ shipping_options_initializer=provider_units.shipping_options_initializer,
99
+ )
100
+
101
+ requests = [
102
+ canadapost.mailing_scenario(
103
+ customer_number=settings.customer_number,
104
+ contract_id=settings.contract_id,
105
+ promo_code=None,
106
+ quote_type=None,
107
+ expected_mailing_date=package.options.shipment_date.state,
108
+ options=(
109
+ canadapost.optionsType(
110
+ option=[
111
+ canadapost.optionType(
112
+ option_code=option.code,
113
+ option_amount=lib.to_money(option.state),
114
+ )
115
+ for _, option in package.options.items()
116
+ if option.state is not False
117
+ ]
118
+ )
119
+ if any(
120
+ [
121
+ option
122
+ for _, option in package.options.items()
123
+ if option.state is not False
124
+ ]
125
+ )
126
+ else None
127
+ ),
128
+ parcel_characteristics=canadapost.parcel_characteristicsType(
129
+ weight=package.weight.map(provider_units.MeasurementOptions).KG,
130
+ dimensions=canadapost.dimensionsType(
131
+ length=package.length.map(provider_units.MeasurementOptions).CM,
132
+ width=package.width.map(provider_units.MeasurementOptions).CM,
133
+ height=package.height.map(provider_units.MeasurementOptions).CM,
134
+ ),
135
+ unpackaged=None,
136
+ mailing_tube=None,
137
+ oversized=None,
138
+ ),
139
+ services=(
140
+ canadapost.servicesType(service_code=[svc.value for svc in services])
141
+ if any(services)
142
+ else None
143
+ ),
144
+ origin_postal_code=provider_utils.format_ca_postal_code(
145
+ payload.shipper.postal_code
146
+ ),
147
+ destination=canadapost.destinationType(
148
+ domestic=(
149
+ canadapost.domesticType(
150
+ postal_code=provider_utils.format_ca_postal_code(
151
+ payload.recipient.postal_code
152
+ )
153
+ )
154
+ if (payload.recipient.country_code == units.Country.CA.name)
155
+ else None
156
+ ),
157
+ united_states=(
158
+ canadapost.united_statesType(
159
+ zip_code=provider_utils.format_ca_postal_code(
160
+ payload.recipient.postal_code
161
+ )
162
+ )
163
+ if (payload.recipient.country_code == units.Country.US.name)
164
+ else None
165
+ ),
166
+ international=(
167
+ canadapost.internationalType(
168
+ country_code=provider_utils.format_ca_postal_code(
169
+ payload.recipient.postal_code
170
+ )
171
+ )
172
+ if (
173
+ payload.recipient.country_code
174
+ not in [units.Country.US.name, units.Country.CA.name]
175
+ )
176
+ else None
177
+ ),
178
+ ),
179
+ )
180
+ for package in packages
181
+ ]
182
+
183
+ return lib.Serializable(
184
+ requests,
185
+ lambda __: [
186
+ lib.to_xml(
187
+ request,
188
+ namespacedef_='xmlns="http://www.canadapost.ca/ws/ship/rate-v4"',
189
+ )
190
+ for request in __
191
+ ],
192
+ )
@@ -0,0 +1,8 @@
1
+ from karrio.providers.canadapost.shipment.create import (
2
+ parse_shipment_response,
3
+ shipment_request,
4
+ )
5
+ from karrio.providers.canadapost.shipment.cancel import (
6
+ parse_shipment_cancel_response,
7
+ shipment_cancel_request,
8
+ )
@@ -0,0 +1,53 @@
1
+ import karrio.schemas.canadapost.shipment as canadapost
2
+ import typing
3
+ import karrio.lib as lib
4
+ import karrio.core.units as units
5
+ import karrio.core.errors as errors
6
+ import karrio.core.models as models
7
+ import karrio.providers.canadapost.error as provider_error
8
+ import karrio.providers.canadapost.units as provider_units
9
+ import karrio.providers.canadapost.utils as provider_utils
10
+
11
+
12
+ def parse_shipment_cancel_response(
13
+ _responses: lib.Deserializable[typing.List[typing.Tuple[str, lib.Element]]],
14
+ settings: provider_utils.Settings,
15
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
16
+ responses = [
17
+ (_, provider_error.parse_error_response(response, settings, shipment_id=_))
18
+ for _, response in _responses.deserialize()
19
+ ]
20
+ messages: typing.List[models.Message] = sum([__ for _, __ in responses], start=[])
21
+ success = any([len(errors) == 0 for _, errors in responses])
22
+
23
+ confirmation: models.ConfirmationDetails = (
24
+ models.ConfirmationDetails(
25
+ carrier_id=settings.carrier_id,
26
+ carrier_name=settings.carrier_name,
27
+ success=success,
28
+ operation="Cancel Shipment",
29
+ )
30
+ if success
31
+ else None
32
+ )
33
+
34
+ return confirmation, messages
35
+
36
+
37
+ def shipment_cancel_request(
38
+ payload: models.ShipmentCancelRequest, _
39
+ ) -> lib.Serializable:
40
+ request = list(
41
+ set(
42
+ [
43
+ payload.shipment_identifier,
44
+ *((payload.options or {}).get("shipment_identifiers") or []),
45
+ ]
46
+ )
47
+ )
48
+
49
+ return lib.Serializable(
50
+ request,
51
+ lib.identity,
52
+ dict(email=payload.options.get("email")),
53
+ )