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,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
|