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,943 @@
1
+ """
2
+ Enhanced tools for handling different input formats for carrier integration generation.
3
+
4
+ This module provides tools to process:
5
+ - OpenAPI/Swagger specifications
6
+ - Website URLs for scraping API documentation
7
+ - PDF documentation files
8
+ - Raw text documentation
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import yaml
14
+ import typing
15
+ import requests
16
+ from pathlib import Path
17
+ from urllib.parse import urljoin, urlparse
18
+ import tempfile
19
+ import re
20
+
21
+ try:
22
+ import PyPDF2
23
+ import pdfplumber
24
+ PDF_AVAILABLE = True
25
+ except ImportError:
26
+ PDF_AVAILABLE = False
27
+
28
+ try:
29
+ from bs4 import BeautifulSoup
30
+ WEB_SCRAPING_AVAILABLE = True
31
+ except ImportError:
32
+ WEB_SCRAPING_AVAILABLE = False
33
+
34
+ try:
35
+ import openapi_schema_validator
36
+ OPENAPI_VALIDATION_AVAILABLE = True
37
+ except ImportError:
38
+ OPENAPI_VALIDATION_AVAILABLE = False
39
+
40
+
41
+ def parse_openapi_spec(
42
+ spec_content: str,
43
+ spec_format: str = "auto"
44
+ ) -> typing.Dict[str, typing.Any]:
45
+ """
46
+ Parse OpenAPI/Swagger specification and extract carrier API information.
47
+
48
+ Args:
49
+ spec_content: OpenAPI specification content (JSON or YAML)
50
+ spec_format: Format of the spec ('json', 'yaml', 'auto')
51
+
52
+ Returns:
53
+ Dictionary containing structured API information
54
+ """
55
+ result = {
56
+ "success": False,
57
+ "api_info": {},
58
+ "endpoints": {},
59
+ "schemas": {},
60
+ "auth_methods": [],
61
+ "error": None
62
+ }
63
+
64
+ try:
65
+ # Parse the specification
66
+ if spec_format == "auto":
67
+ # Try to detect format
68
+ try:
69
+ spec_data = json.loads(spec_content)
70
+ spec_format = "json"
71
+ except json.JSONDecodeError:
72
+ try:
73
+ spec_data = yaml.safe_load(spec_content)
74
+ spec_format = "yaml"
75
+ except yaml.YAMLError:
76
+ result["error"] = "Unable to parse specification as JSON or YAML"
77
+ return result
78
+ elif spec_format == "json":
79
+ spec_data = json.loads(spec_content)
80
+ elif spec_format == "yaml":
81
+ spec_data = yaml.safe_load(spec_content)
82
+ else:
83
+ result["error"] = f"Unsupported format: {spec_format}"
84
+ return result
85
+
86
+ # Extract API information
87
+ result["api_info"] = {
88
+ "title": spec_data.get("info", {}).get("title", "Unknown API"),
89
+ "version": spec_data.get("info", {}).get("version", "1.0.0"),
90
+ "description": spec_data.get("info", {}).get("description", ""),
91
+ "base_url": spec_data.get("servers", [{}])[0].get("url", "") if spec_data.get("servers") else "",
92
+ "openapi_version": spec_data.get("openapi", spec_data.get("swagger", "unknown"))
93
+ }
94
+
95
+ # Extract endpoints
96
+ paths = spec_data.get("paths", {})
97
+ for path, methods in paths.items():
98
+ for method, operation in methods.items():
99
+ if isinstance(operation, dict):
100
+ endpoint_key = f"{method.upper()} {path}"
101
+ result["endpoints"][endpoint_key] = {
102
+ "method": method.upper(),
103
+ "path": path,
104
+ "summary": operation.get("summary", ""),
105
+ "description": operation.get("description", ""),
106
+ "tags": operation.get("tags", []),
107
+ "parameters": operation.get("parameters", []),
108
+ "request_body": operation.get("requestBody", {}),
109
+ "responses": operation.get("responses", {}),
110
+ "operation_id": operation.get("operationId", "")
111
+ }
112
+
113
+ # Extract schemas
114
+ components = spec_data.get("components", {})
115
+ definitions = spec_data.get("definitions", {}) # Swagger 2.0
116
+
117
+ if components:
118
+ result["schemas"] = components.get("schemas", {})
119
+ if definitions:
120
+ result["schemas"].update(definitions)
121
+
122
+ # Extract authentication methods
123
+ security_schemes = components.get("securitySchemes", {}) or spec_data.get("securityDefinitions", {})
124
+ for name, scheme in security_schemes.items():
125
+ auth_type = scheme.get("type", "unknown")
126
+ result["auth_methods"].append({
127
+ "name": name,
128
+ "type": auth_type,
129
+ "scheme": scheme.get("scheme", ""),
130
+ "bearer_format": scheme.get("bearerFormat", ""),
131
+ "in": scheme.get("in", ""),
132
+ "description": scheme.get("description", "")
133
+ })
134
+
135
+ # Categorize endpoints by shipping operations
136
+ result["shipping_endpoints"] = _categorize_shipping_endpoints(result["endpoints"])
137
+
138
+ result["success"] = True
139
+
140
+ except Exception as e:
141
+ result["error"] = str(e)
142
+
143
+ return result
144
+
145
+
146
+ def scrape_api_documentation(
147
+ url: str,
148
+ documentation_type: str = "auto"
149
+ ) -> typing.Dict[str, typing.Any]:
150
+ """
151
+ Scrape API documentation from a URL.
152
+
153
+ Args:
154
+ url: The URL to scrape (e.g., API documentation page)
155
+ documentation_type: Type expected (auto, openapi, website, rest_api)
156
+
157
+ Returns:
158
+ Dictionary with scraped content and analysis
159
+ """
160
+ import requests
161
+ from bs4 import BeautifulSoup
162
+ import re
163
+ import json
164
+
165
+ result = {
166
+ "url": url,
167
+ "success": False,
168
+ "content": "",
169
+ "documentation_type": documentation_type,
170
+ "api_info": {},
171
+ "error": None
172
+ }
173
+
174
+ try:
175
+ # Check if user has beautifulsoup4 installed
176
+ headers = {
177
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
178
+ }
179
+
180
+ # Fetch the page
181
+ response = requests.get(url, headers=headers, timeout=30)
182
+ response.raise_for_status()
183
+
184
+ # Parse the content
185
+ soup = BeautifulSoup(response.content, 'html.parser')
186
+
187
+ # Extract text content
188
+ text_content = soup.get_text()
189
+ result["content"] = text_content[:10000] # Limit to avoid overwhelming the LLM
190
+
191
+ # Look for OpenAPI/Swagger specs
192
+ openapi_indicators = [
193
+ 'swagger', 'openapi', 'api-docs', 'specification',
194
+ 'endpoints', 'paths:', 'components:', 'schemas:'
195
+ ]
196
+
197
+ if any(indicator in text_content.lower() for indicator in openapi_indicators):
198
+ result["documentation_type"] = "openapi"
199
+
200
+ # Look for specific patterns
201
+ api_patterns = {
202
+ "base_url": r"https?://[a-zA-Z0-9.-]+(?:/[a-zA-Z0-9.-]*)*",
203
+ "endpoints": r"/[a-zA-Z0-9/-]+(?:\{[a-zA-Z0-9_]+\})?",
204
+ "authentication": r"(?:api.?key|token|authorization|bearer|oauth)",
205
+ "methods": r"\b(?:GET|POST|PUT|DELETE|PATCH)\b",
206
+ }
207
+
208
+ for pattern_name, pattern in api_patterns.items():
209
+ matches = re.findall(pattern, text_content, re.IGNORECASE)
210
+ if matches:
211
+ result["api_info"][pattern_name] = matches[:10] # Limit results
212
+
213
+ # Try to find JSON content (might be embedded OpenAPI spec)
214
+ json_blocks = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', response.text)
215
+ for block in json_blocks:
216
+ try:
217
+ parsed = json.loads(block)
218
+ if any(key in parsed for key in ['openapi', 'swagger', 'paths', 'info']):
219
+ result["api_info"]["openapi_spec"] = parsed
220
+ result["documentation_type"] = "openapi"
221
+ break
222
+ except:
223
+ continue
224
+
225
+ result["success"] = True
226
+
227
+ # Provide analysis
228
+ result["analysis"] = analyze_carrier_api_documentation(
229
+ text_content[:5000],
230
+ url.split('/')[2] if '://' in url else url, # Extract domain as carrier name
231
+ result["documentation_type"]
232
+ )
233
+
234
+ except ImportError:
235
+ result["error"] = "beautifulsoup4 not installed. Please run: pip install beautifulsoup4 requests"
236
+ except requests.RequestException as e:
237
+ result["error"] = f"Failed to fetch URL: {str(e)}"
238
+ except Exception as e:
239
+ result["error"] = f"Error scraping documentation: {str(e)}"
240
+
241
+ return result
242
+
243
+
244
+ def parse_pdf_documentation(
245
+ pdf_path: str
246
+ ) -> typing.Dict[str, typing.Any]:
247
+ """
248
+ Extract API documentation from PDF files.
249
+
250
+ Args:
251
+ pdf_path: Path to the PDF file
252
+
253
+ Returns:
254
+ Dictionary containing extracted documentation
255
+ """
256
+ result = {
257
+ "success": False,
258
+ "file_path": pdf_path,
259
+ "pages": 0,
260
+ "content": "",
261
+ "api_endpoints": [],
262
+ "code_examples": [],
263
+ "tables": [],
264
+ "error": None
265
+ }
266
+
267
+ if not PDF_AVAILABLE:
268
+ result["error"] = "PDF parsing dependencies not installed. Install with: pip install PyPDF2 pdfplumber"
269
+ return result
270
+
271
+ try:
272
+ pdf_path = Path(pdf_path)
273
+ if not pdf_path.exists():
274
+ result["error"] = f"PDF file not found: {pdf_path}"
275
+ return result
276
+
277
+ # Extract text using pdfplumber (better for structured content)
278
+ with pdfplumber.open(pdf_path) as pdf:
279
+ result["pages"] = len(pdf.pages)
280
+
281
+ all_text = []
282
+ tables = []
283
+
284
+ for page_num, page in enumerate(pdf.pages):
285
+ # Extract text
286
+ page_text = page.extract_text()
287
+ if page_text:
288
+ all_text.append(f"=== Page {page_num + 1} ===\n{page_text}")
289
+
290
+ # Extract tables
291
+ page_tables = page.extract_tables()
292
+ for table in page_tables:
293
+ if table:
294
+ tables.append({
295
+ "page": page_num + 1,
296
+ "data": table[:10] # Limit rows
297
+ })
298
+
299
+ result["content"] = "\n\n".join(all_text)
300
+ result["tables"] = tables
301
+
302
+ # Extract API endpoints and code examples
303
+ result["api_endpoints"] = _extract_api_patterns(result["content"])
304
+ result["code_examples"] = _extract_code_examples(result["content"])
305
+
306
+ result["success"] = True
307
+
308
+ except Exception as e:
309
+ result["error"] = str(e)
310
+
311
+ return result
312
+
313
+
314
+ def analyze_carrier_documentation(
315
+ carrier_name: str,
316
+ documentation_source: str,
317
+ source_type: str = "auto"
318
+ ) -> typing.Dict[str, typing.Any]:
319
+ """
320
+ Analyze carrier documentation from various sources.
321
+
322
+ Args:
323
+ carrier_name: Name of the carrier
324
+ documentation_source: Source content (OpenAPI spec, URL, file path, or text)
325
+ source_type: Type of source ('openapi', 'url', 'pdf', 'text', 'auto')
326
+
327
+ Returns:
328
+ Comprehensive analysis of carrier integration requirements
329
+ """
330
+ result = {
331
+ "carrier_name": carrier_name,
332
+ "source_type": source_type,
333
+ "success": True,
334
+ "error": None
335
+ }
336
+
337
+ try:
338
+ # Basic implementation - can be enhanced with actual parsing
339
+ if source_type == "auto":
340
+ source_type = _detect_source_type(documentation_source)
341
+
342
+ result["source_type"] = source_type
343
+ result["analysis"] = {
344
+ "api_type": "REST",
345
+ "endpoints": _extract_endpoints(documentation_source),
346
+ "auth_methods": _extract_auth_methods(documentation_source)
347
+ }
348
+
349
+ except Exception as e:
350
+ result["error"] = str(e)
351
+ result["success"] = False
352
+
353
+ return result
354
+
355
+
356
+ # Helper functions
357
+ def _categorize_shipping_endpoints(endpoints: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.List[str]]:
358
+ """Categorize endpoints by shipping operations."""
359
+ categories = {
360
+ "rates": [],
361
+ "shipments": [],
362
+ "tracking": [],
363
+ "labels": [],
364
+ "manifests": [],
365
+ "pickup": [],
366
+ "other": []
367
+ }
368
+
369
+ for endpoint_key, endpoint_info in endpoints.items():
370
+ path = endpoint_info["path"].lower()
371
+ summary = endpoint_info["summary"].lower()
372
+ tags = [tag.lower() for tag in endpoint_info["tags"]]
373
+
374
+ categorized = False
375
+ for category in categories.keys():
376
+ if category in path or category in summary or any(category in tag for tag in tags):
377
+ categories[category].append(endpoint_key)
378
+ categorized = True
379
+ break
380
+
381
+ if not categorized:
382
+ # Additional categorization logic
383
+ if any(keyword in path or keyword in summary for keyword in ["quote", "price", "cost"]):
384
+ categories["rates"].append(endpoint_key)
385
+ elif any(keyword in path or keyword in summary for keyword in ["ship", "create", "book"]):
386
+ categories["shipments"].append(endpoint_key)
387
+ elif any(keyword in path or keyword in summary for keyword in ["track", "status", "trace"]):
388
+ categories["tracking"].append(endpoint_key)
389
+ elif any(keyword in path or keyword in summary for keyword in ["label", "print"]):
390
+ categories["labels"].append(endpoint_key)
391
+ else:
392
+ categories["other"].append(endpoint_key)
393
+
394
+ return categories
395
+
396
+
397
+ def _extract_api_patterns(text: str) -> typing.List[str]:
398
+ """Extract API endpoint patterns from text."""
399
+ endpoints = []
400
+ for pattern in [
401
+ r'https?://[^\s/]+/[^\s]*',
402
+ r'/v\d+/[^\s]*',
403
+ r'/api/[^\s]*'
404
+ ]:
405
+ matches = re.findall(pattern, text, re.IGNORECASE)
406
+ endpoints.extend(matches)
407
+ return list(set(endpoints))
408
+
409
+
410
+ def _extract_code_examples(text: str) -> typing.List[str]:
411
+ """Extract code examples from text."""
412
+ examples = []
413
+ for pattern in [
414
+ r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}',
415
+ r'<[^<>]+>.*?</[^<>]+>',
416
+ r'```[\s\S]*?```',
417
+ ]:
418
+ matches = re.findall(pattern, text, re.DOTALL)
419
+ for match in matches:
420
+ if len(match) > 50 and len(match) < 1000:
421
+ examples.append(match.strip())
422
+ return examples[:10]
423
+
424
+
425
+ def _detect_source_type(source: str) -> str:
426
+ """Detect the type of documentation source."""
427
+ if source.startswith(('http://', 'https://')):
428
+ return "url"
429
+ elif source.endswith('.pdf'):
430
+ return "pdf"
431
+ elif 'openapi' in source.lower() or 'swagger' in source.lower():
432
+ return "openapi"
433
+ else:
434
+ return "text"
435
+
436
+
437
+ def _extract_endpoints(content: str) -> typing.List[str]:
438
+ """Extract API endpoints from content."""
439
+ endpoints = []
440
+ for pattern in [
441
+ r'https?://[^\s/]+/[^\s]*',
442
+ r'/v\d+/[^\s]*',
443
+ r'/api/[^\s]*'
444
+ ]:
445
+ matches = re.findall(pattern, content, re.IGNORECASE)
446
+ endpoints.extend(matches)
447
+ return list(set(endpoints))
448
+
449
+
450
+ def _extract_auth_methods(content: str) -> typing.List[str]:
451
+ """Extract authentication methods from content."""
452
+ auth_methods = []
453
+ content_lower = content.lower()
454
+
455
+ if 'api key' in content_lower or 'apikey' in content_lower:
456
+ auth_methods.append('apiKey')
457
+ if 'oauth' in content_lower:
458
+ auth_methods.append('oauth2')
459
+ if 'bearer' in content_lower:
460
+ auth_methods.append('bearer')
461
+
462
+ return auth_methods
463
+
464
+
465
+ def create_karrio_plugin_structure(
466
+ carrier_slug: str,
467
+ carrier_name: str,
468
+ features: str = "rating,shipping,tracking",
469
+ is_xml_api: bool = False,
470
+ output_path: str = "./plugins"
471
+ ) -> typing.Dict[str, typing.Any]:
472
+ """
473
+ Generate a complete Karrio plugin structure using the official kcli SDK tools.
474
+
475
+ Args:
476
+ carrier_slug: Unique identifier (e.g., 'chit_chats', 'my_carrier')
477
+ carrier_name: Display name (e.g., 'Chit Chats', 'My Carrier')
478
+ features: Comma-separated features (rating,shipping,tracking,pickup,address)
479
+ is_xml_api: Whether the carrier uses XML API (vs JSON)
480
+ output_path: Directory where the plugin will be created
481
+
482
+ Returns:
483
+ Dictionary with plugin structure information and next steps
484
+ """
485
+ import subprocess
486
+ import os
487
+ from pathlib import Path
488
+
489
+ result = {
490
+ "carrier_slug": carrier_slug,
491
+ "carrier_name": carrier_name,
492
+ "features": features,
493
+ "is_xml_api": is_xml_api,
494
+ "output_path": output_path,
495
+ "success": False,
496
+ "plugin_directory": None,
497
+ "generated_files": [],
498
+ "next_steps": [],
499
+ "error": None
500
+ }
501
+
502
+ try:
503
+ # Ensure output directory exists
504
+ output_dir = Path(output_path)
505
+ output_dir.mkdir(parents=True, exist_ok=True)
506
+
507
+ # Build the kcli command to generate the plugin
508
+ cmd = [
509
+ "kcli", "sdk", "add-extension",
510
+ "--path", str(output_dir),
511
+ # Use environment variables to avoid interactive prompts
512
+ ]
513
+
514
+ # Set environment variables for non-interactive mode
515
+ env = os.environ.copy()
516
+ env.update({
517
+ "KARRIO_CARRIER_SLUG": carrier_slug,
518
+ "KARRIO_CARRIER_NAME": carrier_name,
519
+ "KARRIO_FEATURES": features,
520
+ "KARRIO_IS_XML": "true" if is_xml_api else "false",
521
+ "KARRIO_VERSION": "2025.1"
522
+ })
523
+
524
+ # For now, provide manual instructions since kcli requires interactive input
525
+ result.update({
526
+ "success": True,
527
+ "plugin_directory": str(output_dir / carrier_slug),
528
+ "manual_command": f"kcli sdk add-extension --path {output_dir}",
529
+ "command_inputs": {
530
+ "carrier_slug": carrier_slug,
531
+ "display_name": carrier_name,
532
+ "features": features,
533
+ "version": "2025.1",
534
+ "is_xml_api": is_xml_api
535
+ },
536
+ "generated_structure": {
537
+ "directories": [
538
+ f"{carrier_slug}/",
539
+ f"{carrier_slug}/schemas/",
540
+ f"{carrier_slug}/tests/{carrier_slug}/",
541
+ f"{carrier_slug}/karrio/plugins/{carrier_slug}/",
542
+ f"{carrier_slug}/karrio/mappers/{carrier_slug}/",
543
+ f"{carrier_slug}/karrio/providers/{carrier_slug}/",
544
+ f"{carrier_slug}/karrio/schemas/{carrier_slug}/",
545
+ ],
546
+ "files": [
547
+ f"{carrier_slug}/pyproject.toml",
548
+ f"{carrier_slug}/README.md",
549
+ f"{carrier_slug}/generate",
550
+ f"{carrier_slug}/schemas/error_response.{'xsd' if is_xml_api else 'json'}",
551
+ ]
552
+ }
553
+ })
554
+
555
+ # Add feature-specific files
556
+ features_list = [f.strip() for f in features.split(",")]
557
+ feature_files = []
558
+
559
+ if "rating" in features_list:
560
+ ext = "xsd" if is_xml_api else "json"
561
+ feature_files.extend([
562
+ f"{carrier_slug}/schemas/rate_request.{ext}",
563
+ f"{carrier_slug}/schemas/rate_response.{ext}",
564
+ ])
565
+
566
+ if "shipping" in features_list:
567
+ ext = "xsd" if is_xml_api else "json"
568
+ feature_files.extend([
569
+ f"{carrier_slug}/schemas/shipment_request.{ext}",
570
+ f"{carrier_slug}/schemas/shipment_response.{ext}",
571
+ f"{carrier_slug}/schemas/shipment_cancel_request.{ext}",
572
+ f"{carrier_slug}/schemas/shipment_cancel_response.{ext}",
573
+ ])
574
+
575
+ if "tracking" in features_list:
576
+ ext = "xsd" if is_xml_api else "json"
577
+ feature_files.extend([
578
+ f"{carrier_slug}/schemas/tracking_request.{ext}",
579
+ f"{carrier_slug}/schemas/tracking_response.{ext}",
580
+ ])
581
+
582
+ if "pickup" in features_list:
583
+ ext = "xsd" if is_xml_api else "json"
584
+ feature_files.extend([
585
+ f"{carrier_slug}/schemas/pickup_create_request.{ext}",
586
+ f"{carrier_slug}/schemas/pickup_create_response.{ext}",
587
+ f"{carrier_slug}/schemas/pickup_update_request.{ext}",
588
+ f"{carrier_slug}/schemas/pickup_update_response.{ext}",
589
+ f"{carrier_slug}/schemas/pickup_cancel_request.{ext}",
590
+ f"{carrier_slug}/schemas/pickup_cancel_response.{ext}",
591
+ ])
592
+
593
+ if "address" in features_list:
594
+ ext = "xsd" if is_xml_api else "json"
595
+ feature_files.extend([
596
+ f"{carrier_slug}/schemas/address_validation_request.{ext}",
597
+ f"{carrier_slug}/schemas/address_validation_response.{ext}",
598
+ ])
599
+
600
+ result["generated_structure"]["files"].extend(feature_files)
601
+
602
+ # Provide next steps
603
+ result["next_steps"] = [
604
+ f"1. Run: {result['manual_command']}",
605
+ f"2. When prompted, enter:",
606
+ f" - Carrier slug: {carrier_slug}",
607
+ f" - Display name: {carrier_name}",
608
+ f" - Features: {features}",
609
+ f" - Version: 2025.1",
610
+ f" - Is XML API: {'Yes' if is_xml_api else 'No'}",
611
+ f"3. Navigate to: {output_dir / carrier_slug}",
612
+ "4. Update the generated schema files with actual API specifications",
613
+ "5. Run the 'generate' script to create Python dataclasses",
614
+ "6. Implement the provider files in karrio/providers/ directory",
615
+ "7. Update mapping files in karrio/mappers/ directory",
616
+ "8. Add tests in tests/ directory",
617
+ ]
618
+
619
+ except Exception as e:
620
+ result["error"] = str(e)
621
+ result["success"] = False
622
+
623
+ return result
624
+
625
+
626
+ def get_karrio_plugin_structure_info() -> typing.Dict[str, typing.Any]:
627
+ """
628
+ Get detailed information about the correct Karrio plugin structure.
629
+
630
+ Returns:
631
+ Dictionary with comprehensive plugin structure information
632
+ """
633
+ return {
634
+ "overview": "Karrio plugins follow a specific directory structure with multiple components",
635
+ "official_generation_command": "kcli sdk add-extension",
636
+ "directory_structure": {
637
+ "root": "{carrier_slug}/",
638
+ "description": "Root directory with the carrier slug as the name",
639
+ "contents": {
640
+ "pyproject.toml": "Project configuration and dependencies",
641
+ "README.md": "Documentation for the carrier integration",
642
+ "generate": "Script to generate Python classes from schemas",
643
+ "schemas/": {
644
+ "description": "JSON/XML schema files for API requests and responses",
645
+ "files": [
646
+ "error_response.json/xsd",
647
+ "rate_request.json/xsd",
648
+ "rate_response.json/xsd",
649
+ "shipment_request.json/xsd",
650
+ "shipment_response.json/xsd",
651
+ "tracking_request.json/xsd",
652
+ "tracking_response.json/xsd",
653
+ "pickup_*.json/xsd (if pickup feature enabled)",
654
+ "address_*.json/xsd (if address feature enabled)"
655
+ ]
656
+ },
657
+ "karrio/": {
658
+ "description": "Python implementation files",
659
+ "subdirectories": {
660
+ "schemas/{carrier_slug}/": {
661
+ "description": "Generated Python dataclasses from schemas",
662
+ "files": ["__init__.py", "generated dataclass files"]
663
+ },
664
+ "providers/{carrier_slug}/": {
665
+ "description": "Core implementation files",
666
+ "files": [
667
+ "__init__.py",
668
+ "error.py - Error handling",
669
+ "utils.py - Utilities and settings",
670
+ "units.py - Enums and unit mappings",
671
+ "rates.py - Rate calculation implementation",
672
+ "shipments.py - Shipping implementation",
673
+ "tracking.py - Tracking implementation",
674
+ "pickup.py - Pickup implementation (if enabled)",
675
+ "address.py - Address validation (if enabled)"
676
+ ]
677
+ },
678
+ "mappers/{carrier_slug}/": {
679
+ "description": "API proxy and request/response handling",
680
+ "files": [
681
+ "__init__.py",
682
+ "proxy.py - HTTP client and API communication",
683
+ "mapper.py - Request/response mapping logic"
684
+ ]
685
+ },
686
+ "plugins/{carrier_slug}/": {
687
+ "description": "Plugin registration and entry point",
688
+ "files": [
689
+ "__init__.py",
690
+ "plugin.py - Plugin definition and registration"
691
+ ]
692
+ }
693
+ }
694
+ },
695
+ "tests/": {
696
+ "description": "Test suite for the carrier integration",
697
+ "files": [
698
+ "{carrier_slug}/",
699
+ "test_rates.py",
700
+ "test_shipments.py",
701
+ "test_tracking.py",
702
+ "fixtures/ - Test data"
703
+ ]
704
+ }
705
+ }
706
+ },
707
+ "implementation_workflow": [
708
+ "1. Generate plugin structure using kcli sdk add-extension",
709
+ "2. Update schema files with actual API specifications",
710
+ "3. Run ./generate to create Python dataclasses",
711
+ "4. Implement provider files (rates.py, shipments.py, tracking.py)",
712
+ "5. Implement utils.py with carrier settings and authentication",
713
+ "6. Implement error.py for error handling",
714
+ "7. Update units.py with carrier-specific enums",
715
+ "8. Implement proxy.py for API communication",
716
+ "9. Create comprehensive tests",
717
+ "10. Test integration with Karrio"
718
+ ],
719
+ "key_differences_from_basic_structure": [
720
+ "Uses official CLI tooling instead of manual file creation",
721
+ "Separates schemas (data) from providers (logic) from mappers (API communication)",
722
+ "Includes comprehensive code generation from schemas",
723
+ "Follows Karrio's modular architecture",
724
+ "Includes proper plugin registration system",
725
+ "Has built-in testing framework integration"
726
+ ]
727
+ }
728
+
729
+
730
+ def analyze_carrier_api_documentation(
731
+ api_documentation: str,
732
+ carrier_name: str,
733
+ documentation_type: str = "auto"
734
+ ) -> typing.Dict[str, typing.Any]:
735
+ """
736
+ Analyze carrier API documentation to extract key information for plugin generation.
737
+
738
+ Args:
739
+ api_documentation: The API documentation content
740
+ carrier_name: Name of the carrier
741
+ documentation_type: Type of documentation (openapi, website, pdf, text, auto)
742
+
743
+ Returns:
744
+ Dictionary with analyzed API information
745
+ """
746
+ import re
747
+ import json
748
+
749
+ result = {
750
+ "carrier_name": carrier_name,
751
+ "documentation_type": documentation_type,
752
+ "api_type": "unknown",
753
+ "authentication": {},
754
+ "endpoints": {},
755
+ "operations": [],
756
+ "data_formats": [],
757
+ "recommendations": {}
758
+ }
759
+
760
+ try:
761
+ # Detect API type
762
+ if documentation_type == "auto":
763
+ if "swagger" in api_documentation.lower() or "openapi" in api_documentation.lower():
764
+ documentation_type = "openapi"
765
+ elif "<" in api_documentation and ">" in api_documentation:
766
+ result["api_type"] = "xml"
767
+ elif "{" in api_documentation and "}" in api_documentation:
768
+ result["api_type"] = "json"
769
+
770
+ # Try to parse as OpenAPI/Swagger
771
+ if documentation_type == "openapi":
772
+ try:
773
+ if api_documentation.strip().startswith('{'):
774
+ spec = json.loads(api_documentation)
775
+ else:
776
+ # Handle YAML (simplified)
777
+ spec = {"info": {"title": carrier_name}}
778
+
779
+ result.update({
780
+ "api_type": "rest",
781
+ "documentation_type": "openapi",
782
+ "spec_version": spec.get("openapi", spec.get("swagger", "unknown")),
783
+ "base_url": spec.get("servers", [{}])[0].get("url", "") if spec.get("servers") else "",
784
+ "paths": list(spec.get("paths", {}).keys()),
785
+ })
786
+
787
+ # Extract operations
788
+ for path, methods in spec.get("paths", {}).items():
789
+ for method, details in methods.items():
790
+ if method.upper() in ["GET", "POST", "PUT", "DELETE"]:
791
+ result["endpoints"][f"{method.upper()} {path}"] = {
792
+ "summary": details.get("summary", ""),
793
+ "description": details.get("description", ""),
794
+ "parameters": details.get("parameters", []),
795
+ "requestBody": details.get("requestBody", {}),
796
+ "responses": details.get("responses", {})
797
+ }
798
+ except:
799
+ pass
800
+
801
+ # Analyze for common shipping operations
802
+ operations_found = []
803
+ if re.search(r"rate|quote|pricing", api_documentation, re.IGNORECASE):
804
+ operations_found.append("rating")
805
+ if re.search(r"ship|label|create.*shipment", api_documentation, re.IGNORECASE):
806
+ operations_found.append("shipping")
807
+ if re.search(r"track|trace|status", api_documentation, re.IGNORECASE):
808
+ operations_found.append("tracking")
809
+ if re.search(r"pickup|collect", api_documentation, re.IGNORECASE):
810
+ operations_found.append("pickup")
811
+ if re.search(r"address.*valid|verify.*address", api_documentation, re.IGNORECASE):
812
+ operations_found.append("address")
813
+
814
+ result["operations"] = operations_found
815
+
816
+ # Detect authentication methods
817
+ auth_methods = []
818
+ if re.search(r"api.?key|x-api-key", api_documentation, re.IGNORECASE):
819
+ auth_methods.append("api_key")
820
+ if re.search(r"bearer|authorization.*bearer", api_documentation, re.IGNORECASE):
821
+ auth_methods.append("bearer_token")
822
+ if re.search(r"oauth|client.*secret", api_documentation, re.IGNORECASE):
823
+ auth_methods.append("oauth")
824
+ if re.search(r"basic.*auth|username.*password", api_documentation, re.IGNORECASE):
825
+ auth_methods.append("basic_auth")
826
+
827
+ result["authentication"]["methods"] = auth_methods
828
+
829
+ # Generate recommendations
830
+ recommendations = {}
831
+
832
+ if not result["api_type"] or result["api_type"] == "unknown":
833
+ if "xml" in api_documentation.lower() or "</" in api_documentation:
834
+ recommendations["api_type"] = "xml"
835
+ else:
836
+ recommendations["api_type"] = "json"
837
+
838
+ if not operations_found:
839
+ recommendations["operations"] = ["rating", "shipping", "tracking"]
840
+ recommendations["note"] = "Could not detect specific operations, using common defaults"
841
+
842
+ if not auth_methods:
843
+ recommendations["authentication"] = "api_key"
844
+ recommendations["auth_note"] = "Could not detect auth method, API key is most common"
845
+
846
+ result["recommendations"] = recommendations
847
+
848
+ # Generate plugin configuration
849
+ result["suggested_plugin_config"] = {
850
+ "carrier_slug": carrier_name.lower().replace(" ", "_").replace("-", "_"),
851
+ "carrier_name": carrier_name,
852
+ "features": ",".join(operations_found or ["rating", "shipping", "tracking"]),
853
+ "is_xml_api": result.get("api_type") == "xml" or recommendations.get("api_type") == "xml",
854
+ "authentication_type": auth_methods[0] if auth_methods else "api_key"
855
+ }
856
+
857
+ except Exception as e:
858
+ result["error"] = str(e)
859
+
860
+ return result
861
+
862
+
863
+ def extract_openapi_from_url(url: str) -> typing.Dict[str, typing.Any]:
864
+ """
865
+ Try to extract OpenAPI specification from various common URL patterns.
866
+
867
+ Args:
868
+ url: Base URL or documentation URL
869
+
870
+ Returns:
871
+ Dictionary with OpenAPI spec if found
872
+ """
873
+ import requests
874
+ import json
875
+
876
+ result = {
877
+ "original_url": url,
878
+ "spec_found": False,
879
+ "spec_url": None,
880
+ "spec": None,
881
+ "error": None
882
+ }
883
+
884
+ # Common OpenAPI spec URL patterns
885
+ if not url.endswith('/'):
886
+ url += '/'
887
+
888
+ spec_patterns = [
889
+ "openapi.json",
890
+ "openapi.yaml",
891
+ "swagger.json",
892
+ "swagger.yaml",
893
+ "api-docs",
894
+ "v1/openapi.json",
895
+ "v2/openapi.json",
896
+ "docs/openapi.json",
897
+ "api/openapi.json",
898
+ "static/openapi.json"
899
+ ]
900
+
901
+ headers = {
902
+ 'Accept': 'application/json, application/yaml, text/yaml, */*',
903
+ 'User-Agent': 'Karrio-Agent/1.0'
904
+ }
905
+
906
+ for pattern in spec_patterns:
907
+ try:
908
+ spec_url = url + pattern
909
+ response = requests.get(spec_url, headers=headers, timeout=10)
910
+
911
+ if response.status_code == 200:
912
+ content_type = response.headers.get('content-type', '').lower()
913
+
914
+ # Try to parse as JSON
915
+ if 'json' in content_type or response.text.strip().startswith('{'):
916
+ try:
917
+ spec = response.json()
918
+ if any(key in spec for key in ['openapi', 'swagger', 'info', 'paths']):
919
+ result.update({
920
+ "spec_found": True,
921
+ "spec_url": spec_url,
922
+ "spec": spec,
923
+ "format": "json"
924
+ })
925
+ return result
926
+ except json.JSONDecodeError:
927
+ continue
928
+
929
+ # Try to parse as YAML
930
+ elif 'yaml' in content_type or any(indicator in response.text for indicator in ['openapi:', 'swagger:', 'info:', 'paths:']):
931
+ result.update({
932
+ "spec_found": True,
933
+ "spec_url": spec_url,
934
+ "spec": response.text,
935
+ "format": "yaml"
936
+ })
937
+ return result
938
+
939
+ except requests.RequestException:
940
+ continue
941
+
942
+ result["error"] = "No OpenAPI specification found at common endpoints"
943
+ return result