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,101 @@
1
+ GET_LOGS = """
2
+ query get_logs($filter: LogFilter) {
3
+ logs(filter: $filter) {
4
+ page_info {
5
+ count
6
+ has_next_page
7
+ has_previous_page
8
+ start_cursor
9
+ end_cursor
10
+ }
11
+ edges {
12
+ node {
13
+ id
14
+ path
15
+ host
16
+ data
17
+ method
18
+ response_ms
19
+ remote_addr
20
+ requested_at
21
+ status_code
22
+ query_params
23
+ response
24
+ records {
25
+ id
26
+ key
27
+ timestamp
28
+ test_mode
29
+ created_at
30
+ meta
31
+ record
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
37
+ """
38
+
39
+ GET_LOG = """
40
+ query get_log($id: Int!) {
41
+ log(id: $id) {
42
+ id
43
+ requested_at
44
+ response_ms
45
+ path
46
+ remote_addr
47
+ host
48
+ method
49
+ query_params
50
+ data
51
+ response
52
+ status_code
53
+ records {
54
+ id
55
+ key
56
+ timestamp
57
+ test_mode
58
+ created_at
59
+ meta
60
+ record
61
+ }
62
+ }
63
+ }
64
+ """
65
+
66
+ GET_EVENTS = """
67
+ query get_events($filter: EventFilter) {
68
+ events(filter: $filter) {
69
+ page_info {
70
+ count
71
+ has_next_page
72
+ has_previous_page
73
+ start_cursor
74
+ end_cursor
75
+ }
76
+ edges {
77
+ node {
78
+ id
79
+ type
80
+ data
81
+ test_mode
82
+ pending_webhooks
83
+ created_at
84
+ }
85
+ }
86
+ }
87
+ }
88
+ """
89
+
90
+ GET_EVENT = """
91
+ query get_event($id: String!) {
92
+ event(id: $id) {
93
+ id
94
+ type
95
+ data
96
+ test_mode
97
+ pending_webhooks
98
+ created_at
99
+ }
100
+ }
101
+ """
@@ -0,0 +1,368 @@
1
+ import json
2
+ import enum
3
+ import pydoc
4
+ import typer
5
+ import requests
6
+ import importlib
7
+ import karrio_cli.commands.login as login
8
+ from rich.syntax import Syntax
9
+ from rich.console import Console
10
+ from .queries import GET_LOGS, GET_LOG, GET_EVENTS, GET_EVENT
11
+
12
+ DEFAULT_FEATURES = [
13
+ "tracking",
14
+ "rating",
15
+ "shipping",
16
+ ]
17
+
18
+ console = Console()
19
+
20
+ # GraphQL Queries
21
+ GET_EVENTS = """
22
+ query get_events($filter: EventFilter) {
23
+ events(filter: $filter) {
24
+ page_info {
25
+ count
26
+ has_next_page
27
+ has_previous_page
28
+ start_cursor
29
+ end_cursor
30
+ }
31
+ edges {
32
+ node {
33
+ id
34
+ type
35
+ data
36
+ test_mode
37
+ pending_webhooks
38
+ created_at
39
+ }
40
+ }
41
+ }
42
+ }
43
+ """
44
+
45
+ GET_EVENT = """
46
+ query get_event($id: String!) {
47
+ event(id: $id) {
48
+ id
49
+ type
50
+ data
51
+ test_mode
52
+ pending_webhooks
53
+ created_at
54
+ }
55
+ }
56
+ """
57
+
58
+ def parse_json_or_xml_string(value: str):
59
+ """Parse a string that might be JSON or XML."""
60
+ if not value or not isinstance(value, str):
61
+ return value
62
+
63
+ # Try to parse as JSON first
64
+ try:
65
+ return json.loads(value)
66
+ except json.JSONDecodeError:
67
+ # If not JSON, return as is (could be XML or other format)
68
+ return value
69
+
70
+
71
+ def parse_records(records):
72
+ """Parse record data and response fields."""
73
+ if not records:
74
+ return records
75
+
76
+ return [
77
+ {
78
+ **record,
79
+ "record": {
80
+ **record["record"],
81
+ "data": parse_json_or_xml_string(record["record"].get("data")),
82
+ "response": parse_json_or_xml_string(record["record"].get("response")),
83
+ } if record.get("record") else record["record"]
84
+ }
85
+ for record in records
86
+ ]
87
+
88
+
89
+ def parse_log_response(data):
90
+ """Parse log response data, including nested records."""
91
+ if not data:
92
+ return data
93
+
94
+ # Handle logs list response
95
+ if "logs" in data:
96
+ edges = data["logs"].get("edges", [])
97
+ for edge in edges:
98
+ node = edge.get("node", {})
99
+ if node:
100
+ node["response"] = parse_json_or_xml_string(node.get("response"))
101
+ node["data"] = parse_json_or_xml_string(node.get("data"))
102
+ node["records"] = parse_records(node.get("records", []))
103
+ return data
104
+
105
+ # Handle single log response
106
+ if "log" in data:
107
+ log = data["log"]
108
+ if log:
109
+ log["response"] = parse_json_or_xml_string(log.get("response"))
110
+ log["data"] = parse_json_or_xml_string(log.get("data"))
111
+ log["records"] = parse_records(log.get("records", []))
112
+ return data
113
+
114
+ return data
115
+
116
+
117
+ def format_json_output(data, pretty_print=False, line_numbers=False):
118
+ json_str = json.dumps(data, default=str, indent=2)
119
+ if pretty_print:
120
+ syntax = Syntax(json_str, "json", theme="ansi_light", line_numbers=line_numbers)
121
+ console.print(syntax)
122
+ else:
123
+ typer.echo(json_str)
124
+
125
+
126
+ def get_api_url_and_headers():
127
+ host, api_key = login.get_host_and_key()
128
+ headers = {"Authorization": f"Token {api_key}"} if api_key else {}
129
+ return host, headers
130
+
131
+
132
+ def make_request(
133
+ method,
134
+ endpoint: str,
135
+ payload: dict = None,
136
+ params: dict = None,
137
+ pretty_print: bool = False,
138
+ line_numbers: bool = False,
139
+ ):
140
+ api_url, headers = get_api_url_and_headers()
141
+ url = f"{api_url}/{endpoint}"
142
+
143
+ try:
144
+ if method == "GET":
145
+ response = requests.get(url, headers=headers, params=params)
146
+ elif method == "POST":
147
+ response = requests.post(url, json=payload, headers=headers)
148
+ elif method == "PATCH":
149
+ response = requests.patch(url, json=payload, headers=headers)
150
+ elif method == "DELETE":
151
+ response = requests.delete(url, headers=headers)
152
+ else:
153
+ raise ValueError(f"Unsupported HTTP method: {method}")
154
+
155
+ response.raise_for_status()
156
+
157
+ if response.status_code == 204: # No Content
158
+ return None
159
+
160
+ data = response.json()
161
+ format_json_output(data, pretty_print, line_numbers)
162
+ return data
163
+ except requests.RequestException as e:
164
+ error_message = {"error": str(e)}
165
+ format_json_output(error_message, pretty_print, line_numbers)
166
+ return None
167
+
168
+
169
+ def make_get_request(
170
+ endpoint: str,
171
+ params: dict = None,
172
+ pretty_print: bool = False,
173
+ line_numbers: bool = False,
174
+ ):
175
+ return make_request(
176
+ "GET",
177
+ endpoint,
178
+ params=params,
179
+ pretty_print=pretty_print,
180
+ line_numbers=line_numbers,
181
+ )
182
+
183
+
184
+ def make_post_request(
185
+ endpoint: str, payload: dict, pretty_print: bool = False, line_numbers: bool = False
186
+ ):
187
+ return make_request(
188
+ "POST",
189
+ endpoint,
190
+ payload=payload,
191
+ pretty_print=pretty_print,
192
+ line_numbers=line_numbers,
193
+ )
194
+
195
+
196
+ def make_patch_request(
197
+ endpoint: str, payload: dict, pretty_print: bool = False, line_numbers: bool = False
198
+ ):
199
+ return make_request(
200
+ "PATCH",
201
+ endpoint,
202
+ payload=payload,
203
+ pretty_print=pretty_print,
204
+ line_numbers=line_numbers,
205
+ )
206
+
207
+
208
+ def make_delete_request(
209
+ endpoint: str, pretty_print: bool = False, line_numbers: bool = False
210
+ ):
211
+ return make_request(
212
+ "DELETE", endpoint, pretty_print=pretty_print, line_numbers=line_numbers
213
+ )
214
+
215
+
216
+ def parse_event_response(data):
217
+ """Parse the GraphQL response for events."""
218
+ if "events" in data:
219
+ return {"events": data["events"]}
220
+ elif "event" in data:
221
+ return {"event": data["event"]}
222
+ return data
223
+
224
+
225
+ def make_graphql_request(
226
+ query_name: str,
227
+ variables: dict = None,
228
+ pretty_print: bool = False,
229
+ line_numbers: bool = False,
230
+ ):
231
+ """
232
+ Make a GraphQL request to the API.
233
+
234
+ Args:
235
+ query_name: Name of the query to execute (e.g., 'get_logs', 'get_log', 'get_events', 'get_event')
236
+ variables: Variables to pass to the query
237
+ pretty_print: Whether to pretty print the output
238
+ line_numbers: Whether to show line numbers in pretty print
239
+ """
240
+ api_url, headers = get_api_url_and_headers()
241
+ url = f"{api_url}/graphql"
242
+
243
+ # Get the query based on the query name
244
+ query = {
245
+ "get_logs": GET_LOGS,
246
+ "get_log": GET_LOG,
247
+ "get_events": GET_EVENTS,
248
+ "get_event": GET_EVENT,
249
+ }.get(query_name)
250
+
251
+ if not query:
252
+ raise ValueError(f"Unknown query: {query_name}")
253
+
254
+ payload = {
255
+ "query": query,
256
+ "variables": variables or {}
257
+ }
258
+
259
+ try:
260
+ response = requests.post(url, json=payload, headers=headers)
261
+ response.raise_for_status()
262
+
263
+ data = response.json()
264
+
265
+ # Check for GraphQL errors
266
+ if "errors" in data:
267
+ error_message = {"error": data["errors"]}
268
+ format_json_output(error_message, pretty_print, line_numbers)
269
+ return None
270
+
271
+ # Extract and parse the data from the response
272
+ if query_name.startswith("get_event"):
273
+ result = parse_event_response(data.get("data", {}))
274
+ else:
275
+ result = parse_log_response(data.get("data", {}))
276
+
277
+ format_json_output(result, pretty_print, line_numbers)
278
+ return result
279
+
280
+ except requests.RequestException as e:
281
+ error_message = {"error": str(e)}
282
+ format_json_output(error_message, pretty_print, line_numbers)
283
+ return None
284
+
285
+
286
+ def gen(entity):
287
+ return pydoc.render_doc(entity, renderer=pydoc.plaintext)
288
+
289
+
290
+ def format_dimension(code, dim):
291
+ return (
292
+ f"| `{ code }` "
293
+ f'| { f" x ".join([str(d) for d in dim.values() if isinstance(d, float)]) } '
294
+ f'| { f" x ".join([k for k in dim.keys() if isinstance(dim[k], float)]) }'
295
+ )
296
+
297
+
298
+ def instantiate_tree(cls, indent=0, alias=""):
299
+ tree = f"{alias}{cls.__name__}(\n"
300
+ indent += 1
301
+ items = cls.__annotations__.items() if hasattr(cls, "__annotations__") else []
302
+
303
+ for name, typ in items:
304
+ if typ.__name__ == "Optional" and hasattr(typ, "__args__"):
305
+ typ = typ.__args__[0]
306
+ if typ.__name__ == "List" and hasattr(typ, "__args__"):
307
+ typ = typ.__args__[0]
308
+ if hasattr(typ, "__annotations__"):
309
+ tree += (
310
+ " " * indent * 4
311
+ + f"{name}=[\n"
312
+ + " " * (indent + 1) * 4
313
+ + f"{instantiate_tree(typ, indent + 1, alias=alias)}\n"
314
+ + " " * indent * 4
315
+ + "],\n"
316
+ )
317
+ else:
318
+ tree += " " * indent * 4 + f"{name}=[],\n"
319
+ elif hasattr(typ, "__annotations__"):
320
+ tree += (
321
+ " " * indent * 4
322
+ + f"{name}={instantiate_tree(typ, indent, alias=alias)},\n"
323
+ )
324
+ else:
325
+ tree += " " * indent * 4 + f"{name}=None,\n"
326
+
327
+ tree += " " * (indent - 1) * 4 + ")"
328
+ return tree
329
+
330
+
331
+ def instantiate_class_from_module(
332
+ module_name: str,
333
+ class_name: str,
334
+ module_alias: str = "",
335
+ ):
336
+ module = importlib.import_module(module_name)
337
+ cls = getattr(module, class_name)
338
+ alias = f"{module_alias}." if module_alias != "" else ""
339
+
340
+ return instantiate_tree(cls, alias=alias)
341
+
342
+
343
+ def parse_nested_properties(properties: list[str]) -> dict:
344
+ """
345
+ Parse a list of nested properties into a dictionary.
346
+
347
+ Example input: ["payment[paid_by]=sender", "metadata[key1]=value1"]
348
+ """
349
+ result = {}
350
+
351
+ def set_nested(d, path, value):
352
+ keys = path.split("[")
353
+ for i, key in enumerate(keys):
354
+ key = key.rstrip("]")
355
+ if i == len(keys) - 1:
356
+ d[key] = value
357
+ else:
358
+ d = d.setdefault(key, {})
359
+
360
+ for prop in properties:
361
+ if "=" not in prop:
362
+ raise ValueError(
363
+ f"Invalid property format: {prop}. Use 'key=value' or 'nested[key]=value'."
364
+ )
365
+ path, value = prop.split("=", 1)
366
+ set_nested(result, path, value)
367
+
368
+ return result
File without changes
@@ -0,0 +1,91 @@
1
+ import typer
2
+ import karrio_cli.common.utils as utils
3
+ import typing
4
+
5
+ app = typer.Typer()
6
+
7
+ @app.command("list")
8
+ def list_carriers(
9
+ pretty: bool = typer.Option(False, "--pretty", "-p", help="Pretty print the output"),
10
+ line_numbers: bool = typer.Option(False, "--line-numbers", "-n", help="Show line numbers in pretty print"),
11
+ ):
12
+ """
13
+ List all carriers.
14
+
15
+ Examples:
16
+ ```terminal
17
+ # Get all carriers and display as a table
18
+ kcli carriers list | jq -r ".results[] | [.name, .display_name, .capabilities[]] | @tsv" | column -t -s $"\t"
19
+ ```
20
+
21
+ ```terminal
22
+ # Get carriers and extract specific fields
23
+ kcli carriers list | jq ".results[] | {name, display_name, capabilities}"
24
+ ```
25
+
26
+ Example Output:
27
+ ```json
28
+ {
29
+ "count": 3,
30
+ "next": null,
31
+ "previous": null,
32
+ "results": [
33
+ {
34
+ "name": "ups",
35
+ "display_name": "UPS",
36
+ "capabilities": ["rating", "shipping", "tracking"],
37
+ "services": {
38
+ "ups_standard": "11",
39
+ "ups_express": "01",
40
+ "ups_expedited": "02"
41
+ },
42
+ "requirements": ["api_key", "password", "account_number"]
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+ """
48
+ utils.make_get_request(
49
+ "v1/carriers", params=None, pretty_print=pretty, line_numbers=line_numbers
50
+ )
51
+
52
+ @app.command("retrieve")
53
+ def retrieve_carrier(
54
+ carrier_name: str,
55
+ pretty: bool = typer.Option(False, "--pretty", "-p", help="Pretty print the output"),
56
+ line_numbers: bool = typer.Option(False, "--line-numbers", "-n", help="Show line numbers in pretty print"),
57
+ ):
58
+ """
59
+ Retrieve a carrier by name.
60
+
61
+ Example:
62
+ ```terminal
63
+ kcli carriers retrieve ups | jq "{name, display_name, capabilities, services: .services | length}"
64
+ ```
65
+
66
+ Example Output:
67
+ ```json
68
+ {
69
+ "name": "ups",
70
+ "display_name": "UPS",
71
+ "capabilities": ["rating", "shipping", "tracking"],
72
+ "services": {
73
+ "ups_standard": "11",
74
+ "ups_express": "01",
75
+ "ups_expedited": "02",
76
+ "ups_express_plus": "14",
77
+ "ups_worldwide_express": "07",
78
+ "ups_worldwide_expedited": "08",
79
+ "ups_standard_international": "65"
80
+ },
81
+ "requirements": ["api_key", "password", "account_number"],
82
+ "metadata": {
83
+ "test_mode_supported": true,
84
+ "multi_piece_supported": true
85
+ }
86
+ }
87
+ ```
88
+ """
89
+ utils.make_get_request(
90
+ f"v1/carriers/{carrier_name}", pretty_print=pretty, line_numbers=line_numbers
91
+ )