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.
- karrio_cli/__init__.py +0 -0
- karrio_cli/__main__.py +105 -0
- karrio_cli/ai/README.md +335 -0
- karrio_cli/ai/__init__.py +0 -0
- karrio_cli/ai/commands.py +102 -0
- karrio_cli/ai/karrio_ai/__init__.py +1 -0
- karrio_cli/ai/karrio_ai/agent.py +972 -0
- karrio_cli/ai/karrio_ai/architecture/INTEGRATION_AGENT_PROMPT.md +497 -0
- karrio_cli/ai/karrio_ai/architecture/MAPPING_AGENT_PROMPT.md +355 -0
- karrio_cli/ai/karrio_ai/architecture/REAL_WORLD_TESTING.md +305 -0
- karrio_cli/ai/karrio_ai/architecture/SCHEMA_AGENT_PROMPT.md +183 -0
- karrio_cli/ai/karrio_ai/architecture/TESTING_AGENT_PROMPT.md +448 -0
- karrio_cli/ai/karrio_ai/architecture/TESTING_GUIDE.md +271 -0
- karrio_cli/ai/karrio_ai/enhanced_tools.py +943 -0
- karrio_cli/ai/karrio_ai/rag_system.py +503 -0
- karrio_cli/ai/karrio_ai/tests/test_agent.py +350 -0
- karrio_cli/ai/karrio_ai/tests/test_real_integration.py +360 -0
- karrio_cli/ai/karrio_ai/tests/test_real_world_scenarios.py +513 -0
- karrio_cli/commands/__init__.py +0 -0
- karrio_cli/commands/codegen.py +336 -0
- karrio_cli/commands/login.py +139 -0
- karrio_cli/commands/plugins.py +168 -0
- karrio_cli/commands/sdk.py +870 -0
- karrio_cli/common/queries.py +101 -0
- karrio_cli/common/utils.py +368 -0
- karrio_cli/resources/__init__.py +0 -0
- karrio_cli/resources/carriers.py +91 -0
- karrio_cli/resources/connections.py +207 -0
- karrio_cli/resources/events.py +151 -0
- karrio_cli/resources/logs.py +151 -0
- karrio_cli/resources/orders.py +144 -0
- karrio_cli/resources/shipments.py +210 -0
- karrio_cli/resources/trackers.py +287 -0
- karrio_cli/templates/__init__.py +9 -0
- karrio_cli/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/__init__.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/address.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/address.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/docs.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/docs.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/documents.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/documents.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/manifest.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/manifest.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/pickup.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/pickup.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/rates.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/rates.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/sdk.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/sdk.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/shipments.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/shipments.cpython-312.pyc +0 -0
- karrio_cli/templates/__pycache__/tracking.cpython-311.pyc +0 -0
- karrio_cli/templates/__pycache__/tracking.cpython-312.pyc +0 -0
- karrio_cli/templates/address.py +308 -0
- karrio_cli/templates/docs.py +150 -0
- karrio_cli/templates/documents.py +428 -0
- karrio_cli/templates/manifest.py +396 -0
- karrio_cli/templates/pickup.py +839 -0
- karrio_cli/templates/rates.py +638 -0
- karrio_cli/templates/sdk.py +947 -0
- karrio_cli/templates/shipments.py +892 -0
- karrio_cli/templates/tracking.py +437 -0
- karrio_cli-2025.5rc3.dist-info/METADATA +165 -0
- karrio_cli-2025.5rc3.dist-info/RECORD +68 -0
- karrio_cli-2025.5rc3.dist-info/WHEEL +5 -0
- karrio_cli-2025.5rc3.dist-info/entry_points.txt +2 -0
- 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
|
+
]''')
|