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,870 @@
1
+ import os
2
+ import time
3
+ import typer
4
+ import string
5
+ import pathlib
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn
7
+ import karrio_cli.templates as templates
8
+ import karrio_cli.common.utils as utils
9
+ import typing
10
+ import datetime
11
+
12
+
13
+ def _add_extension(
14
+ id: str,
15
+ name: str,
16
+ feature: str,
17
+ version: str,
18
+ is_xml_api: bool,
19
+ path: str,
20
+ ):
21
+ # Resolve the provided path
22
+ base_dir = pathlib.Path(path).resolve()
23
+
24
+ # Create full path for confirmation
25
+ full_path = base_dir / id
26
+
27
+ features = [f.strip() for f in feature.split(",")]
28
+ context = dict(
29
+ id=id,
30
+ name=name,
31
+ features=features,
32
+ version=version,
33
+ is_xml_api=is_xml_api,
34
+ compact_name=name.strip()
35
+ .replace("-", "")
36
+ .replace("_", "")
37
+ .replace("&", "")
38
+ .replace(" ", ""),
39
+ )
40
+
41
+ # Update the directory templates with the base directory
42
+ root_dir = full_path
43
+ schemas_dir = root_dir / "schemas"
44
+ tests_dir = root_dir / "tests" / id
45
+ plugins_dir = root_dir / "karrio" / "plugins" / id
46
+ mappers_dir = root_dir / "karrio" / "mappers" / id
47
+ providers_dir = root_dir / "karrio" / "providers" / id
48
+ schema_datatypes_dir = root_dir / "karrio" / "schemas" / id
49
+
50
+ with Progress(
51
+ SpinnerColumn(),
52
+ TextColumn("[progress.description]{task.description}"),
53
+ transient=True,
54
+ ) as progress:
55
+ progress.add_task(description="Processing...", total=None)
56
+ os.makedirs(tests_dir, exist_ok=True)
57
+ os.makedirs(schemas_dir, exist_ok=True)
58
+ os.makedirs(plugins_dir, exist_ok=True)
59
+ os.makedirs(mappers_dir, exist_ok=True)
60
+ os.makedirs(providers_dir, exist_ok=True)
61
+ os.makedirs(schema_datatypes_dir, exist_ok=True)
62
+ time.sleep(1)
63
+
64
+ # project files
65
+ templates.PYPROJECT_TEMPLATE.stream(**context).dump(
66
+ f"{root_dir}/pyproject.toml"
67
+ )
68
+ templates.README_TEMPLATE.stream(**context).dump(
69
+ f"{root_dir}/README.md"
70
+ )
71
+ (
72
+ templates.XML_GENERATE_TEMPLATE
73
+ if is_xml_api
74
+ else templates.JSON_GENERATE_TEMPLATE
75
+ ).stream(**context).dump(f"{root_dir}/generate")
76
+
77
+ # schema files - create error response schema for all carriers
78
+ if is_xml_api:
79
+ templates.XML_SCHEMA_ERROR_TEMPLATE.stream(**context).dump(
80
+ f"{schemas_dir}/error_response.xsd"
81
+ )
82
+ else:
83
+ templates.JSON_SCHEMA_ERROR_TEMPLATE.stream(**context).dump(
84
+ f"{schemas_dir}/error_response.json"
85
+ )
86
+
87
+ templates.EMPTY_FILE_TEMPLATE.stream(**context).dump(
88
+ f"{schema_datatypes_dir}/__init__.py"
89
+ )
90
+
91
+ # Generate schema files for selected features
92
+ if "rating" in features:
93
+ if is_xml_api:
94
+ templates.XML_SCHEMA_RATE_REQUEST_TEMPLATE.stream(**context).dump(
95
+ f"{schemas_dir}/rate_request.xsd"
96
+ )
97
+ templates.XML_SCHEMA_RATE_RESPONSE_TEMPLATE.stream(**context).dump(
98
+ f"{schemas_dir}/rate_response.xsd"
99
+ )
100
+ else:
101
+ templates.JSON_SCHEMA_RATE_REQUEST_TEMPLATE.stream(**context).dump(
102
+ f"{schemas_dir}/rate_request.json"
103
+ )
104
+ templates.JSON_SCHEMA_RATE_RESPONSE_TEMPLATE.stream(**context).dump(
105
+ f"{schemas_dir}/rate_response.json"
106
+ )
107
+
108
+ if "tracking" in features:
109
+ if is_xml_api:
110
+ templates.XML_SCHEMA_TRACKING_REQUEST_TEMPLATE.stream(**context).dump(
111
+ f"{schemas_dir}/tracking_request.xsd"
112
+ )
113
+ templates.XML_SCHEMA_TRACKING_RESPONSE_TEMPLATE.stream(**context).dump(
114
+ f"{schemas_dir}/tracking_response.xsd"
115
+ )
116
+ else:
117
+ templates.JSON_SCHEMA_TRACKING_REQUEST_TEMPLATE.stream(**context).dump(
118
+ f"{schemas_dir}/tracking_request.json"
119
+ )
120
+ templates.JSON_SCHEMA_TRACKING_RESPONSE_TEMPLATE.stream(**context).dump(
121
+ f"{schemas_dir}/tracking_response.json"
122
+ )
123
+
124
+ if "shipping" in features:
125
+ if is_xml_api:
126
+ templates.XML_SCHEMA_SHIPMENT_REQUEST_TEMPLATE.stream(**context).dump(
127
+ f"{schemas_dir}/shipment_request.xsd"
128
+ )
129
+ templates.XML_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE.stream(**context).dump(
130
+ f"{schemas_dir}/shipment_response.xsd"
131
+ )
132
+ templates.XML_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
133
+ f"{schemas_dir}/shipment_cancel_request.xsd"
134
+ )
135
+ templates.XML_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
136
+ f"{schemas_dir}/shipment_cancel_response.xsd"
137
+ )
138
+ else:
139
+ templates.JSON_SCHEMA_SHIPMENT_REQUEST_TEMPLATE.stream(**context).dump(
140
+ f"{schemas_dir}/shipment_request.json"
141
+ )
142
+ templates.JSON_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE.stream(**context).dump(
143
+ f"{schemas_dir}/shipment_response.json"
144
+ )
145
+ templates.JSON_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
146
+ f"{schemas_dir}/shipment_cancel_request.json"
147
+ )
148
+ templates.JSON_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
149
+ f"{schemas_dir}/shipment_cancel_response.json"
150
+ )
151
+
152
+ if "pickup" in features:
153
+ if is_xml_api:
154
+ templates.XML_SCHEMA_PICKUP_CREATE_REQUEST_TEMPLATE.stream(**context).dump(
155
+ f"{schemas_dir}/pickup_create_request.xsd"
156
+ )
157
+ templates.XML_SCHEMA_PICKUP_CREATE_RESPONSE_TEMPLATE.stream(**context).dump(
158
+ f"{schemas_dir}/pickup_create_response.xsd"
159
+ )
160
+ templates.XML_SCHEMA_PICKUP_UPDATE_REQUEST_TEMPLATE.stream(**context).dump(
161
+ f"{schemas_dir}/pickup_update_request.xsd"
162
+ )
163
+ templates.XML_SCHEMA_PICKUP_UPDATE_RESPONSE_TEMPLATE.stream(**context).dump(
164
+ f"{schemas_dir}/pickup_update_response.xsd"
165
+ )
166
+ templates.XML_SCHEMA_PICKUP_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
167
+ f"{schemas_dir}/pickup_cancel_request.xsd"
168
+ )
169
+ templates.XML_SCHEMA_PICKUP_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
170
+ f"{schemas_dir}/pickup_cancel_response.xsd"
171
+ )
172
+ else:
173
+ templates.JSON_SCHEMA_PICKUP_CREATE_REQUEST_TEMPLATE.stream(**context).dump(
174
+ f"{schemas_dir}/pickup_create_request.json"
175
+ )
176
+ templates.JSON_SCHEMA_PICKUP_CREATE_RESPONSE_TEMPLATE.stream(**context).dump(
177
+ f"{schemas_dir}/pickup_create_response.json"
178
+ )
179
+ templates.JSON_SCHEMA_PICKUP_UPDATE_REQUEST_TEMPLATE.stream(**context).dump(
180
+ f"{schemas_dir}/pickup_update_request.json"
181
+ )
182
+ templates.JSON_SCHEMA_PICKUP_UPDATE_RESPONSE_TEMPLATE.stream(**context).dump(
183
+ f"{schemas_dir}/pickup_update_response.json"
184
+ )
185
+ templates.JSON_SCHEMA_PICKUP_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
186
+ f"{schemas_dir}/pickup_cancel_request.json"
187
+ )
188
+ templates.JSON_SCHEMA_PICKUP_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
189
+ f"{schemas_dir}/pickup_cancel_response.json"
190
+ )
191
+
192
+ if "address" in features:
193
+ if is_xml_api:
194
+ templates.XML_SCHEMA_ADDRESS_VALIDATION_REQUEST_TEMPLATE.stream(**context).dump(
195
+ f"{schemas_dir}/address_validation_request.xsd"
196
+ )
197
+ templates.XML_SCHEMA_ADDRESS_VALIDATION_RESPONSE_TEMPLATE.stream(**context).dump(
198
+ f"{schemas_dir}/address_validation_response.xsd"
199
+ )
200
+ else:
201
+ templates.JSON_SCHEMA_ADDRESS_VALIDATION_REQUEST_TEMPLATE.stream(**context).dump(
202
+ f"{schemas_dir}/address_validation_request.json"
203
+ )
204
+ templates.JSON_SCHEMA_ADDRESS_VALIDATION_RESPONSE_TEMPLATE.stream(**context).dump(
205
+ f"{schemas_dir}/address_validation_response.json"
206
+ )
207
+
208
+ # tests files
209
+ templates.TEST_FIXTURE_TEMPLATE.stream(**context).dump(
210
+ f"{tests_dir}/fixture.py"
211
+ )
212
+ templates.TEST_PROVIDER_IMPORTS_TEMPLATE.stream(**context).dump(
213
+ f"{tests_dir}/__init__.py"
214
+ )
215
+ templates.TEST_IMPORTS_TEMPLATE.stream(**context).dump(
216
+ f"{root_dir}/tests/__init__.py"
217
+ )
218
+
219
+ # plugin files (new structure)
220
+ templates.PLUGIN_METADATA_TEMPLATE.stream(**context).dump(
221
+ f"{plugins_dir}/__init__.py"
222
+ )
223
+
224
+ # mappers files (legacy structure)
225
+ templates.MAPPER_TEMPLATE.stream(**context).dump(
226
+ f"{mappers_dir}/mapper.py"
227
+ )
228
+ templates.MAPPER_PROXY_TEMPLATE.stream(**context).dump(
229
+ f"{mappers_dir}/proxy.py"
230
+ )
231
+ templates.MAPPER_SETTINGS_TEMPLATE.stream(**context).dump(
232
+ f"{mappers_dir}/settings.py"
233
+ )
234
+ templates.MAPPER_IMPORTS_TEMPLATE.stream(**context).dump(
235
+ f"{mappers_dir}/__init__.py"
236
+ )
237
+
238
+ # providers files
239
+ templates.PROVIDER_ERROR_TEMPLATE.stream(**context).dump(
240
+ f"{providers_dir}/error.py"
241
+ )
242
+ templates.PROVIDER_UNITS_TEMPLATE.stream(**context).dump(
243
+ f"{providers_dir}/units.py"
244
+ )
245
+ templates.PROVIDER_UTILS_TEMPLATE.stream(**context).dump(
246
+ f"{providers_dir}/utils.py"
247
+ )
248
+ templates.PROVIDER_IMPORTS_TEMPLATE.stream(**context).dump(
249
+ f"{providers_dir}/__init__.py"
250
+ )
251
+
252
+ if "address" in features:
253
+ templates.TEST_ADDRESS_TEMPLATE.stream(**context).dump(
254
+ f"{tests_dir}/test_address.py"
255
+ )
256
+
257
+ templates.PROVIDER_ADDRESS_TEMPLATE.stream(**context).dump(
258
+ f"{providers_dir}/address.py"
259
+ )
260
+
261
+ if "rating" in features:
262
+ templates.TEST_RATE_TEMPLATE.stream(**context).dump(
263
+ f"{tests_dir}/test_rate.py"
264
+ )
265
+
266
+ templates.PROVIDER_RATE_TEMPLATE.stream(**context).dump(
267
+ f"{providers_dir}/rate.py"
268
+ )
269
+
270
+ if "tracking" in features:
271
+ templates.TEST_TRACKING_TEMPLATE.stream(**context).dump(
272
+ f"{tests_dir}/test_tracking.py"
273
+ )
274
+
275
+ templates.PROVIDER_TRACKING_TEMPLATE.stream(**context).dump(
276
+ f"{providers_dir}/tracking.py"
277
+ )
278
+
279
+ if "document" in features:
280
+ templates.TEST_DOCUMENT_UPLOAD_TEMPLATE.stream(**context).dump(
281
+ f"{tests_dir}/test_document.py"
282
+ )
283
+
284
+ templates.PROVIDER_DOCUMENT_UPLOAD_TEMPLATE.stream(**context).dump(
285
+ f"{providers_dir}/document.py"
286
+ )
287
+
288
+ if "manifest" in features:
289
+ templates.TEST_MANIFEST_TEMPLATE.stream(**context).dump(
290
+ f"{tests_dir}/test_manifest.py"
291
+ )
292
+
293
+ templates.PROVIDER_MANIFEST_TEMPLATE.stream(**context).dump(
294
+ f"{providers_dir}/manifest.py"
295
+ )
296
+
297
+ if "shipping" in features:
298
+ templates.TEST_SHIPMENT_TEMPLATE.stream(**context).dump(
299
+ f"{tests_dir}/test_shipment.py"
300
+ )
301
+
302
+ os.makedirs(f"{providers_dir}/shipment", exist_ok=True)
303
+ templates.PROVIDER_SHIPMENT_CANCEL_TEMPLATE.stream(**context).dump(
304
+ f"{providers_dir}/shipment/cancel.py"
305
+ )
306
+ templates.PROVIDER_SHIPMENT_CREATE_TEMPLATE.stream(**context).dump(
307
+ f"{providers_dir}/shipment/create.py"
308
+ )
309
+ templates.PROVIDER_SHIPMENT_IMPORTS_TEMPLATE.stream(**context).dump(
310
+ f"{providers_dir}/shipment/__init__.py"
311
+ )
312
+
313
+ if "pickup" in features:
314
+ templates.TEST_PICKUP_TEMPLATE.stream(**context).dump(
315
+ f"{tests_dir}/test_pickup.py"
316
+ )
317
+
318
+ os.makedirs(f"{providers_dir}/pickup", exist_ok=True)
319
+ templates.PROVIDER_PICKUP_CANCEL_TEMPLATE.stream(**context).dump(
320
+ f"{providers_dir}/pickup/cancel.py"
321
+ )
322
+ templates.PROVIDER_PICKUP_CREATE_TEMPLATE.stream(**context).dump(
323
+ f"{providers_dir}/pickup/create.py"
324
+ )
325
+ templates.PROVIDER_PICKUP_UPDATE_TEMPLATE.stream(**context).dump(
326
+ f"{providers_dir}/pickup/update.py"
327
+ )
328
+ templates.PROVIDER_PICKUP_IMPORTS_TEMPLATE.stream(**context).dump(
329
+ f"{providers_dir}/pickup/__init__.py"
330
+ )
331
+
332
+ if "manifest" in features:
333
+ if is_xml_api:
334
+ templates.XML_SCHEMA_MANIFEST_REQUEST_TEMPLATE.stream(**context).dump(
335
+ f"{schemas_dir}/manifest_request.xsd"
336
+ )
337
+ templates.XML_SCHEMA_MANIFEST_RESPONSE_TEMPLATE.stream(**context).dump(
338
+ f"{schemas_dir}/manifest_response.xsd"
339
+ )
340
+ else:
341
+ templates.JSON_SCHEMA_MANIFEST_REQUEST_TEMPLATE.stream(**context).dump(
342
+ f"{schemas_dir}/manifest_request.json"
343
+ )
344
+ templates.JSON_SCHEMA_MANIFEST_RESPONSE_TEMPLATE.stream(**context).dump(
345
+ f"{schemas_dir}/manifest_response.json"
346
+ )
347
+
348
+ if "document" in features:
349
+ if is_xml_api:
350
+ templates.XML_SCHEMA_DOCUMENT_UPLOAD_REQUEST_TEMPLATE.stream(**context).dump(
351
+ f"{schemas_dir}/document_upload_request.xsd"
352
+ )
353
+ templates.XML_SCHEMA_DOCUMENT_UPLOAD_RESPONSE_TEMPLATE.stream(**context).dump(
354
+ f"{schemas_dir}/document_upload_response.xsd"
355
+ )
356
+ else:
357
+ templates.JSON_SCHEMA_DOCUMENT_UPLOAD_REQUEST_TEMPLATE.stream(**context).dump(
358
+ f"{schemas_dir}/document_upload_request.json"
359
+ )
360
+ templates.JSON_SCHEMA_DOCUMENT_UPLOAD_RESPONSE_TEMPLATE.stream(**context).dump(
361
+ f"{schemas_dir}/document_upload_response.json"
362
+ )
363
+
364
+ if "address" in features:
365
+ templates.TEST_ADDRESS_TEMPLATE.stream(**context).dump(
366
+ f"{tests_dir}/test_address.py"
367
+ )
368
+
369
+ templates.PROVIDER_ADDRESS_TEMPLATE.stream(**context).dump(
370
+ f"{providers_dir}/address.py"
371
+ )
372
+
373
+ if "rating" in features:
374
+ templates.TEST_RATE_TEMPLATE.stream(**context).dump(
375
+ f"{tests_dir}/test_rate.py"
376
+ )
377
+
378
+ templates.PROVIDER_RATE_TEMPLATE.stream(**context).dump(
379
+ f"{providers_dir}/rate.py"
380
+ )
381
+
382
+ if "tracking" in features:
383
+ templates.TEST_TRACKING_TEMPLATE.stream(**context).dump(
384
+ f"{tests_dir}/test_tracking.py"
385
+ )
386
+
387
+ templates.PROVIDER_TRACKING_TEMPLATE.stream(**context).dump(
388
+ f"{providers_dir}/tracking.py"
389
+ )
390
+
391
+ if "document" in features:
392
+ templates.TEST_DOCUMENT_UPLOAD_TEMPLATE.stream(**context).dump(
393
+ f"{tests_dir}/test_document.py"
394
+ )
395
+
396
+ templates.PROVIDER_DOCUMENT_UPLOAD_TEMPLATE.stream(**context).dump(
397
+ f"{providers_dir}/document.py"
398
+ )
399
+
400
+ if "manifest" in features:
401
+ templates.TEST_MANIFEST_TEMPLATE.stream(**context).dump(
402
+ f"{tests_dir}/test_manifest.py"
403
+ )
404
+
405
+ templates.PROVIDER_MANIFEST_TEMPLATE.stream(**context).dump(
406
+ f"{providers_dir}/manifest.py"
407
+ )
408
+
409
+ if "shipping" in features:
410
+ templates.TEST_SHIPMENT_TEMPLATE.stream(**context).dump(
411
+ f"{tests_dir}/test_shipment.py"
412
+ )
413
+
414
+ os.makedirs(f"{providers_dir}/shipment", exist_ok=True)
415
+ templates.PROVIDER_SHIPMENT_CANCEL_TEMPLATE.stream(**context).dump(
416
+ f"{providers_dir}/shipment/cancel.py"
417
+ )
418
+ templates.PROVIDER_SHIPMENT_CREATE_TEMPLATE.stream(**context).dump(
419
+ f"{providers_dir}/shipment/create.py"
420
+ )
421
+ templates.PROVIDER_SHIPMENT_IMPORTS_TEMPLATE.stream(**context).dump(
422
+ f"{providers_dir}/shipment/__init__.py"
423
+ )
424
+
425
+ if "pickup" in features:
426
+ templates.TEST_PICKUP_TEMPLATE.stream(**context).dump(
427
+ f"{tests_dir}/test_pickup.py"
428
+ )
429
+
430
+ os.makedirs(f"{providers_dir}/pickup", exist_ok=True)
431
+ templates.PROVIDER_PICKUP_CANCEL_TEMPLATE.stream(**context).dump(
432
+ f"{providers_dir}/pickup/cancel.py"
433
+ )
434
+ templates.PROVIDER_PICKUP_CREATE_TEMPLATE.stream(**context).dump(
435
+ f"{providers_dir}/pickup/create.py"
436
+ )
437
+ templates.PROVIDER_PICKUP_UPDATE_TEMPLATE.stream(**context).dump(
438
+ f"{providers_dir}/pickup/update.py"
439
+ )
440
+ templates.PROVIDER_PICKUP_IMPORTS_TEMPLATE.stream(**context).dump(
441
+ f"{providers_dir}/pickup/__init__.py"
442
+ )
443
+
444
+ typer.echo("Done!")
445
+
446
+
447
+ def _add_features(
448
+ id: str,
449
+ name: str,
450
+ feature: str,
451
+ is_xml_api: bool,
452
+ path: str,
453
+ ):
454
+ # Resolve the provided path
455
+ base_dir = pathlib.Path(path).resolve()
456
+
457
+ # Create full path for confirmation
458
+ full_path = os.path.join(base_dir, id)
459
+
460
+ if not os.path.exists(full_path):
461
+ utils.karrio_log(f"Path {full_path} does not exist.")
462
+ utils.karrio_log(f"The extension {id} does not exist.", type="error")
463
+ return
464
+
465
+ features = [f.strip() for f in feature.split(",")]
466
+ context = dict(
467
+ id=id,
468
+ name=name,
469
+ features=features,
470
+ is_xml_api=is_xml_api,
471
+ compact_name=name.strip()
472
+ .replace("-", "")
473
+ .replace("_", "")
474
+ .replace("&", "")
475
+ .replace(" ", ""),
476
+ )
477
+
478
+ # Update the directory templates with the base directory
479
+ root_dir = os.path.join(base_dir, id)
480
+ schemas_dir = os.path.join(root_dir, "schemas")
481
+ tests_dir = os.path.join(root_dir, "tests", id)
482
+ plugins_dir = os.path.join(root_dir, "karrio", "plugins", id) # New plugin structure
483
+ mappers_dir = os.path.join(root_dir, "karrio", "mappers", id)
484
+ providers_dir = os.path.join(root_dir, "karrio", "providers", id)
485
+ schema_datatypes_dir = os.path.join(root_dir, "karrio", "schemas", id)
486
+
487
+ with Progress(
488
+ SpinnerColumn(),
489
+ TextColumn("[progress.description]{task.description}"),
490
+ transient=True,
491
+ ) as progress:
492
+ progress.add_task(description="Adding features...", total=None)
493
+ time.sleep(1)
494
+
495
+ # Generate schema files for selected features
496
+ if "rating" in features:
497
+ if is_xml_api:
498
+ utils.karrio_log("Adding rating schema files...")
499
+ templates.XML_SCHEMA_RATE_REQUEST_TEMPLATE.stream(**context).dump(
500
+ os.path.join(schemas_dir, "rate_request.xsd")
501
+ )
502
+ templates.XML_SCHEMA_RATE_RESPONSE_TEMPLATE.stream(**context).dump(
503
+ os.path.join(schemas_dir, "rate_response.xsd")
504
+ )
505
+ else:
506
+ utils.karrio_log("Adding rating schema files...")
507
+ templates.JSON_SCHEMA_RATE_REQUEST_TEMPLATE.stream(**context).dump(
508
+ os.path.join(schemas_dir, "rate_request.json")
509
+ )
510
+ templates.JSON_SCHEMA_RATE_RESPONSE_TEMPLATE.stream(**context).dump(
511
+ os.path.join(schemas_dir, "rate_response.json")
512
+ )
513
+
514
+ utils.karrio_log("Adding rating test file...")
515
+ templates.TEST_RATE_TEMPLATE.stream(**context).dump(
516
+ os.path.join(tests_dir, "test_rate.py")
517
+ )
518
+
519
+ utils.karrio_log("Adding rating provider files...")
520
+ templates.PROVIDER_RATE_TEMPLATE.stream(**context).dump(
521
+ os.path.join(providers_dir, "rate.py")
522
+ )
523
+
524
+ if "tracking" in features:
525
+ if is_xml_api:
526
+ utils.karrio_log("Adding tracking schema files...")
527
+ templates.XML_SCHEMA_TRACKING_REQUEST_TEMPLATE.stream(**context).dump(
528
+ os.path.join(schemas_dir, "tracking_request.xsd")
529
+ )
530
+ templates.XML_SCHEMA_TRACKING_RESPONSE_TEMPLATE.stream(**context).dump(
531
+ os.path.join(schemas_dir, "tracking_response.xsd")
532
+ )
533
+ else:
534
+ utils.karrio_log("Adding tracking schema files...")
535
+ templates.JSON_SCHEMA_TRACKING_REQUEST_TEMPLATE.stream(**context).dump(
536
+ os.path.join(schemas_dir, "tracking_request.json")
537
+ )
538
+ templates.JSON_SCHEMA_TRACKING_RESPONSE_TEMPLATE.stream(**context).dump(
539
+ os.path.join(schemas_dir, "tracking_response.json")
540
+ )
541
+
542
+ utils.karrio_log("Adding tracking test file...")
543
+ templates.TEST_TRACKING_TEMPLATE.stream(**context).dump(
544
+ os.path.join(tests_dir, "test_tracking.py")
545
+ )
546
+
547
+ utils.karrio_log("Adding tracking provider files...")
548
+ templates.PROVIDER_TRACKING_TEMPLATE.stream(**context).dump(
549
+ os.path.join(providers_dir, "tracking.py")
550
+ )
551
+
552
+ if "shipping" in features:
553
+ os.makedirs(os.path.join(providers_dir, "shipment"), exist_ok=True)
554
+ templates.EMPTY_FILE_TEMPLATE.stream(**context).dump(
555
+ os.path.join(providers_dir, "shipment", "__init__.py")
556
+ )
557
+
558
+ if is_xml_api:
559
+ utils.karrio_log("Adding shipment schema files...")
560
+ templates.XML_SCHEMA_SHIPMENT_REQUEST_TEMPLATE.stream(**context).dump(
561
+ os.path.join(schemas_dir, "shipment_request.xsd")
562
+ )
563
+ templates.XML_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE.stream(**context).dump(
564
+ os.path.join(schemas_dir, "shipment_response.xsd")
565
+ )
566
+ templates.XML_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
567
+ os.path.join(schemas_dir, "shipment_cancel_request.xsd")
568
+ )
569
+ templates.XML_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
570
+ os.path.join(schemas_dir, "shipment_cancel_response.xsd")
571
+ )
572
+ else:
573
+ utils.karrio_log("Adding shipment schema files...")
574
+ templates.JSON_SCHEMA_SHIPMENT_REQUEST_TEMPLATE.stream(**context).dump(
575
+ os.path.join(schemas_dir, "shipment_request.json")
576
+ )
577
+ templates.JSON_SCHEMA_SHIPMENT_RESPONSE_TEMPLATE.stream(**context).dump(
578
+ os.path.join(schemas_dir, "shipment_response.json")
579
+ )
580
+ templates.JSON_SCHEMA_SHIPMENT_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
581
+ os.path.join(schemas_dir, "shipment_cancel_request.json")
582
+ )
583
+ templates.JSON_SCHEMA_SHIPMENT_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
584
+ os.path.join(schemas_dir, "shipment_cancel_response.json")
585
+ )
586
+
587
+ utils.karrio_log("Adding shipment test file...")
588
+ templates.TEST_SHIPMENT_TEMPLATE.stream(**context).dump(
589
+ os.path.join(tests_dir, "test_shipment.py")
590
+ )
591
+
592
+ utils.karrio_log("Adding shipment provider files...")
593
+ templates.PROVIDER_SHIPMENT_TEMPLATE.stream(**context).dump(
594
+ os.path.join(providers_dir, "shipment", "create.py")
595
+ )
596
+ templates.PROVIDER_SHIPMENT_CANCEL_TEMPLATE.stream(**context).dump(
597
+ os.path.join(providers_dir, "shipment", "cancel.py")
598
+ )
599
+ templates.PROVIDER_SHIPMENT_INDEX_TEMPLATE.stream(**context).dump(
600
+ os.path.join(providers_dir, "shipment.py")
601
+ )
602
+
603
+ if "pickup" in features:
604
+ os.makedirs(os.path.join(providers_dir, "pickup"), exist_ok=True)
605
+ templates.EMPTY_FILE_TEMPLATE.stream(**context).dump(
606
+ os.path.join(providers_dir, "pickup", "__init__.py")
607
+ )
608
+
609
+ if is_xml_api:
610
+ utils.karrio_log("Adding pickup schema files...")
611
+ templates.XML_SCHEMA_PICKUP_CREATE_REQUEST_TEMPLATE.stream(**context).dump(
612
+ os.path.join(schemas_dir, "pickup_create_request.xsd")
613
+ )
614
+ templates.XML_SCHEMA_PICKUP_CREATE_RESPONSE_TEMPLATE.stream(**context).dump(
615
+ os.path.join(schemas_dir, "pickup_create_response.xsd")
616
+ )
617
+ templates.XML_SCHEMA_PICKUP_UPDATE_REQUEST_TEMPLATE.stream(**context).dump(
618
+ os.path.join(schemas_dir, "pickup_update_request.xsd")
619
+ )
620
+ templates.XML_SCHEMA_PICKUP_UPDATE_RESPONSE_TEMPLATE.stream(**context).dump(
621
+ os.path.join(schemas_dir, "pickup_update_response.xsd")
622
+ )
623
+ templates.XML_SCHEMA_PICKUP_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
624
+ os.path.join(schemas_dir, "pickup_cancel_request.xsd")
625
+ )
626
+ templates.XML_SCHEMA_PICKUP_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
627
+ os.path.join(schemas_dir, "pickup_cancel_response.xsd")
628
+ )
629
+ else:
630
+ utils.karrio_log("Adding pickup schema files...")
631
+ templates.JSON_SCHEMA_PICKUP_CREATE_REQUEST_TEMPLATE.stream(**context).dump(
632
+ os.path.join(schemas_dir, "pickup_create_request.json")
633
+ )
634
+ templates.JSON_SCHEMA_PICKUP_CREATE_RESPONSE_TEMPLATE.stream(**context).dump(
635
+ os.path.join(schemas_dir, "pickup_create_response.json")
636
+ )
637
+ templates.JSON_SCHEMA_PICKUP_UPDATE_REQUEST_TEMPLATE.stream(**context).dump(
638
+ os.path.join(schemas_dir, "pickup_update_request.json")
639
+ )
640
+ templates.JSON_SCHEMA_PICKUP_UPDATE_RESPONSE_TEMPLATE.stream(**context).dump(
641
+ os.path.join(schemas_dir, "pickup_update_response.json")
642
+ )
643
+ templates.JSON_SCHEMA_PICKUP_CANCEL_REQUEST_TEMPLATE.stream(**context).dump(
644
+ os.path.join(schemas_dir, "pickup_cancel_request.json")
645
+ )
646
+ templates.JSON_SCHEMA_PICKUP_CANCEL_RESPONSE_TEMPLATE.stream(**context).dump(
647
+ os.path.join(schemas_dir, "pickup_cancel_response.json")
648
+ )
649
+
650
+ utils.karrio_log("Adding pickup test file...")
651
+ templates.TEST_PICKUP_TEMPLATE.stream(**context).dump(
652
+ os.path.join(tests_dir, "test_pickup.py")
653
+ )
654
+
655
+ utils.karrio_log("Adding pickup provider files...")
656
+ templates.PROVIDER_PICKUP_TEMPLATE.stream(**context).dump(
657
+ os.path.join(providers_dir, "pickup", "create.py")
658
+ )
659
+ templates.PROVIDER_PICKUP_UPDATE_TEMPLATE.stream(**context).dump(
660
+ os.path.join(providers_dir, "pickup", "update.py")
661
+ )
662
+ templates.PROVIDER_PICKUP_CANCEL_TEMPLATE.stream(**context).dump(
663
+ os.path.join(providers_dir, "pickup", "cancel.py")
664
+ )
665
+ templates.PROVIDER_PICKUP_INDEX_TEMPLATE.stream(**context).dump(
666
+ os.path.join(providers_dir, "pickup.py")
667
+ )
668
+
669
+ if "address" in features:
670
+ if is_xml_api:
671
+ utils.karrio_log("Adding address schema files...")
672
+ templates.XML_SCHEMA_ADDRESS_VALIDATION_REQUEST_TEMPLATE.stream(**context).dump(
673
+ os.path.join(schemas_dir, "address_validation_request.xsd")
674
+ )
675
+ templates.XML_SCHEMA_ADDRESS_VALIDATION_RESPONSE_TEMPLATE.stream(**context).dump(
676
+ os.path.join(schemas_dir, "address_validation_response.xsd")
677
+ )
678
+ else:
679
+ utils.karrio_log("Adding address schema files...")
680
+ templates.JSON_SCHEMA_ADDRESS_VALIDATION_REQUEST_TEMPLATE.stream(**context).dump(
681
+ os.path.join(schemas_dir, "address_validation_request.json")
682
+ )
683
+ templates.JSON_SCHEMA_ADDRESS_VALIDATION_RESPONSE_TEMPLATE.stream(**context).dump(
684
+ os.path.join(schemas_dir, "address_validation_response.json")
685
+ )
686
+
687
+ utils.karrio_log("Adding address test file...")
688
+ templates.TEST_ADDRESS_TEMPLATE.stream(**context).dump(
689
+ os.path.join(tests_dir, "test_address.py")
690
+ )
691
+
692
+ utils.karrio_log("Adding address provider files...")
693
+ templates.PROVIDER_ADDRESS_TEMPLATE.stream(**context).dump(
694
+ os.path.join(providers_dir, "address.py")
695
+ )
696
+
697
+ # If this is a validator plugin, create validators directory
698
+ validators_dir = os.path.join(root_dir, "karrio", "validators", id)
699
+ os.makedirs(validators_dir, exist_ok=True)
700
+ templates.VALIDATOR_TEMPLATE.stream(**context).dump(
701
+ os.path.join(validators_dir, "validator.py")
702
+ )
703
+ templates.VALIDATOR_IMPORTS_TEMPLATE.stream(**context).dump(
704
+ os.path.join(validators_dir, "__init__.py")
705
+ )
706
+
707
+ if "document" in features:
708
+ if is_xml_api:
709
+ utils.karrio_log("Adding document schema files...")
710
+ templates.XML_SCHEMA_DOCUMENT_UPLOAD_REQUEST_TEMPLATE.stream(**context).dump(
711
+ os.path.join(schemas_dir, "document_upload_request.xsd")
712
+ )
713
+ templates.XML_SCHEMA_DOCUMENT_UPLOAD_RESPONSE_TEMPLATE.stream(**context).dump(
714
+ os.path.join(schemas_dir, "document_upload_response.xsd")
715
+ )
716
+ else:
717
+ utils.karrio_log("Adding document schema files...")
718
+ templates.JSON_SCHEMA_DOCUMENT_UPLOAD_REQUEST_TEMPLATE.stream(**context).dump(
719
+ os.path.join(schemas_dir, "document_upload_request.json")
720
+ )
721
+ templates.JSON_SCHEMA_DOCUMENT_UPLOAD_RESPONSE_TEMPLATE.stream(**context).dump(
722
+ os.path.join(schemas_dir, "document_upload_response.json")
723
+ )
724
+
725
+ utils.karrio_log("Adding document test file...")
726
+ templates.TEST_DOCUMENT_TEMPLATE.stream(**context).dump(
727
+ os.path.join(tests_dir, "test_document.py")
728
+ )
729
+
730
+ utils.karrio_log("Adding document provider files...")
731
+ templates.PROVIDER_DOCUMENT_TEMPLATE.stream(**context).dump(
732
+ os.path.join(providers_dir, "document.py")
733
+ )
734
+
735
+ if "manifest" in features:
736
+ if is_xml_api:
737
+ utils.karrio_log("Adding manifest schema files...")
738
+ templates.XML_SCHEMA_MANIFEST_REQUEST_TEMPLATE.stream(**context).dump(
739
+ os.path.join(schemas_dir, "manifest_request.xsd")
740
+ )
741
+ templates.XML_SCHEMA_MANIFEST_RESPONSE_TEMPLATE.stream(**context).dump(
742
+ os.path.join(schemas_dir, "manifest_response.xsd")
743
+ )
744
+ else:
745
+ utils.karrio_log("Adding manifest schema files...")
746
+ templates.JSON_SCHEMA_MANIFEST_REQUEST_TEMPLATE.stream(**context).dump(
747
+ os.path.join(schemas_dir, "manifest_request.json")
748
+ )
749
+ templates.JSON_SCHEMA_MANIFEST_RESPONSE_TEMPLATE.stream(**context).dump(
750
+ os.path.join(schemas_dir, "manifest_response.json")
751
+ )
752
+
753
+ utils.karrio_log("Adding manifest test file...")
754
+ templates.TEST_MANIFEST_TEMPLATE.stream(**context).dump(
755
+ os.path.join(tests_dir, "test_manifest.py")
756
+ )
757
+
758
+ utils.karrio_log("Adding manifest provider files...")
759
+ templates.PROVIDER_MANIFEST_TEMPLATE.stream(**context).dump(
760
+ os.path.join(providers_dir, "manifest.py")
761
+ )
762
+
763
+ # Create plugins directory if it doesn't exist (for new structure)
764
+ if not os.path.exists(plugins_dir):
765
+ os.makedirs(plugins_dir, exist_ok=True)
766
+
767
+ # If we're adding plugins directory to an existing project, create the __init__.py with METADATA
768
+ if os.path.exists(mappers_dir):
769
+ # Check if there's existing METADATA in mappers/__init__.py
770
+ mappers_init_path = os.path.join(mappers_dir, "__init__.py")
771
+ if os.path.exists(mappers_init_path):
772
+ utils.karrio_log("Creating plugin METADATA file...")
773
+ templates.PLUGIN_METADATA_TEMPLATE.stream(**context).dump(
774
+ os.path.join(plugins_dir, "__init__.py")
775
+ )
776
+
777
+ # If the mapper has METADATA, convert it to just imports
778
+ with open(mappers_init_path, "r") as f:
779
+ content = f.read()
780
+
781
+ if "METADATA" in content:
782
+ utils.karrio_log("Updating mapper __init__.py to use imports only...")
783
+ templates.MAPPER_IMPORTS_TEMPLATE.stream(**context).dump(
784
+ mappers_init_path
785
+ )
786
+
787
+
788
+ app = typer.Typer()
789
+
790
+
791
+ @app.command()
792
+ def add_extension(
793
+ path: str = typer.Option(..., "--path", "-p", help="Path where the extension will be created"),
794
+ carrier_slug: str = typer.Option(
795
+ ...,
796
+ prompt=True,
797
+ help="The unique identifier for the carrier (e.g., dhl_express, ups, fedex, canadapost)"
798
+ ),
799
+ display_name: str = typer.Option(
800
+ ...,
801
+ prompt=True,
802
+ help="The human-readable name for the carrier (e.g., DHL, UPS, FedEx, Canada Post)"
803
+ ),
804
+ features: typing.Optional[str] = typer.Option(
805
+ ", ".join(utils.DEFAULT_FEATURES), prompt=True
806
+ ),
807
+ version: typing.Optional[str] = typer.Option(
808
+ f"{datetime.datetime.now().strftime('%Y.%-m')}", prompt=True
809
+ ),
810
+ is_xml_api: typing.Optional[bool] = typer.Option(False, prompt="Is XML API?"),
811
+ ):
812
+ # Resolve the provided path for confirmation
813
+ full_path = pathlib.Path(path).resolve() / carrier_slug.lower()
814
+
815
+ confirmation_text = (
816
+ f"\nGenerate new carrier extension with:\n"
817
+ f" Name: {display_name}\n"
818
+ f" Alias: {carrier_slug}\n"
819
+ f" Features: [{features}]\n"
820
+ f" Path: {full_path}\n"
821
+ )
822
+
823
+ typer.echo(confirmation_text)
824
+ typer.confirm("Do you want to proceed?", abort=True)
825
+
826
+ _add_extension(
827
+ carrier_slug.lower(),
828
+ display_name,
829
+ features,
830
+ version,
831
+ is_xml_api,
832
+ path,
833
+ )
834
+
835
+
836
+ @app.command()
837
+ def add_features(
838
+ carrier_slug: str = typer.Option(
839
+ ...,
840
+ prompt=True,
841
+ help="The unique identifier for the carrier (e.g., dhl_express, ups, fedex, canadapost)"
842
+ ),
843
+ display_name: str = typer.Option(
844
+ ...,
845
+ prompt=True,
846
+ help="The human-readable name for the carrier (e.g., DHL, UPS, FedEx, Canada Post)"
847
+ ),
848
+ features: typing.Optional[str] = typer.Option(
849
+ ", ".join(utils.DEFAULT_FEATURES), prompt=True
850
+ ),
851
+ is_xml_api: typing.Optional[bool] = typer.Option(False, prompt="Is XML API?"),
852
+ path: str = typer.Option(..., "--path", "-p", help="Path where the features will be created"),
853
+ ):
854
+ # Resolve the provided path for confirmation
855
+ full_path = pathlib.Path(path).resolve() / carrier_slug.lower()
856
+
857
+ confirmation_text = (
858
+ f"\nBootstrap features for carrier extension with:\n"
859
+ f" Name: {display_name}\n"
860
+ f" ID: {carrier_slug}\n"
861
+ f" Features: [{features.replace(',', ', ')}]\n"
862
+ f" Path: {full_path}\n"
863
+ )
864
+
865
+ typer.echo(confirmation_text)
866
+ typer.confirm("Do you want to proceed?", abort=True)
867
+
868
+ features = features.split(",")
869
+ features = [feature.strip() for feature in features]
870
+ _add_features(carrier_slug, display_name, ",".join(features), is_xml_api, path)