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