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,336 @@
|
|
1
|
+
import re
|
2
|
+
import sys
|
3
|
+
import typer
|
4
|
+
import importlib
|
5
|
+
from typing import Optional, List, Set
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
|
9
|
+
app = typer.Typer()
|
10
|
+
|
11
|
+
@app.command("transform")
|
12
|
+
def transform(
|
13
|
+
input_file: Optional[str] = typer.Argument(
|
14
|
+
None, help="Input file path. If not provided, reads from stdin."
|
15
|
+
),
|
16
|
+
output_file: Optional[str] = typer.Argument(
|
17
|
+
None, help="Output file path. If not provided, writes to stdout."
|
18
|
+
),
|
19
|
+
append_type_suffix: bool = typer.Option(
|
20
|
+
True, help="Append 'Type' to class names", is_flag=True
|
21
|
+
),
|
22
|
+
):
|
23
|
+
"""
|
24
|
+
Transform Python code generated by quicktype (using dataclasses)
|
25
|
+
into code that uses attrs and jstruct decorators.
|
26
|
+
"""
|
27
|
+
# Read input from file or stdin
|
28
|
+
if input_file:
|
29
|
+
with open(input_file, "r") as f:
|
30
|
+
content = f.read()
|
31
|
+
else:
|
32
|
+
content = sys.stdin.read()
|
33
|
+
|
34
|
+
# Transform the content
|
35
|
+
transformed = transform_content(content, append_type_suffix)
|
36
|
+
|
37
|
+
# Write output to file or stdout
|
38
|
+
if output_file:
|
39
|
+
with open(output_file, "w") as f:
|
40
|
+
f.write(transformed)
|
41
|
+
else:
|
42
|
+
print(transformed)
|
43
|
+
|
44
|
+
def extract_class_names(content: str) -> Set[str]:
|
45
|
+
"""Extract all class names from the content."""
|
46
|
+
return set(re.findall(r"class\s+(\w+)", content))
|
47
|
+
|
48
|
+
def transform_content(content: str, append_type_suffix: bool = True) -> str:
|
49
|
+
"""Transform dataclass-based code to jstruct-based code."""
|
50
|
+
# Check if we already have a typing import
|
51
|
+
has_typing_import = "import typing" in content
|
52
|
+
|
53
|
+
# Replace imports
|
54
|
+
if "from dataclasses import dataclass" in content:
|
55
|
+
content = content.replace(
|
56
|
+
"from dataclasses import dataclass",
|
57
|
+
"import attr\nimport jstruct" + ("" if has_typing_import else "\nimport typing")
|
58
|
+
)
|
59
|
+
else:
|
60
|
+
# If dataclasses import is not found, add the imports anyway
|
61
|
+
imports = "import attr\nimport jstruct"
|
62
|
+
if not has_typing_import:
|
63
|
+
imports += "\nimport typing"
|
64
|
+
content = imports + "\n" + content
|
65
|
+
|
66
|
+
# Remove any "from typing import" lines completely
|
67
|
+
content = re.sub(r"from typing import [^\n]+\n", "", content)
|
68
|
+
|
69
|
+
# Replace @dataclass decorator with @attr.s
|
70
|
+
content = re.sub(r"@dataclass", "@attr.s(auto_attribs=True)", content)
|
71
|
+
|
72
|
+
# Get all class names
|
73
|
+
class_names = extract_class_names(content)
|
74
|
+
|
75
|
+
# Dictionary to keep track of original class names and their new versions with Type suffix
|
76
|
+
class_name_mapping = {}
|
77
|
+
|
78
|
+
# Create a new names dictionary if we're appending Type
|
79
|
+
if append_type_suffix:
|
80
|
+
for class_name in class_names:
|
81
|
+
# If the class name already ends with 'Type', append 'ObjectType' instead
|
82
|
+
if class_name.endswith('Type'):
|
83
|
+
class_name_mapping[class_name] = f"{class_name}ObjectType"
|
84
|
+
else:
|
85
|
+
class_name_mapping[class_name] = f"{class_name}Type"
|
86
|
+
|
87
|
+
# Rename class definitions only, not field names
|
88
|
+
for original_name, new_name in class_name_mapping.items():
|
89
|
+
# Replace class definitions
|
90
|
+
content = re.sub(
|
91
|
+
rf"class\s+{original_name}\s*:",
|
92
|
+
f"class {new_name}:",
|
93
|
+
content
|
94
|
+
)
|
95
|
+
|
96
|
+
# Process all property definitions
|
97
|
+
lines = content.split('\n')
|
98
|
+
for i in range(len(lines)):
|
99
|
+
line = lines[i]
|
100
|
+
|
101
|
+
# Skip if this is not a class property line
|
102
|
+
if not re.search(r'^\s+\w+\s*:', line):
|
103
|
+
continue
|
104
|
+
|
105
|
+
# Replace typing annotations with module references
|
106
|
+
line = re.sub(r':\s*Any\s*=', r': typing.Any =', line) # Replace standalone Any as a type
|
107
|
+
line = re.sub(r':\s*Union\s*=', r': typing.Union =', line) # Replace standalone Union as a type
|
108
|
+
line = re.sub(r':\s*Dict\s*=', r': typing.Dict =', line) # Replace standalone Dict as a type
|
109
|
+
line = re.sub(r'Optional\[', r'typing.Optional[', line)
|
110
|
+
line = re.sub(r'List\[', r'typing.List[', line)
|
111
|
+
line = re.sub(r'Union\[', r'typing.Union[', line) # Add handling for Union types
|
112
|
+
line = re.sub(r'Dict\[', r'typing.Dict[', line) # Add handling for Dict types
|
113
|
+
line = re.sub(r'(\[|,\s*)Any(\]|,|\s*\]=)', r'\1typing.Any\2', line) # Match Any inside brackets or between commas
|
114
|
+
line = re.sub(r'(\[|,\s*)Union(\[)', r'\1typing.Union\2', line) # Match Union inside brackets or between commas
|
115
|
+
line = re.sub(r'(\[|,\s*)Dict(\[)', r'\1typing.Dict\2', line) # Match Dict inside brackets or between commas
|
116
|
+
|
117
|
+
# Handle nested Union inside Optional
|
118
|
+
line = re.sub(r'typing\.Optional\[(Union\[)', r'typing.Optional[typing.\1', line)
|
119
|
+
|
120
|
+
# Handle nested Dict inside Optional
|
121
|
+
line = re.sub(r'typing\.Optional\[(Dict\[)', r'typing.Optional[typing.\1', line)
|
122
|
+
|
123
|
+
# Handle properties with null values (e.g., "category: None" -> "category: typing.Any = None")
|
124
|
+
line = re.sub(r'(\w+):\s*None\s*$', r'\1: typing.Any = None', line)
|
125
|
+
lines[i] = line
|
126
|
+
|
127
|
+
# Handle complex type annotations for each class
|
128
|
+
for original_name in class_names:
|
129
|
+
class_name = class_name_mapping.get(original_name, original_name) if append_type_suffix else original_name
|
130
|
+
|
131
|
+
# Update type annotations references only, not field names
|
132
|
+
if append_type_suffix and original_name != class_name:
|
133
|
+
# Replace original_name with class_name only when it's inside type annotation brackets
|
134
|
+
# This prevents changing field names that match class names
|
135
|
+
|
136
|
+
# Handle typing.Optional[OriginalName]
|
137
|
+
pattern = re.compile(rf'(typing\.\w+\[)({original_name})(\])')
|
138
|
+
lines[i] = re.sub(pattern, f'\\1{class_name}\\3', lines[i])
|
139
|
+
|
140
|
+
# Handle Union types that contain the class name
|
141
|
+
union_pattern = re.compile(rf'(typing\.Union\[[^,\]]*,\s*)({original_name})(\s*\]|\s*,)')
|
142
|
+
lines[i] = re.sub(union_pattern, f'\\1{class_name}\\3', lines[i])
|
143
|
+
|
144
|
+
# Also handle Union when the class name is the first element
|
145
|
+
union_first_pattern = re.compile(rf'(typing\.Union\[)({original_name})(\s*,)')
|
146
|
+
lines[i] = re.sub(union_first_pattern, f'\\1{class_name}\\3', lines[i])
|
147
|
+
|
148
|
+
# Handle Dict with class name as key or value type
|
149
|
+
dict_pattern = re.compile(rf'(typing\.Dict\[[^,\]]*,\s*)({original_name})(\s*\])')
|
150
|
+
lines[i] = re.sub(dict_pattern, f'\\1{class_name}\\3', lines[i])
|
151
|
+
|
152
|
+
# Handle Dict with class name as key type
|
153
|
+
dict_key_pattern = re.compile(rf'(typing\.Dict\[)({original_name})(\s*,)')
|
154
|
+
lines[i] = re.sub(dict_key_pattern, f'\\1{class_name}\\3', lines[i])
|
155
|
+
|
156
|
+
# Handle nested types: typing.Optional[typing.List[OriginalName]]
|
157
|
+
nested_pattern = re.compile(rf'(typing\.\w+\[typing\.\w+\[)({original_name})(\]\])')
|
158
|
+
lines[i] = re.sub(nested_pattern, f'\\1{class_name}\\3', lines[i])
|
159
|
+
|
160
|
+
# Handle jstruct.JStruct[OriginalName]
|
161
|
+
jstruct_pattern = re.compile(rf'(jstruct\.JStruct\[)({original_name})(\])')
|
162
|
+
lines[i] = re.sub(jstruct_pattern, f'\\1{class_name}\\3', lines[i])
|
163
|
+
|
164
|
+
# Handle jstruct.JList[OriginalName]
|
165
|
+
jlist_pattern = re.compile(rf'(jstruct\.JList\[)({original_name})(\])')
|
166
|
+
lines[i] = re.sub(jlist_pattern, f'\\1{class_name}\\3', lines[i])
|
167
|
+
|
168
|
+
# Check for Optional[ClassType]
|
169
|
+
if re.search(rf'typing\.Optional\[{class_name}\]\s*=\s*None', lines[i]):
|
170
|
+
lines[i] = re.sub(
|
171
|
+
rf'typing\.Optional\[{class_name}\]\s*=\s*None',
|
172
|
+
f'typing.Optional[{class_name}] = jstruct.JStruct[{class_name}]',
|
173
|
+
lines[i]
|
174
|
+
)
|
175
|
+
|
176
|
+
# Check for List[ClassType]
|
177
|
+
elif re.search(rf'typing\.List\[{class_name}\]\s*=\s*None', lines[i]):
|
178
|
+
lines[i] = re.sub(
|
179
|
+
rf'typing\.List\[{class_name}\]\s*=\s*None',
|
180
|
+
f'typing.List[{class_name}] = jstruct.JList[{class_name}]',
|
181
|
+
lines[i]
|
182
|
+
)
|
183
|
+
|
184
|
+
# Check for Optional[List[ClassType]]
|
185
|
+
elif re.search(rf'typing\.Optional\[typing\.List\[{class_name}\]\]\s*=\s*None', lines[i]):
|
186
|
+
lines[i] = re.sub(
|
187
|
+
rf'typing\.Optional\[typing\.List\[{class_name}\]\]\s*=\s*None',
|
188
|
+
f'typing.Optional[typing.List[{class_name}]] = jstruct.JList[{class_name}]',
|
189
|
+
lines[i]
|
190
|
+
)
|
191
|
+
|
192
|
+
# Check for Optional[Union[...]]
|
193
|
+
elif re.search(rf'typing\.Optional\[typing\.Union\[[^\]]*{class_name}[^\]]*\]\]\s*=\s*None', lines[i]):
|
194
|
+
# For simplicity, we're not replacing the value here since Union types are complex
|
195
|
+
# and we don't want to make assumptions about the appropriate jstruct type
|
196
|
+
pass
|
197
|
+
|
198
|
+
# Check for Optional[Dict[...]]
|
199
|
+
elif re.search(rf'typing\.Optional\[typing\.Dict\[[^\]]*{class_name}[^\]]*\]\]\s*=\s*None', lines[i]):
|
200
|
+
# For simplicity, we're not replacing the value here since Dict types are complex
|
201
|
+
# and we don't want to make assumptions about the appropriate jstruct type
|
202
|
+
pass
|
203
|
+
|
204
|
+
return '\n'.join(lines)
|
205
|
+
|
206
|
+
@app.command("generate")
|
207
|
+
def generate(
|
208
|
+
input_file: str = typer.Argument(..., help="Input JSON schema file path"),
|
209
|
+
output_file: Optional[str] = typer.Argument(
|
210
|
+
None, help="Output Python file path. If not provided, writes to stdout."
|
211
|
+
),
|
212
|
+
python_version: str = typer.Option("3.11", help="Python version to target"),
|
213
|
+
just_types: bool = typer.Option(True, help="Generate just the type definitions without serialization code"),
|
214
|
+
append_type_suffix: bool = typer.Option(True, help="Append 'Type' to class names"),
|
215
|
+
nice_property_names: bool = typer.Option(False, help="Use nice property names"),
|
216
|
+
):
|
217
|
+
"""
|
218
|
+
Generate Python code with jstruct from a JSON schema file using quicktype.
|
219
|
+
"""
|
220
|
+
import subprocess
|
221
|
+
|
222
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
223
|
+
logger = logging.getLogger(__name__)
|
224
|
+
|
225
|
+
# Build quicktype command
|
226
|
+
cmd = [
|
227
|
+
"npx",
|
228
|
+
"quicktype",
|
229
|
+
"--no-uuids",
|
230
|
+
"--no-enums",
|
231
|
+
"--no-date-times",
|
232
|
+
"--src-lang", "json",
|
233
|
+
"--lang", "python",
|
234
|
+
"--all-properties-optional",
|
235
|
+
"--no-nice-property-names",
|
236
|
+
f"--python-version", python_version,
|
237
|
+
"--src", input_file
|
238
|
+
]
|
239
|
+
|
240
|
+
if just_types:
|
241
|
+
cmd.append("--just-types")
|
242
|
+
|
243
|
+
if nice_property_names:
|
244
|
+
cmd.remove("--no-nice-property-names")
|
245
|
+
|
246
|
+
# Run quicktype to generate Python code with dataclasses
|
247
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
248
|
+
|
249
|
+
if result.returncode != 0:
|
250
|
+
logger.error(f"Error generating code for {os.path.basename(input_file)}: {result.stderr}")
|
251
|
+
print(f"Error running quicktype: {result.stderr}", file=sys.stderr)
|
252
|
+
sys.exit(1)
|
253
|
+
|
254
|
+
# Transform the output to use jstruct
|
255
|
+
transformed = transform_content(result.stdout, append_type_suffix)
|
256
|
+
|
257
|
+
# Write output to file or stdout
|
258
|
+
if output_file:
|
259
|
+
with open(output_file, "w") as f:
|
260
|
+
f.write(transformed)
|
261
|
+
logger.info(f"Generated {os.path.basename(output_file)} from {os.path.basename(input_file)}")
|
262
|
+
else:
|
263
|
+
print(transformed)
|
264
|
+
logger.info(f"Generated code from {os.path.basename(input_file)} to stdout")
|
265
|
+
|
266
|
+
def instantiate_tree(cls, indent=0, alias=""):
|
267
|
+
tree = f"{alias}{cls.__name__}(\n"
|
268
|
+
indent += 1
|
269
|
+
items = cls.__annotations__.items() if hasattr(cls, "__annotations__") else []
|
270
|
+
|
271
|
+
for name, typ in items:
|
272
|
+
if typ.__name__ == "Optional" and hasattr(typ, "__args__"):
|
273
|
+
typ = typ.__args__[0]
|
274
|
+
if typ.__name__ == "List" and hasattr(typ, "__args__"):
|
275
|
+
typ = typ.__args__[0]
|
276
|
+
if hasattr(typ, "__annotations__"):
|
277
|
+
tree += (
|
278
|
+
" " * indent * 4
|
279
|
+
+ f"{name}=[\n"
|
280
|
+
+ " " * (indent + 1) * 4
|
281
|
+
+ f"{instantiate_tree(typ, indent + 1, alias=alias)}\n"
|
282
|
+
+ " " * indent * 4
|
283
|
+
+ "],\n"
|
284
|
+
)
|
285
|
+
else:
|
286
|
+
tree += " " * indent * 4 + f"{name}=[],\n"
|
287
|
+
elif hasattr(typ, "__annotations__"):
|
288
|
+
tree += (
|
289
|
+
" " * indent * 4
|
290
|
+
+ f"{name}={instantiate_tree(typ, indent, alias=alias)},\n"
|
291
|
+
)
|
292
|
+
else:
|
293
|
+
tree += " " * indent * 4 + f"{name}=None,\n"
|
294
|
+
|
295
|
+
tree += " " * (indent - 1) * 4 + ")"
|
296
|
+
return tree
|
297
|
+
|
298
|
+
|
299
|
+
def instantiate_class_from_module(
|
300
|
+
module_name: str,
|
301
|
+
class_name: str,
|
302
|
+
module_alias: str = "",
|
303
|
+
):
|
304
|
+
module = importlib.import_module(module_name)
|
305
|
+
cls = getattr(module, class_name)
|
306
|
+
alias = f"{module_alias}." if module_alias != "" else ""
|
307
|
+
|
308
|
+
return instantiate_tree(cls, alias=alias)
|
309
|
+
|
310
|
+
|
311
|
+
@app.command("create-tree")
|
312
|
+
def create_tree(
|
313
|
+
module: str = typer.Option(..., prompt=True, help="Module containing the class"),
|
314
|
+
class_name: str = typer.Option(..., prompt=True, help="Class name to generate a tree for"),
|
315
|
+
module_alias: str = typer.Option("", help="Optional alias for the module in the output"),
|
316
|
+
):
|
317
|
+
"""
|
318
|
+
Generate a Python code tree from a class definition.
|
319
|
+
|
320
|
+
This command imports a class from a specified module and generates
|
321
|
+
a Python code snippet that shows how to construct an instance of that class
|
322
|
+
with all its nested properties.
|
323
|
+
"""
|
324
|
+
if not module or not class_name:
|
325
|
+
print("module and class_name are required")
|
326
|
+
raise typer.Abort()
|
327
|
+
|
328
|
+
output = instantiate_class_from_module(
|
329
|
+
module,
|
330
|
+
class_name,
|
331
|
+
module_alias=module_alias,
|
332
|
+
)
|
333
|
+
typer.echo(output)
|
334
|
+
|
335
|
+
if __name__ == "__main__":
|
336
|
+
app()
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import os
|
2
|
+
import typer
|
3
|
+
import requests
|
4
|
+
|
5
|
+
app = typer.Typer()
|
6
|
+
|
7
|
+
DEFAULT_HOST = "http://localhost:5002"
|
8
|
+
|
9
|
+
|
10
|
+
def get_config():
|
11
|
+
config_file = os.path.expanduser("~/.karrio/config")
|
12
|
+
if os.path.exists(config_file):
|
13
|
+
with open(config_file, "r") as f:
|
14
|
+
return dict(line.strip().split("=") for line in f)
|
15
|
+
return {}
|
16
|
+
|
17
|
+
|
18
|
+
def get_host_and_key():
|
19
|
+
config = get_config()
|
20
|
+
host = os.environ.get("KARRIO_HOST") or config.get("KARRIO_HOST") or DEFAULT_HOST
|
21
|
+
api_key = os.environ.get("KARRIO_API_KEY") or config.get("KARRIO_API_KEY")
|
22
|
+
return host, api_key
|
23
|
+
|
24
|
+
|
25
|
+
@app.command()
|
26
|
+
def login(
|
27
|
+
host: str = typer.Option(
|
28
|
+
DEFAULT_HOST,
|
29
|
+
prompt="Enter the Karrio host URL",
|
30
|
+
help="The URL of the Karrio instance",
|
31
|
+
),
|
32
|
+
api_key: str = typer.Option(
|
33
|
+
...,
|
34
|
+
prompt="Enter your Karrio API key",
|
35
|
+
hide_input=True,
|
36
|
+
help="Your Karrio API key",
|
37
|
+
),
|
38
|
+
):
|
39
|
+
"""
|
40
|
+
Configure a connection to a Karrio instance.
|
41
|
+
|
42
|
+
Example:
|
43
|
+
```terminal
|
44
|
+
kcli login --host http://localhost:5002 --api-key your_api_key_here | jq '{message: "Login successful", host: .host}'
|
45
|
+
```
|
46
|
+
"""
|
47
|
+
if not host:
|
48
|
+
host = DEFAULT_HOST
|
49
|
+
|
50
|
+
try:
|
51
|
+
# Validate the connection
|
52
|
+
headers = {"Authorization": f"Token {api_key}"}
|
53
|
+
response = requests.get(f"{host}", headers=headers)
|
54
|
+
response.raise_for_status()
|
55
|
+
|
56
|
+
# Save the configuration
|
57
|
+
config_dir = os.path.expanduser("~/.karrio")
|
58
|
+
os.makedirs(config_dir, exist_ok=True)
|
59
|
+
config_file = os.path.join(config_dir, "config")
|
60
|
+
|
61
|
+
with open(config_file, "w") as f:
|
62
|
+
f.write(f"KARRIO_HOST={host}\n")
|
63
|
+
f.write(f"KARRIO_API_KEY={api_key}\n")
|
64
|
+
|
65
|
+
typer.echo(f"Successfully logged in to Karrio instance at {host}")
|
66
|
+
typer.echo(f"Configuration saved to {config_file}")
|
67
|
+
|
68
|
+
except requests.RequestException as e:
|
69
|
+
typer.echo(f"Error connecting to Karrio instance: {str(e)}", err=True)
|
70
|
+
except IOError as e:
|
71
|
+
typer.echo(f"Error saving configuration: {str(e)}", err=True)
|
72
|
+
|
73
|
+
|
74
|
+
@app.command()
|
75
|
+
def logout():
|
76
|
+
"""
|
77
|
+
Remove the saved Karrio configuration.
|
78
|
+
|
79
|
+
Example:
|
80
|
+
```terminal
|
81
|
+
kcli logout | jq '{message: "Logout successful"}'
|
82
|
+
```
|
83
|
+
"""
|
84
|
+
config_file = os.path.expanduser("~/.karrio/config")
|
85
|
+
try:
|
86
|
+
os.remove(config_file)
|
87
|
+
typer.echo("Successfully logged out. Karrio configuration removed.")
|
88
|
+
except FileNotFoundError:
|
89
|
+
typer.echo("No saved Karrio configuration found.")
|
90
|
+
except IOError as e:
|
91
|
+
typer.echo(f"Error removing configuration: {str(e)}", err=True)
|
92
|
+
|
93
|
+
|
94
|
+
@app.command()
|
95
|
+
def status():
|
96
|
+
"""
|
97
|
+
Check the current login status and connection to Karrio.
|
98
|
+
|
99
|
+
Example:
|
100
|
+
```terminal
|
101
|
+
kcli status | jq '{status: "Connected", host: .host, api_key: "********"}'
|
102
|
+
```
|
103
|
+
"""
|
104
|
+
host, api_key = get_host_and_key()
|
105
|
+
|
106
|
+
if api_key:
|
107
|
+
try:
|
108
|
+
headers = {"Authorization": f"Token {api_key}"}
|
109
|
+
response = requests.get(f"{host}", headers=headers)
|
110
|
+
response.raise_for_status()
|
111
|
+
|
112
|
+
# Display connection status
|
113
|
+
typer.echo(f"Connected to Karrio instance at {host}")
|
114
|
+
|
115
|
+
# Display API metadata if available
|
116
|
+
try:
|
117
|
+
metadata = response.json()
|
118
|
+
typer.echo("\nAPI Metadata:")
|
119
|
+
for key, value in metadata.items():
|
120
|
+
typer.echo(f" {key}: {value}")
|
121
|
+
except ValueError:
|
122
|
+
typer.echo("\nNo API metadata available")
|
123
|
+
|
124
|
+
# Display API key source
|
125
|
+
if os.environ.get("KARRIO_API_KEY"):
|
126
|
+
typer.echo("\nUsing API key from environment variable")
|
127
|
+
elif os.path.exists(os.path.expanduser("~/.karrio/config")):
|
128
|
+
typer.echo("\nUsing API key from config file")
|
129
|
+
|
130
|
+
except requests.RequestException as e:
|
131
|
+
typer.echo(f"Error connecting to Karrio instance: {str(e)}", err=True)
|
132
|
+
else:
|
133
|
+
typer.echo(
|
134
|
+
"Not logged in. Use 'karrio login' to configure the connection or set KARRIO_API_KEY environment variable."
|
135
|
+
)
|
136
|
+
|
137
|
+
|
138
|
+
if __name__ == "__main__":
|
139
|
+
app()
|
@@ -0,0 +1,168 @@
|
|
1
|
+
import typer
|
2
|
+
import karrio.references as references
|
3
|
+
import typing
|
4
|
+
|
5
|
+
app = typer.Typer()
|
6
|
+
|
7
|
+
@app.command("list")
|
8
|
+
def list_plugins(
|
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 plugins with short description and active status.
|
14
|
+
|
15
|
+
Examples:
|
16
|
+
```terminal
|
17
|
+
# Get all plugins and display as a table (default)
|
18
|
+
kcli plugins list
|
19
|
+
```
|
20
|
+
|
21
|
+
```terminal
|
22
|
+
# Get plugins in JSON format
|
23
|
+
kcli plugins list --pretty | jq ".[] | {id, label, status, enabled}"
|
24
|
+
```
|
25
|
+
|
26
|
+
Example Output:
|
27
|
+
```json
|
28
|
+
[
|
29
|
+
{
|
30
|
+
"id": "plugin_id",
|
31
|
+
"label": "Plugin Name",
|
32
|
+
"status": "active",
|
33
|
+
"enabled": true,
|
34
|
+
"description": "A brief description of the plugin functionality"
|
35
|
+
}
|
36
|
+
]
|
37
|
+
```
|
38
|
+
"""
|
39
|
+
plugins = references.collect_plugins_data()
|
40
|
+
registry = references.Registry()
|
41
|
+
results = []
|
42
|
+
for plugin_id, plugin in plugins.items():
|
43
|
+
enabled = registry.get(f"{plugin_id.upper()}_ENABLED", True)
|
44
|
+
results.append({
|
45
|
+
"id": plugin_id,
|
46
|
+
"label": plugin.get("label", ""),
|
47
|
+
"status": plugin.get("status", ""),
|
48
|
+
"enabled": enabled,
|
49
|
+
"description": plugin.get("description", ""),
|
50
|
+
})
|
51
|
+
if pretty:
|
52
|
+
import json
|
53
|
+
typer.echo(json.dumps(results, indent=2))
|
54
|
+
else:
|
55
|
+
try:
|
56
|
+
from tabulate import tabulate
|
57
|
+
table = [
|
58
|
+
[i, plugin['id'], plugin['label'], plugin['status'], 'ENABLED' if plugin['enabled'] else 'DISABLED']
|
59
|
+
for i, plugin in enumerate(results, 1)
|
60
|
+
]
|
61
|
+
headers = ['#', 'ID', 'Label', 'Status', 'Enabled']
|
62
|
+
typer.echo(tabulate(table, headers=headers, tablefmt='github'))
|
63
|
+
except ImportError:
|
64
|
+
for i, plugin in enumerate(results, 1):
|
65
|
+
line = f"{i}. {plugin['id']} - {plugin['label']} ({'ENABLED' if plugin['enabled'] else 'DISABLED'}) - {plugin['status']}"
|
66
|
+
if line_numbers:
|
67
|
+
typer.echo(f"{i}: {line}")
|
68
|
+
else:
|
69
|
+
typer.echo(line)
|
70
|
+
|
71
|
+
@app.command("show")
|
72
|
+
def show_plugin(
|
73
|
+
plugin_id: str,
|
74
|
+
pretty: bool = typer.Option(False, "--pretty", "-p", help="Pretty print the output"),
|
75
|
+
line_numbers: bool = typer.Option(False, "--line-numbers", "-n", help="Show line numbers in pretty print"),
|
76
|
+
):
|
77
|
+
"""
|
78
|
+
Show full details for a plugin by ID.
|
79
|
+
|
80
|
+
Example:
|
81
|
+
```terminal
|
82
|
+
kcli plugins show plugin_id --pretty | jq "{id, label, description, status, enabled}"
|
83
|
+
```
|
84
|
+
|
85
|
+
Example Output:
|
86
|
+
```json
|
87
|
+
{
|
88
|
+
"id": "plugin_id",
|
89
|
+
"label": "Plugin Name",
|
90
|
+
"description": "A detailed description of the plugin functionality",
|
91
|
+
"status": "active",
|
92
|
+
"enabled": true,
|
93
|
+
"version": "1.0.0",
|
94
|
+
"author": "Plugin Author",
|
95
|
+
"website": "https://plugin-website.com",
|
96
|
+
"dependencies": {
|
97
|
+
"python": ">=3.8",
|
98
|
+
"karrio": ">=2024.12"
|
99
|
+
}
|
100
|
+
}
|
101
|
+
```
|
102
|
+
"""
|
103
|
+
details = references.get_plugin_details(plugin_id)
|
104
|
+
if not details:
|
105
|
+
typer.echo(f"Plugin '{plugin_id}' not found.", err=True)
|
106
|
+
raise typer.Exit(code=1)
|
107
|
+
import json
|
108
|
+
if pretty:
|
109
|
+
typer.echo(json.dumps(details, indent=2))
|
110
|
+
else:
|
111
|
+
for i, (k, v) in enumerate(details.items(), 1):
|
112
|
+
line = f"{k}: {v}"
|
113
|
+
if line_numbers:
|
114
|
+
typer.echo(f"{i}: {line}")
|
115
|
+
else:
|
116
|
+
typer.echo(line)
|
117
|
+
|
118
|
+
@app.command("enable")
|
119
|
+
def enable_plugin(
|
120
|
+
plugin_id: str,
|
121
|
+
):
|
122
|
+
"""
|
123
|
+
Enable a plugin by updating the Django Constance env var associated.
|
124
|
+
|
125
|
+
Example:
|
126
|
+
```terminal
|
127
|
+
kcli plugins enable plugin_id
|
128
|
+
```
|
129
|
+
|
130
|
+
Example Output:
|
131
|
+
```text
|
132
|
+
Plugin 'plugin_id' enabled.
|
133
|
+
```
|
134
|
+
"""
|
135
|
+
registry = references.Registry()
|
136
|
+
key = f"{plugin_id.upper()}_ENABLED"
|
137
|
+
try:
|
138
|
+
registry[key] = True
|
139
|
+
typer.echo(f"Plugin '{plugin_id}' enabled.")
|
140
|
+
except Exception as e:
|
141
|
+
typer.echo(f"Failed to enable plugin '{plugin_id}': {e}", err=True)
|
142
|
+
raise typer.Exit(code=1)
|
143
|
+
|
144
|
+
@app.command("disable")
|
145
|
+
def disable_plugin(
|
146
|
+
plugin_id: str,
|
147
|
+
):
|
148
|
+
"""
|
149
|
+
Disable a plugin by updating the Django Constance env var associated.
|
150
|
+
|
151
|
+
Example:
|
152
|
+
```terminal
|
153
|
+
kcli plugins disable plugin_id
|
154
|
+
```
|
155
|
+
|
156
|
+
Example Output:
|
157
|
+
```text
|
158
|
+
Plugin 'plugin_id' disabled.
|
159
|
+
```
|
160
|
+
"""
|
161
|
+
registry = references.Registry()
|
162
|
+
key = f"{plugin_id.upper()}_ENABLED"
|
163
|
+
try:
|
164
|
+
registry[key] = False
|
165
|
+
typer.echo(f"Plugin '{plugin_id}' disabled.")
|
166
|
+
except Exception as e:
|
167
|
+
typer.echo(f"Failed to disable plugin '{plugin_id}': {e}", err=True)
|
168
|
+
raise typer.Exit(code=1)
|