karrio-cli 2025.5rc3__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 (68) hide show
  1. karrio_cli/__init__.py +0 -0
  2. karrio_cli/__main__.py +105 -0
  3. karrio_cli/ai/README.md +335 -0
  4. karrio_cli/ai/__init__.py +0 -0
  5. karrio_cli/ai/commands.py +102 -0
  6. karrio_cli/ai/karrio_ai/__init__.py +1 -0
  7. karrio_cli/ai/karrio_ai/agent.py +972 -0
  8. karrio_cli/ai/karrio_ai/architecture/INTEGRATION_AGENT_PROMPT.md +497 -0
  9. karrio_cli/ai/karrio_ai/architecture/MAPPING_AGENT_PROMPT.md +355 -0
  10. karrio_cli/ai/karrio_ai/architecture/REAL_WORLD_TESTING.md +305 -0
  11. karrio_cli/ai/karrio_ai/architecture/SCHEMA_AGENT_PROMPT.md +183 -0
  12. karrio_cli/ai/karrio_ai/architecture/TESTING_AGENT_PROMPT.md +448 -0
  13. karrio_cli/ai/karrio_ai/architecture/TESTING_GUIDE.md +271 -0
  14. karrio_cli/ai/karrio_ai/enhanced_tools.py +943 -0
  15. karrio_cli/ai/karrio_ai/rag_system.py +503 -0
  16. karrio_cli/ai/karrio_ai/tests/test_agent.py +350 -0
  17. karrio_cli/ai/karrio_ai/tests/test_real_integration.py +360 -0
  18. karrio_cli/ai/karrio_ai/tests/test_real_world_scenarios.py +513 -0
  19. karrio_cli/commands/__init__.py +0 -0
  20. karrio_cli/commands/codegen.py +336 -0
  21. karrio_cli/commands/login.py +139 -0
  22. karrio_cli/commands/plugins.py +168 -0
  23. karrio_cli/commands/sdk.py +870 -0
  24. karrio_cli/common/queries.py +101 -0
  25. karrio_cli/common/utils.py +368 -0
  26. karrio_cli/resources/__init__.py +0 -0
  27. karrio_cli/resources/carriers.py +91 -0
  28. karrio_cli/resources/connections.py +207 -0
  29. karrio_cli/resources/events.py +151 -0
  30. karrio_cli/resources/logs.py +151 -0
  31. karrio_cli/resources/orders.py +144 -0
  32. karrio_cli/resources/shipments.py +210 -0
  33. karrio_cli/resources/trackers.py +287 -0
  34. karrio_cli/templates/__init__.py +9 -0
  35. karrio_cli/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  36. karrio_cli/templates/__pycache__/__init__.cpython-312.pyc +0 -0
  37. karrio_cli/templates/__pycache__/address.cpython-311.pyc +0 -0
  38. karrio_cli/templates/__pycache__/address.cpython-312.pyc +0 -0
  39. karrio_cli/templates/__pycache__/docs.cpython-311.pyc +0 -0
  40. karrio_cli/templates/__pycache__/docs.cpython-312.pyc +0 -0
  41. karrio_cli/templates/__pycache__/documents.cpython-311.pyc +0 -0
  42. karrio_cli/templates/__pycache__/documents.cpython-312.pyc +0 -0
  43. karrio_cli/templates/__pycache__/manifest.cpython-311.pyc +0 -0
  44. karrio_cli/templates/__pycache__/manifest.cpython-312.pyc +0 -0
  45. karrio_cli/templates/__pycache__/pickup.cpython-311.pyc +0 -0
  46. karrio_cli/templates/__pycache__/pickup.cpython-312.pyc +0 -0
  47. karrio_cli/templates/__pycache__/rates.cpython-311.pyc +0 -0
  48. karrio_cli/templates/__pycache__/rates.cpython-312.pyc +0 -0
  49. karrio_cli/templates/__pycache__/sdk.cpython-311.pyc +0 -0
  50. karrio_cli/templates/__pycache__/sdk.cpython-312.pyc +0 -0
  51. karrio_cli/templates/__pycache__/shipments.cpython-311.pyc +0 -0
  52. karrio_cli/templates/__pycache__/shipments.cpython-312.pyc +0 -0
  53. karrio_cli/templates/__pycache__/tracking.cpython-311.pyc +0 -0
  54. karrio_cli/templates/__pycache__/tracking.cpython-312.pyc +0 -0
  55. karrio_cli/templates/address.py +308 -0
  56. karrio_cli/templates/docs.py +150 -0
  57. karrio_cli/templates/documents.py +428 -0
  58. karrio_cli/templates/manifest.py +396 -0
  59. karrio_cli/templates/pickup.py +839 -0
  60. karrio_cli/templates/rates.py +638 -0
  61. karrio_cli/templates/sdk.py +947 -0
  62. karrio_cli/templates/shipments.py +892 -0
  63. karrio_cli/templates/tracking.py +437 -0
  64. karrio_cli-2025.5rc3.dist-info/METADATA +165 -0
  65. karrio_cli-2025.5rc3.dist-info/RECORD +68 -0
  66. karrio_cli-2025.5rc3.dist-info/WHEEL +5 -0
  67. karrio_cli-2025.5rc3.dist-info/entry_points.txt +2 -0
  68. karrio_cli-2025.5rc3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,892 @@
1
+ from jinja2 import Template
2
+
3
+ PROVIDER_SHIPMENT_IMPORTS_TEMPLATE = Template(
4
+ """
5
+ from karrio.providers.{{id}}.shipment.create import (
6
+ parse_shipment_response,
7
+ shipment_request,
8
+ )
9
+ from karrio.providers.{{id}}.shipment.cancel import (
10
+ parse_shipment_cancel_response,
11
+ shipment_cancel_request,
12
+ )
13
+
14
+ """
15
+ )
16
+
17
+ PROVIDER_SHIPMENT_CANCEL_TEMPLATE = Template(
18
+ '''"""Karrio {{name}} shipment cancellation API implementation."""
19
+ import typing
20
+ import karrio.lib as lib
21
+ import karrio.core.models as models
22
+ import karrio.providers.{{id}}.error as error
23
+ import karrio.providers.{{id}}.utils as provider_utils
24
+ import karrio.providers.{{id}}.units as provider_units
25
+
26
+
27
+ def parse_shipment_cancel_response(
28
+ _response: lib.Deserializable[{% if is_xml_api %}lib.Element{% else %}dict{% endif %}],
29
+ settings: provider_utils.Settings,
30
+ ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
31
+ """
32
+ Parse shipment cancellation response from carrier API
33
+
34
+ _response: The carrier response to deserialize
35
+ settings: The carrier connection settings
36
+
37
+ Returns a tuple with (ConfirmationDetails, List[Message])
38
+ """
39
+ response = _response.deserialize()
40
+ messages = error.parse_error_response(response, settings)
41
+
42
+ # Extract success state from the response
43
+ success = _extract_cancellation_status(response)
44
+
45
+ # Create confirmation details if successful
46
+ confirmation = (
47
+ models.ConfirmationDetails(
48
+ carrier_id=settings.carrier_id,
49
+ carrier_name=settings.carrier_name,
50
+ operation="Cancel Shipment",
51
+ success=success,
52
+ ) if success else None
53
+ )
54
+
55
+ return confirmation, messages
56
+
57
+
58
+ def _extract_cancellation_status(
59
+ response: {% if is_xml_api %}lib.Element{% else %}dict{% endif %}
60
+ ) -> bool:
61
+ """
62
+ Extract cancellation success status from the carrier response
63
+
64
+ response: The deserialized carrier response
65
+
66
+ Returns True if cancellation was successful, False otherwise
67
+ """
68
+ {% if is_xml_api %}
69
+ # Example implementation for XML response:
70
+ # status_node = lib.find_element("shipment-status", response, first=True)
71
+ # return status_node is not None and status_node.text.lower() == "cancelled"
72
+
73
+ # For development, always return success
74
+ return True
75
+ {% else %}
76
+ # Example implementation for JSON response:
77
+ # return response.get("success", False)
78
+
79
+ # For development, always return success
80
+ return True
81
+ {% endif %}
82
+
83
+
84
+ def shipment_cancel_request(
85
+ payload: models.ShipmentCancelRequest,
86
+ settings: provider_utils.Settings,
87
+ ) -> lib.Serializable:
88
+ """
89
+ Create a shipment cancellation request for the carrier API
90
+
91
+ payload: The standardized ShipmentCancelRequest from karrio
92
+ settings: The carrier connection settings
93
+
94
+ Returns a Serializable object that can be sent to the carrier API
95
+ """
96
+ {% if is_xml_api %}
97
+ # Create XML request for shipment cancellation
98
+ # Example implementation:
99
+ # import karrio.schemas.{{id}}.shipment_cancel_request as {{id}}_req
100
+ #
101
+ # request = {{id}}_req.ShipmentCancelRequest(
102
+ # AccountNumber=settings.account_number,
103
+ # ShipmentReference=payload.shipment_identifier,
104
+ # # Add any other required fields
105
+ # )
106
+ #
107
+ # return lib.Serializable(
108
+ # request,
109
+ # lambda _: lib.to_xml(
110
+ # _,
111
+ # name_="ShipmentCancelRequest",
112
+ # namespacedef_=(
113
+ # 'xmlns="http://{{id}}.com/schema/shipment/cancel"'
114
+ # ),
115
+ # )
116
+ # )
117
+
118
+ # For development, return a simple XML request
119
+ request = f"""<?xml version="1.0"?>
120
+ <shipment-cancel-request>
121
+ <shipment-reference>{payload.shipment_identifier}</shipment-reference>
122
+ </shipment-cancel-request>"""
123
+
124
+ return lib.Serializable(request, lambda r: r)
125
+ {% else %}
126
+ # Create JSON request for shipment cancellation
127
+ # Example implementation:
128
+ # import karrio.schemas.{{id}}.shipment_cancel_request as {{id}}_req
129
+ #
130
+ # request = {{id}}_req.ShipmentCancelRequestType(
131
+ # shipmentId=payload.shipment_identifier,
132
+ # accountNumber=settings.account_number,
133
+ # # Add any other required fields
134
+ # )
135
+ #
136
+ # return lib.Serializable(request, lib.to_dict)
137
+
138
+ # For development, return a simple JSON request
139
+ request = {
140
+ "shipmentId": payload.shipment_identifier
141
+ }
142
+
143
+ return lib.Serializable(request, lib.to_dict)
144
+ {% endif %}
145
+
146
+ '''
147
+ )
148
+
149
+ PROVIDER_SHIPMENT_CREATE_TEMPLATE = Template(
150
+ '''"""Karrio {{name}} shipment API implementation."""
151
+
152
+ # IMPLEMENTATION INSTRUCTIONS:
153
+ # 1. Uncomment the imports when the schema types are generated
154
+ # 2. Import the specific request and response types you need
155
+ # 3. Create a request instance with the appropriate request type
156
+ # 4. Extract shipment details from the response
157
+ #
158
+ # NOTE: JSON schema types are generated with "Type" suffix (e.g., ShipmentRequestType),
159
+ # while XML schema types don't have this suffix (e.g., ShipmentRequest).
160
+
161
+ import karrio.schemas.{{id}}.shipment_request as {{id}}_req
162
+ import karrio.schemas.{{id}}.shipment_response as {{id}}_res
163
+
164
+ import typing
165
+ import karrio.lib as lib
166
+ import karrio.core.units as units
167
+ import karrio.core.models as models
168
+ import karrio.providers.{{id}}.error as error
169
+ import karrio.providers.{{id}}.utils as provider_utils
170
+ import karrio.providers.{{id}}.units as provider_units
171
+
172
+
173
+ def parse_shipment_response(
174
+ _response: lib.Deserializable[{% if is_xml_api %}lib.Element{% else %}dict{% endif %}],
175
+ settings: provider_utils.Settings,
176
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
177
+ response = _response.deserialize()
178
+ messages = error.parse_error_response(response, settings)
179
+
180
+ # Check if we have valid shipment data
181
+ {% if is_xml_api %}
182
+ has_shipment = response.xpath(".//shipment") if hasattr(response, 'xpath') else False
183
+ {% else %}
184
+ has_shipment = "shipment" in response if hasattr(response, 'get') else False
185
+ {% endif %}
186
+
187
+ shipment = _extract_details(response, settings) if has_shipment else None
188
+
189
+ return shipment, messages
190
+
191
+
192
+ def _extract_details(
193
+ data: {% if is_xml_api %}lib.Element{% else %}dict{% endif %},
194
+ settings: provider_utils.Settings,
195
+ ) -> models.ShipmentDetails:
196
+ """
197
+ Extract shipment details from carrier response data
198
+
199
+ data: The carrier-specific shipment data structure
200
+ settings: The carrier connection settings
201
+
202
+ Returns a ShipmentDetails object with extracted shipment information
203
+ """
204
+ # Convert the carrier data to a proper object for easy attribute access
205
+ {% if is_xml_api %}
206
+ # For XML APIs, convert Element to proper response object
207
+ shipment = lib.to_object({{id}}_res.ShipmentResponse, data)
208
+
209
+ # Extract tracking info
210
+ tracking_number = shipment.tracking_number if hasattr(shipment, 'tracking_number') else ""
211
+ shipment_id = shipment.shipment_id if hasattr(shipment, 'shipment_id') else ""
212
+
213
+ # Extract label info
214
+ label_format = shipment.label_format if hasattr(shipment, 'label_format') else "PDF"
215
+ label_base64 = shipment.label_image if hasattr(shipment, 'label_image') else ""
216
+
217
+ # Extract optional invoice
218
+ invoice_base64 = shipment.invoice_image if hasattr(shipment, 'invoice_image') else ""
219
+
220
+ # Extract service code for metadata
221
+ service_code = shipment.service_code if hasattr(shipment, 'service_code') else ""
222
+ {% else %}
223
+ # For JSON APIs, convert dict to proper response object
224
+ response_obj = lib.to_object({{id}}_res.ShipmentResponseType, data)
225
+
226
+ # Access the shipment data
227
+ shipment = response_obj.shipment if hasattr(response_obj, 'shipment') else None
228
+
229
+ if shipment:
230
+ # Extract tracking info
231
+ tracking_number = shipment.trackingNumber if hasattr(shipment, 'trackingNumber') else ""
232
+ shipment_id = shipment.shipmentId if hasattr(shipment, 'shipmentId') else ""
233
+
234
+ # Extract label info
235
+ label_data = shipment.labelData if hasattr(shipment, 'labelData') else None
236
+ label_format = label_data.format if label_data and hasattr(label_data, 'format') else "PDF"
237
+ label_base64 = label_data.image if label_data and hasattr(label_data, 'image') else ""
238
+
239
+ # Extract optional invoice
240
+ invoice_base64 = shipment.invoiceImage if hasattr(shipment, 'invoiceImage') else ""
241
+
242
+ # Extract service code for metadata
243
+ service_code = shipment.serviceCode if hasattr(shipment, 'serviceCode') else ""
244
+ else:
245
+ tracking_number = ""
246
+ shipment_id = ""
247
+ label_format = "PDF"
248
+ label_base64 = ""
249
+ invoice_base64 = ""
250
+ service_code = ""
251
+ {% endif %}
252
+
253
+ documents = models.Documents(
254
+ label=label_base64,
255
+ )
256
+
257
+ # Add invoice if present
258
+ if invoice_base64:
259
+ documents.invoice = invoice_base64
260
+
261
+ return models.ShipmentDetails(
262
+ carrier_id=settings.carrier_id,
263
+ carrier_name=settings.carrier_name,
264
+ tracking_number=tracking_number,
265
+ shipment_identifier=shipment_id,
266
+ label_type=label_format,
267
+ docs=documents,
268
+ meta=dict(
269
+ service_code=service_code,
270
+ # Add any other relevant metadata from the carrier's response
271
+ ),
272
+ )
273
+
274
+
275
+ def shipment_request(
276
+ payload: models.ShipmentRequest,
277
+ settings: provider_utils.Settings,
278
+ ) -> lib.Serializable:
279
+ """
280
+ Create a shipment request for the carrier API
281
+
282
+ payload: The standardized ShipmentRequest from karrio
283
+ settings: The carrier connection settings
284
+
285
+ Returns a Serializable object that can be sent to the carrier API
286
+ """
287
+ # Convert karrio models to carrier-specific format
288
+ shipper = lib.to_address(payload.shipper)
289
+ recipient = lib.to_address(payload.recipient)
290
+ packages = lib.to_packages(payload.parcels)
291
+ service = provider_units.ShippingService.map(payload.service).value_or_key
292
+ options = lib.to_shipping_options(
293
+ payload.options,
294
+ package_options=packages.options,
295
+ initializer=provider_units.shipping_options_initializer,
296
+ )
297
+
298
+ # Create the carrier-specific request object
299
+ {% if is_xml_api %}
300
+ # For XML API request
301
+ request = {{id}}_req.ShipmentRequest(
302
+ # Map shipper details
303
+ shipper={{id}}_req.Address(
304
+ address_line1=shipper.address_line1,
305
+ city=shipper.city,
306
+ postal_code=shipper.postal_code,
307
+ country_code=shipper.country_code,
308
+ state_code=shipper.state_code,
309
+ person_name=shipper.person_name,
310
+ company_name=shipper.company_name,
311
+ phone_number=shipper.phone_number,
312
+ email=shipper.email,
313
+ ),
314
+ # Map recipient details
315
+ recipient={{id}}_req.Address(
316
+ address_line1=recipient.address_line1,
317
+ city=recipient.city,
318
+ postal_code=recipient.postal_code,
319
+ country_code=recipient.country_code,
320
+ state_code=recipient.state_code,
321
+ person_name=recipient.person_name,
322
+ company_name=recipient.company_name,
323
+ phone_number=recipient.phone_number,
324
+ email=recipient.email,
325
+ ),
326
+ # Map package details
327
+ packages=[
328
+ {{id}}_req.Package(
329
+ weight=package.weight.value,
330
+ weight_unit=provider_units.WeightUnit[package.weight.unit].value,
331
+ length=package.length.value if package.length else None,
332
+ width=package.width.value if package.width else None,
333
+ height=package.height.value if package.height else None,
334
+ dimension_unit=provider_units.DimensionUnit[package.dimension_unit].value if package.dimension_unit else None,
335
+ packaging_type=provider_units.PackagingType[package.packaging_type or 'your_packaging'].value,
336
+ )
337
+ for package in packages
338
+ ],
339
+ # Add service code
340
+ service_code=service,
341
+ # Add account information
342
+ customer_number=settings.customer_number,
343
+ # Add label details
344
+ label_format=payload.label_type or "PDF",
345
+ # Add any other required fields for the carrier API
346
+ )
347
+ {% else %}
348
+ # For JSON API request
349
+ request = {{id}}_req.ShipmentRequestType(
350
+ # Map shipper details
351
+ shipper={
352
+ "addressLine1": shipper.address_line1,
353
+ "city": shipper.city,
354
+ "postalCode": shipper.postal_code,
355
+ "countryCode": shipper.country_code,
356
+ "stateCode": shipper.state_code,
357
+ "personName": shipper.person_name,
358
+ "companyName": shipper.company_name,
359
+ "phoneNumber": shipper.phone_number,
360
+ "email": shipper.email,
361
+ },
362
+ # Map recipient details
363
+ recipient={
364
+ "addressLine1": recipient.address_line1,
365
+ "city": recipient.city,
366
+ "postalCode": recipient.postal_code,
367
+ "countryCode": recipient.country_code,
368
+ "stateCode": recipient.state_code,
369
+ "personName": recipient.person_name,
370
+ "companyName": recipient.company_name,
371
+ "phoneNumber": recipient.phone_number,
372
+ "email": recipient.email,
373
+ },
374
+ # Map package details
375
+ packages=[
376
+ {
377
+ "weight": package.weight.value,
378
+ "weightUnit": provider_units.WeightUnit[package.weight.unit].value,
379
+ "length": package.length.value if package.length else None,
380
+ "width": package.width.value if package.width else None,
381
+ "height": package.height.value if package.height else None,
382
+ "dimensionUnit": provider_units.DimensionUnit[package.dimension_unit].value if package.dimension_unit else None,
383
+ "packagingType": provider_units.PackagingType[package.packaging_type or 'your_packaging'].value,
384
+ }
385
+ for package in packages
386
+ ],
387
+ # Add service code
388
+ serviceCode=service,
389
+ # Add account information
390
+ customerNumber=settings.customer_number,
391
+ # Add label details
392
+ labelFormat=payload.label_type or "PDF",
393
+ # Add any other required fields for this carrier's API
394
+ )
395
+ {% endif %}
396
+
397
+ return lib.Serializable(request, {% if is_xml_api %}lib.to_xml{% else %}lib.to_dict{% endif %})
398
+
399
+ '''
400
+ )
401
+
402
+
403
+ XML_SCHEMA_SHIPMENT_REQUEST_TEMPLATE = Template("""<?xml version="1.0"?>
404
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://{{id}}.com/ws/shipment" xmlns="http://{{id}}.com/ws/shipment" elementFormDefault="qualified">
405
+ <xsd:element name="shipment-request">
406
+ <xsd:complexType>
407
+ <xsd:all>
408
+ <xsd:element name="shipper">
409
+ <xsd:complexType>
410
+ <xsd:all>
411
+ <xsd:element name="address-line1" type="xsd:string" />
412
+ <xsd:element name="city" type="xsd:string" />
413
+ <xsd:element name="postal-code" type="xsd:string" />
414
+ <xsd:element name="country-code" type="xsd:string" />
415
+ <xsd:element name="state-code" type="xsd:string" minOccurs="0" />
416
+ <xsd:element name="person-name" type="xsd:string" minOccurs="0" />
417
+ <xsd:element name="company-name" type="xsd:string" minOccurs="0" />
418
+ <xsd:element name="phone-number" type="xsd:string" minOccurs="0" />
419
+ <xsd:element name="email" type="xsd:string" minOccurs="0" />
420
+ </xsd:all>
421
+ </xsd:complexType>
422
+ </xsd:element>
423
+ <xsd:element name="recipient">
424
+ <xsd:complexType>
425
+ <xsd:all>
426
+ <xsd:element name="address-line1" type="xsd:string" />
427
+ <xsd:element name="city" type="xsd:string" />
428
+ <xsd:element name="postal-code" type="xsd:string" />
429
+ <xsd:element name="country-code" type="xsd:string" />
430
+ <xsd:element name="state-code" type="xsd:string" minOccurs="0" />
431
+ <xsd:element name="person-name" type="xsd:string" minOccurs="0" />
432
+ <xsd:element name="company-name" type="xsd:string" minOccurs="0" />
433
+ <xsd:element name="phone-number" type="xsd:string" minOccurs="0" />
434
+ <xsd:element name="email" type="xsd:string" minOccurs="0" />
435
+ </xsd:all>
436
+ </xsd:complexType>
437
+ </xsd:element>
438
+ <xsd:element name="packages">
439
+ <xsd:complexType>
440
+ <xsd:sequence>
441
+ <xsd:element name="package" maxOccurs="unbounded">
442
+ <xsd:complexType>
443
+ <xsd:all>
444
+ <xsd:element name="weight" type="xsd:decimal" />
445
+ <xsd:element name="weight-unit" type="xsd:string" />
446
+ <xsd:element name="length" type="xsd:decimal" minOccurs="0" />
447
+ <xsd:element name="width" type="xsd:decimal" minOccurs="0" />
448
+ <xsd:element name="height" type="xsd:decimal" minOccurs="0" />
449
+ <xsd:element name="dimension-unit" type="xsd:string" minOccurs="0" />
450
+ <xsd:element name="packaging-type" type="xsd:string" minOccurs="0" />
451
+ </xsd:all>
452
+ </xsd:complexType>
453
+ </xsd:element>
454
+ </xsd:sequence>
455
+ </xsd:complexType>
456
+ </xsd:element>
457
+ <xsd:element name="service" type="xsd:string" />
458
+ <xsd:element name="options" type="xsd:string" minOccurs="0" />
459
+ <xsd:element name="label-type" type="xsd:string" minOccurs="0" />
460
+ </xsd:all>
461
+ </xsd:complexType>
462
+ </xsd:element>
463
+ </xsd:schema>
464
+ """
465
+ )
466
+
467
+ XML_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE = Template("""<?xml version="1.0"?>
468
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://{{id}}.com/ws/shipment" xmlns="http://{{id}}.com/ws/shipment" elementFormDefault="qualified">
469
+ <xsd:element name="shipment-response">
470
+ <xsd:complexType>
471
+ <xsd:all>
472
+ <xsd:element name="tracking-number" type="xsd:string" />
473
+ <xsd:element name="shipment-identifier" type="xsd:string" />
474
+ <xsd:element name="label-type" type="xsd:string" />
475
+ <xsd:element name="label" type="xsd:base64Binary" />
476
+ <xsd:element name="documents" minOccurs="0">
477
+ <xsd:complexType>
478
+ <xsd:all>
479
+ <xsd:element name="invoice" type="xsd:base64Binary" minOccurs="0" />
480
+ </xsd:all>
481
+ </xsd:complexType>
482
+ </xsd:element>
483
+ <xsd:element name="meta" minOccurs="0">
484
+ <xsd:complexType>
485
+ <xsd:all>
486
+ <xsd:element name="service-code" type="xsd:string" minOccurs="0" />
487
+ </xsd:all>
488
+ </xsd:complexType>
489
+ </xsd:element>
490
+ </xsd:all>
491
+ </xsd:complexType>
492
+ </xsd:element>
493
+ </xsd:schema>
494
+ """
495
+ )
496
+
497
+ JSON_SCHEMA_SHIPMENT_REQUEST_TEMPLATE = Template(
498
+ """{
499
+ "shipmentRequest": {
500
+ "shipper": {
501
+ "addressLine1": "123 Main St",
502
+ "city": "Anytown",
503
+ "postalCode": "12345",
504
+ "countryCode": "US",
505
+ "stateCode": "CA",
506
+ "personName": "John Doe",
507
+ "companyName": "ACME Corp",
508
+ "phoneNumber": "555-123-4567",
509
+ "email": "john@example.com"
510
+ },
511
+ "recipient": {
512
+ "addressLine1": "456 Oak St",
513
+ "city": "Somewhere",
514
+ "postalCode": "67890",
515
+ "countryCode": "US",
516
+ "stateCode": "NY",
517
+ "personName": "Jane Smith",
518
+ "companyName": "XYZ Inc",
519
+ "phoneNumber": "555-987-6543",
520
+ "email": "jane@example.com"
521
+ },
522
+ "packages": [
523
+ {
524
+ "weight": 10.5,
525
+ "weightUnit": "KG",
526
+ "length": 20.0,
527
+ "width": 15.0,
528
+ "height": 10.0,
529
+ "dimensionUnit": "CM",
530
+ "packagingType": "BOX"
531
+ }
532
+ ],
533
+ "service": "EXPRESS",
534
+ "options": {
535
+ "insurance": true,
536
+ "signature_required": false
537
+ },
538
+ "labelType": "PDF"
539
+ }
540
+ }
541
+ """
542
+ )
543
+
544
+ JSON_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE = Template(
545
+ """{
546
+ "shipmentResponse": {
547
+ "trackingNumber": "1Z999999999999999",
548
+ "shipmentIdentifier": "SHIP123456",
549
+ "labelType": "PDF",
550
+ "label": "base64_encoded_label_data",
551
+ "documents": {
552
+ "invoice": "base64_encoded_invoice_data"
553
+ },
554
+ "meta": {
555
+ "serviceCode": "EXPRESS"
556
+ }
557
+ }
558
+ }
559
+ """
560
+ )
561
+
562
+ XML_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE = Template("""<?xml version="1.0"?>
563
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://{{id}}.com/ws/shipment-cancel" xmlns="http://{{id}}.com/ws/shipment-cancel" elementFormDefault="qualified">
564
+ <xsd:element name="shipment-cancel-request">
565
+ <xsd:complexType>
566
+ <xsd:all>
567
+ <xsd:element name="shipment-identifier" type="xsd:string" />
568
+ </xsd:all>
569
+ </xsd:complexType>
570
+ </xsd:element>
571
+ </xsd:schema>
572
+ """
573
+ )
574
+
575
+ XML_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE = Template("""<?xml version="1.0"?>
576
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://{{id}}.com/ws/shipment-cancel" xmlns="http://{{id}}.com/ws/shipment-cancel" elementFormDefault="qualified">
577
+ <xsd:element name="shipment-cancel-response">
578
+ <xsd:complexType>
579
+ <xsd:all>
580
+ <xsd:element name="success" type="xsd:boolean" />
581
+ <xsd:element name="message" type="xsd:string" minOccurs="0" />
582
+ </xsd:all>
583
+ </xsd:complexType>
584
+ </xsd:element>
585
+ </xsd:schema>
586
+ """
587
+ )
588
+
589
+ JSON_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE = Template(
590
+ """{
591
+ "shipmentCancelRequest": {
592
+ "shipmentIdentifier": "SHIP123456"
593
+ }
594
+ }
595
+ """
596
+ )
597
+
598
+ JSON_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE = Template(
599
+ """{
600
+ "shipmentCancelResponse": {
601
+ "success": true,
602
+ "message": "Shipment successfully cancelled"
603
+ }
604
+ }
605
+ """
606
+ )
607
+
608
+ TEST_SHIPMENT_TEMPLATE = Template('''"""{{name}} carrier shipment tests."""
609
+
610
+ import unittest
611
+ from unittest.mock import patch, ANY
612
+ from .fixture import gateway
613
+ import logging
614
+ import karrio.sdk as karrio
615
+ import karrio.lib as lib
616
+ import karrio.core.models as models
617
+
618
+ logger = logging.getLogger(__name__)
619
+
620
+ class Test{{compact_name}}Shipment(unittest.TestCase):
621
+ def setUp(self):
622
+ self.maxDiff = None
623
+ self.ShipmentRequest = models.ShipmentRequest(**ShipmentPayload)
624
+ self.ShipmentCancelRequest = models.ShipmentCancelRequest(**ShipmentCancelPayload)
625
+
626
+ def test_create_shipment_request(self):
627
+ request = gateway.mapper.create_shipment_request(self.ShipmentRequest)
628
+ self.assertEqual(lib.to_dict(request.serialize()), ShipmentRequest)
629
+
630
+ def test_create_shipment(self):
631
+ with patch("karrio.mappers.{{id}}.proxy.lib.request") as mock:
632
+ mock.return_value = {% if is_xml_api %}"<r></r>"{% else %}"{}"{% endif %}
633
+ karrio.Shipment.create(self.ShipmentRequest).from_(gateway)
634
+ self.assertEqual(
635
+ mock.call_args[1]["url"],
636
+ f"{gateway.settings.server_url}/shipments"
637
+ )
638
+
639
+ def test_parse_shipment_response(self):
640
+ with patch("karrio.mappers.{{id}}.proxy.lib.request") as mock:
641
+ mock.return_value = ShipmentResponse
642
+ parsed_response = (
643
+ karrio.Shipment.create(self.ShipmentRequest)
644
+ .from_(gateway)
645
+ .parse()
646
+ )
647
+ self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentResponse)
648
+
649
+ def test_create_shipment_cancel_request(self):
650
+ request = gateway.mapper.create_shipment_cancel_request(self.ShipmentCancelRequest)
651
+ self.assertEqual(lib.to_dict(request.serialize()), ShipmentCancelRequest)
652
+
653
+ def test_cancel_shipment(self):
654
+ with patch("karrio.mappers.{{id}}.proxy.lib.request") as mock:
655
+ mock.return_value = {% if is_xml_api %}"<r></r>"{% else %}"{}"{% endif %}
656
+ karrio.Shipment.cancel(self.ShipmentCancelRequest).from_(gateway)
657
+ self.assertEqual(
658
+ mock.call_args[1]["url"],
659
+ f"{gateway.settings.server_url}/shipments/SHIP123456/cancel"
660
+ )
661
+
662
+ def test_parse_shipment_cancel_response(self):
663
+ with patch("karrio.mappers.{{id}}.proxy.lib.request") as mock:
664
+ mock.return_value = ShipmentCancelResponse
665
+ parsed_response = (
666
+ karrio.Shipment.cancel(self.ShipmentCancelRequest)
667
+ .from_(gateway)
668
+ .parse()
669
+ )
670
+ self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentCancelResponse)
671
+
672
+ def test_parse_error_response(self):
673
+ with patch("karrio.mappers.{{id}}.proxy.lib.request") as mock:
674
+ mock.return_value = ErrorResponse
675
+ parsed_response = (
676
+ karrio.Shipment.create(self.ShipmentRequest)
677
+ .from_(gateway)
678
+ .parse()
679
+ )
680
+ self.assertListEqual(lib.to_dict(parsed_response), ParsedErrorResponse)
681
+
682
+
683
+ if __name__ == "__main__":
684
+ unittest.main()
685
+
686
+
687
+ ShipmentPayload = {
688
+ "shipper": {
689
+ "address_line1": "123 Test Street",
690
+ "city": "Test City",
691
+ "postal_code": "12345",
692
+ "country_code": "US",
693
+ "state_code": "CA",
694
+ "person_name": "Test Person",
695
+ "company_name": "Test Company",
696
+ "phone_number": "1234567890",
697
+ "email": "test@example.com"
698
+ },
699
+ "recipient": {
700
+ "address_line1": "123 Test Street",
701
+ "city": "Test City",
702
+ "postal_code": "12345",
703
+ "country_code": "US",
704
+ "state_code": "CA",
705
+ "person_name": "Test Person",
706
+ "company_name": "Test Company",
707
+ "phone_number": "1234567890",
708
+ "email": "test@example.com"
709
+ },
710
+ "parcels": [{
711
+ "weight": 10.0,
712
+ "width": 10.0,
713
+ "height": 10.0,
714
+ "length": 10.0,
715
+ "weight_unit": "KG",
716
+ "dimension_unit": "CM",
717
+ "packaging_type": "BOX"
718
+ }],
719
+ "service": "express"
720
+ }
721
+
722
+ ShipmentCancelPayload = {
723
+ "shipment_identifier": "SHIP123456"
724
+ }
725
+
726
+ ShipmentRequest = {% if is_xml_api %}{
727
+ "shipper": {
728
+ "address_line1": "123 Test Street",
729
+ "city": "Test City",
730
+ "postal_code": "12345",
731
+ "country_code": "US",
732
+ "state_code": "CA",
733
+ "person_name": "Test Person",
734
+ "company_name": "Test Company",
735
+ "phone_number": "1234567890",
736
+ "email": "test@example.com"
737
+ },
738
+ "recipient": {
739
+ "address_line1": "123 Test Street",
740
+ "city": "Test City",
741
+ "postal_code": "12345",
742
+ "country_code": "US",
743
+ "state_code": "CA",
744
+ "person_name": "Test Person",
745
+ "company_name": "Test Company",
746
+ "phone_number": "1234567890",
747
+ "email": "test@example.com"
748
+ },
749
+ "packages": [
750
+ {
751
+ "weight": 10.0,
752
+ "weight_unit": "KG",
753
+ "length": 10.0,
754
+ "width": 10.0,
755
+ "height": 10.0,
756
+ "dimension_unit": "CM",
757
+ "packaging_type": "BOX"
758
+ }
759
+ ],
760
+ "service_code": "express",
761
+ "label_format": "PDF"
762
+ }{% else %}{
763
+ "shipper": {
764
+ "addressLine1": "123 Test Street",
765
+ "city": "Test City",
766
+ "postalCode": "12345",
767
+ "countryCode": "US",
768
+ "stateCode": "CA",
769
+ "personName": "Test Person",
770
+ "companyName": "Test Company",
771
+ "phoneNumber": "1234567890",
772
+ "email": "test@example.com"
773
+ },
774
+ "recipient": {
775
+ "addressLine1": "123 Test Street",
776
+ "city": "Test City",
777
+ "postalCode": "12345",
778
+ "countryCode": "US",
779
+ "stateCode": "CA",
780
+ "personName": "Test Person",
781
+ "companyName": "Test Company",
782
+ "phoneNumber": "1234567890",
783
+ "email": "test@example.com"
784
+ },
785
+ "packages": [
786
+ {
787
+ "weight": 10.0,
788
+ "weightUnit": "KG",
789
+ "length": 10.0,
790
+ "width": 10.0,
791
+ "height": 10.0,
792
+ "dimensionUnit": "CM",
793
+ "packagingType": "BOX"
794
+ }
795
+ ],
796
+ "serviceCode": "express",
797
+ "labelFormat": "PDF"
798
+ }{% endif %}
799
+
800
+ ShipmentCancelRequest = {% if is_xml_api %}{
801
+ "shipment_identifier": "SHIP123456"
802
+ }{% else %}{
803
+ "shipmentIdentifier": "SHIP123456"
804
+ }{% endif %}
805
+
806
+ ShipmentResponse = {% if is_xml_api %}"""<?xml version="1.0"?>
807
+ <shipment-response>
808
+ <tracking-number>1Z999999999999999</tracking-number>
809
+ <shipment-id>SHIP123456</shipment-id>
810
+ <label-format>PDF</label-format>
811
+ <label-image>base64_encoded_label_data</label-image>
812
+ <invoice-image>base64_encoded_invoice_data</invoice-image>
813
+ <service-code>express</service-code>
814
+ </shipment-response>"""{% else %}"""{
815
+ "shipment": {
816
+ "trackingNumber": "1Z999999999999999",
817
+ "shipmentId": "SHIP123456",
818
+ "labelData": {
819
+ "format": "PDF",
820
+ "image": "base64_encoded_label_data"
821
+ },
822
+ "invoiceImage": "base64_encoded_invoice_data",
823
+ "serviceCode": "express"
824
+ }
825
+ }"""{% endif %}
826
+
827
+ ShipmentCancelResponse = {% if is_xml_api %}"""<?xml version="1.0"?>
828
+ <shipment-cancel-response>
829
+ <success>true</success>
830
+ <message>Shipment successfully cancelled</message>
831
+ </shipment-cancel-response>"""{% else %}"""{
832
+ "success": true,
833
+ "message": "Shipment successfully cancelled"
834
+ }"""{% endif %}
835
+
836
+ ErrorResponse = {% if is_xml_api %}"""<?xml version="1.0"?>
837
+ <error-response>
838
+ <e>
839
+ <code>shipment_error</code>
840
+ <message>Unable to create shipment</message>
841
+ <details>Invalid shipment information provided</details>
842
+ </e>
843
+ </error-response>"""{% else %}"""{
844
+ "error": {
845
+ "code": "shipment_error",
846
+ "message": "Unable to create shipment",
847
+ "details": "Invalid shipment information provided"
848
+ }
849
+ }"""{% endif %}
850
+
851
+ ParsedShipmentResponse = [
852
+ {
853
+ "carrier_id": "{{id}}",
854
+ "carrier_name": "{{id}}",
855
+ "tracking_number": "1Z999999999999999",
856
+ "shipment_identifier": "SHIP123456",
857
+ "label_type": "PDF",
858
+ "docs": {
859
+ "label": "base64_encoded_label_data",
860
+ "invoice": "base64_encoded_invoice_data"
861
+ },
862
+ "meta": {
863
+ "service_code": "express"
864
+ }
865
+ },
866
+ []
867
+ ]
868
+
869
+ ParsedShipmentCancelResponse = [
870
+ {
871
+ "carrier_id": "{{id}}",
872
+ "carrier_name": "{{id}}",
873
+ "success": True,
874
+ "operation": "Cancel Shipment"
875
+ },
876
+ []
877
+ ]
878
+
879
+ ParsedErrorResponse = [
880
+ {},
881
+ [
882
+ {
883
+ "carrier_id": "{{id}}",
884
+ "carrier_name": "{{id}}",
885
+ "code": "shipment_error",
886
+ "message": "Unable to create shipment",
887
+ "details": {
888
+ "details": "Invalid shipment information provided"
889
+ }
890
+ }
891
+ ]
892
+ ]''')