ecosyste-ms-cli 1.4.0__tar.gz → 1.4.2__tar.gz

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 (136) hide show
  1. ecosyste_ms_cli-1.4.2/CHANGELOG.md +21 -0
  2. {ecosyste_ms_cli-1.4.0/ecosyste_ms_cli.egg-info → ecosyste_ms_cli-1.4.2}/PKG-INFO +1 -1
  3. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2/ecosyste_ms_cli.egg-info}/PKG-INFO +1 -1
  4. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosyste_ms_cli.egg-info/SOURCES.txt +4 -0
  5. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/__init__.py +1 -1
  6. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/cli.py +1 -103
  7. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/advisories.py +21 -38
  8. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/decorators.py +77 -0
  9. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/dependabot.py +16 -0
  10. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/diff.py +58 -0
  11. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/execution.py +49 -34
  12. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/generator.py +24 -20
  13. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/advisories.py +17 -0
  14. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/archives.py +13 -0
  15. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/base.py +92 -0
  16. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/commits.py +15 -0
  17. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/default.py +14 -0
  18. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/dependabot.py +29 -0
  19. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/diff.py +16 -0
  20. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/docker.py +22 -0
  21. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/issues.py +26 -0
  22. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/licenses.py +16 -0
  23. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/opencollective.py +18 -0
  24. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/handlers/packages.py +0 -61
  25. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/parser.py +16 -0
  26. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/repos.py +38 -0
  27. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/resolve.py +17 -0
  28. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/sbom.py +16 -0
  29. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/sponsors.py +13 -0
  30. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/summary.py +18 -0
  31. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/handlers/timeline.py +16 -0
  32. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/licenses.py +55 -0
  33. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/packages.py +51 -0
  34. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/parser.py +55 -0
  35. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/purl_options.py +82 -0
  36. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/repos.py +20 -0
  37. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/resolve.py +66 -0
  38. ecosyste_ms_cli-1.4.2/ecosystems_cli/commands/sbom.py +55 -0
  39. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/constants.py +3 -0
  40. ecosyste_ms_cli-1.4.2/ecosystems_cli/helpers/build_kwargs.py +17 -0
  41. ecosyste_ms_cli-1.4.2/ecosystems_cli/helpers/click_params.py +97 -0
  42. ecosyste_ms_cli-1.4.2/ecosystems_cli/helpers/job_polling.py +149 -0
  43. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/print_output.py +31 -12
  44. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/purl_parser.py +39 -1
  45. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/mcp_server.py +77 -61
  46. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/openapi_client.py +15 -3
  47. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/pyproject.toml +1 -1
  48. ecosyste_ms_cli-1.4.2/tests/test_handlers.py +57 -0
  49. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_mcp_server.py +100 -31
  50. ecosyste_ms_cli-1.4.2/tests/test_openapi_client.py +421 -0
  51. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_print_output.py +51 -0
  52. ecosyste_ms_cli-1.4.0/CHANGELOG.md +0 -7
  53. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/decorators.py +0 -101
  54. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/dependabot.py +0 -5
  55. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/diff.py +0 -144
  56. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/advisories.py +0 -73
  57. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/archives.py +0 -40
  58. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/base.py +0 -38
  59. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/commits.py +0 -76
  60. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/default.py +0 -40
  61. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/dependabot.py +0 -205
  62. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/diff.py +0 -72
  63. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/docker.py +0 -142
  64. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/issues.py +0 -87
  65. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/licenses.py +0 -52
  66. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/opencollective.py +0 -86
  67. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/parser.py +0 -57
  68. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/repos.py +0 -97
  69. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/resolve.py +0 -68
  70. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/sbom.py +0 -52
  71. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/sponsors.py +0 -52
  72. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/summary.py +0 -81
  73. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/handlers/timeline.py +0 -45
  74. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/licenses.py +0 -135
  75. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/packages.py +0 -151
  76. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/parser.py +0 -135
  77. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/repos.py +0 -5
  78. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/resolve.py +0 -160
  79. ecosyste_ms_cli-1.4.0/ecosystems_cli/commands/sbom.py +0 -135
  80. ecosyste_ms_cli-1.4.0/ecosystems_cli/helpers/click_params.py +0 -49
  81. ecosyste_ms_cli-1.4.0/tests/test_openapi_client.py +0 -187
  82. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/LICENSE +0 -0
  83. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/MANIFEST.in +0 -0
  84. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/Readme.md +0 -0
  85. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosyste_ms_cli.egg-info/dependency_links.txt +0 -0
  86. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosyste_ms_cli.egg-info/entry_points.txt +0 -0
  87. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosyste_ms_cli.egg-info/requires.txt +0 -0
  88. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosyste_ms_cli.egg-info/top_level.txt +0 -0
  89. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/__main__.py +0 -0
  90. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/__init__.py +0 -0
  91. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/advisories.openapi.yaml +0 -0
  92. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/archives.openapi.yaml +0 -0
  93. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/commits.openapi.yaml +0 -0
  94. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/dependabot.openapi.yaml +0 -0
  95. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/diff.openapi.yaml +0 -0
  96. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/docker.openapi.yaml +0 -0
  97. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/issues.openapi.yaml +0 -0
  98. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/licenses.openapi.yaml +0 -0
  99. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/opencollective.openapi.yaml +0 -0
  100. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/packages.openapi.yaml +0 -0
  101. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/parser.openapi.yaml +0 -0
  102. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/registries.yaml +0 -0
  103. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/repos.openapi.yaml +0 -0
  104. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/resolve.openapi.yaml +0 -0
  105. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/sbom.openapi.yaml +0 -0
  106. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/sponsors.openapi.yaml +0 -0
  107. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/summary.openapi.yaml +0 -0
  108. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/apis/timeline.openapi.yaml +0 -0
  109. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/__init__.py +0 -0
  110. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/archives.py +0 -0
  111. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/commits.py +0 -0
  112. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/docker.py +0 -0
  113. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/handlers/__init__.py +0 -0
  114. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/handlers/factory.py +0 -0
  115. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/issues.py +0 -0
  116. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/mcp.py +0 -0
  117. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/opencollective.py +0 -0
  118. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/sponsors.py +0 -0
  119. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/summary.py +0 -0
  120. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/commands/timeline.py +0 -0
  121. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/exceptions.py +0 -0
  122. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/flatten_dict.py +0 -0
  123. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/format_value.py +0 -0
  124. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/get_domain.py +0 -0
  125. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/load_api_spec.py +0 -0
  126. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/print_error.py +0 -0
  127. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/ecosystems_cli/helpers/print_operations.py +0 -0
  128. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/setup.cfg +0 -0
  129. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_cli.py +0 -0
  130. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_cli_domain.py +0 -0
  131. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_constants.py +0 -0
  132. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_format_value.py +0 -0
  133. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_help_output.py +0 -0
  134. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_load_api_spec.py +0 -0
  135. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_package_metadata.py +0 -0
  136. {ecosyste_ms_cli-1.4.0 → ecosyste_ms_cli-1.4.2}/tests/test_print_operations.py +0 -0
@@ -0,0 +1,21 @@
1
+ # Changelog for v1.4.2
2
+
3
+ ## Changes since v1.4.1
4
+
5
+ - refactor: call_tool tests (42a5ae5)
6
+ - refactor: sleep handling in polling (6566f5e)
7
+ - fix: date putput in json (9d87d5e)
8
+ - fix: Non-dict list responses crash table/TSV output. (cf09255)
9
+ - refactor: merge purl precedence (31bc8b8)
10
+ - refactor: merge purl precedence code (a19038b)
11
+ - refactor: archives handler simplified (51ec6cc)
12
+ - refactor: Unify the three `--purl` mechanisms. (86cc871)
13
+ - refactor: Delete `DefaultOperationHandler`'s stale config (1826703)
14
+ - refactor: add tests for openapi (5597b66)
15
+ - fix: passed body to request (159ecb7)
16
+ - refactor: several fixes\ \ * JSONL/TSV wrapping error fixed\ * poll loop with timeout\ * mcp events improved \ * default --max-wait of 600s \ (29f1b8d)
17
+ - refactor: the big clean\ \ * remove dead code\ * sync experimental mcp with all apis\ * handler consolidation completed\ * polling simplified and less duplicated code\ * packages.py --purl cleanup (a1b8915)
18
+ - refactor: replace_auto_command and Lift `_extract_param` to `handlers/base.py` (4171d09)
19
+ - refactor: polling (3a77384)
20
+ - refactor: star maintianing a build kwargs method (f12e930)
21
+ - refactor: Appending the apply_purl helper (d2a0eb0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecosyste_ms_cli
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: CLI for ecosyste.ms API
5
5
  Author-email: Sebastian Schürmann <sebs@2xs.org>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ecosyste_ms_cli
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: CLI for ecosyste.ms API
5
5
  Author-email: Sebastian Schürmann <sebs@2xs.org>
6
6
  License-Expression: MIT
@@ -51,6 +51,7 @@ ecosystems_cli/commands/mcp.py
51
51
  ecosystems_cli/commands/opencollective.py
52
52
  ecosystems_cli/commands/packages.py
53
53
  ecosystems_cli/commands/parser.py
54
+ ecosystems_cli/commands/purl_options.py
54
55
  ecosystems_cli/commands/repos.py
55
56
  ecosystems_cli/commands/resolve.py
56
57
  ecosystems_cli/commands/sbom.py
@@ -78,10 +79,12 @@ ecosystems_cli/commands/handlers/sbom.py
78
79
  ecosystems_cli/commands/handlers/sponsors.py
79
80
  ecosystems_cli/commands/handlers/summary.py
80
81
  ecosystems_cli/commands/handlers/timeline.py
82
+ ecosystems_cli/helpers/build_kwargs.py
81
83
  ecosystems_cli/helpers/click_params.py
82
84
  ecosystems_cli/helpers/flatten_dict.py
83
85
  ecosystems_cli/helpers/format_value.py
84
86
  ecosystems_cli/helpers/get_domain.py
87
+ ecosystems_cli/helpers/job_polling.py
85
88
  ecosystems_cli/helpers/load_api_spec.py
86
89
  ecosystems_cli/helpers/print_error.py
87
90
  ecosystems_cli/helpers/print_operations.py
@@ -91,6 +94,7 @@ tests/test_cli.py
91
94
  tests/test_cli_domain.py
92
95
  tests/test_constants.py
93
96
  tests/test_format_value.py
97
+ tests/test_handlers.py
94
98
  tests/test_help_output.py
95
99
  tests/test_load_api_spec.py
96
100
  tests/test_mcp_server.py
@@ -1,3 +1,3 @@
1
1
  """Ecosystems CLI tool for interacting with ecosyste.ms APIs."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.4.2"
@@ -1,11 +1,6 @@
1
1
  """Command line interface for ecosystems CLI."""
2
2
 
3
- import json
4
- from typing import Any, Dict, List, Optional
5
-
6
3
  import click
7
- from rich.console import Console
8
- from rich.panel import Panel
9
4
 
10
5
  from ecosystems_cli import __version__
11
6
  from ecosystems_cli.commands.advisories import advisories
@@ -29,18 +24,8 @@ from ecosystems_cli.commands.timeline import timeline
29
24
  from ecosystems_cli.constants import (
30
25
  DEFAULT_OUTPUT_FORMAT,
31
26
  DEFAULT_TIMEOUT,
32
- ERROR_PANEL_STYLE,
33
- ERROR_PREFIX,
34
27
  OUTPUT_FORMATS,
35
28
  )
36
- from ecosystems_cli.exceptions import EcosystemsCLIError, JSONParseError
37
- from ecosystems_cli.helpers.format_value import format_value
38
- from ecosystems_cli.helpers.get_domain import build_base_url, get_domain_with_precedence
39
- from ecosystems_cli.helpers.print_operations import print_operations
40
- from ecosystems_cli.helpers.print_output import print_output
41
- from ecosystems_cli.openapi_client import _factory as api_factory
42
-
43
- console = Console()
44
29
 
45
30
 
46
31
  @click.group(invoke_without_command=True)
@@ -97,20 +82,8 @@ def main(ctx, timeout, format, domain, mailto, install_completion):
97
82
  ctx.obj["mailto"] = mailto
98
83
 
99
84
 
100
- # Command Registration Strategy:
101
- # We use a hybrid approach for command registration to balance usability and flexibility:
102
- #
103
- # 1. High-level API commands (e.g., 'ecosystems repos', 'ecosystems packages')
104
- # - Registered via COMMAND_REGISTRY below
105
- # - Provide user-friendly commands with custom logic and convenience methods
106
- # - Each command is a BaseCommand subclass with tailored functionality
107
- #
108
- # 2. Low-level operation commands (e.g., 'ecosystems op repos get_topic')
109
- # - Registered dynamically in register_op_commands()
110
- # - Provide direct access to all API operations
111
- # - Auto-generated from OpenAPI specifications
112
-
113
85
  # Command registry - maps API names to their command instances
86
+ # (each value is a Click group generated from an OpenAPI spec).
114
87
  COMMAND_REGISTRY = {
115
88
  "advisories": advisories,
116
89
  "archives": archives,
@@ -137,84 +110,9 @@ for api_name, command in COMMAND_REGISTRY.items():
137
110
 
138
111
  # Register MCP server command
139
112
  main.add_command(mcp)
140
- # Dynamic op commands have been removed
141
113
 
142
114
 
143
115
  @main.command(name="version", help="Show the ecosystems CLI version and exit.")
144
116
  def version():
145
117
  """Print the installed ecosystems CLI version."""
146
118
  click.echo(__version__)
147
-
148
-
149
- def _parse_json_param(param: Optional[str]) -> Optional[Dict]:
150
- """Parse JSON parameter if provided."""
151
- if not param:
152
- return None
153
- try:
154
- return json.loads(param)
155
- except json.JSONDecodeError as e:
156
- raise JSONParseError(f"Invalid JSON: {param}. Error: {str(e)}")
157
-
158
-
159
- def _call_operation(api: str, operation: str, path_params: str, query_params: str, body: str, context=None):
160
- """Call an operation on the specified API."""
161
- # Get timeout, format, and domain from context if available
162
- if context and hasattr(context, "obj"):
163
- timeout = context.obj.get("timeout", DEFAULT_TIMEOUT)
164
- format_type = context.obj.get("format", DEFAULT_OUTPUT_FORMAT)
165
- domain = context.obj.get("domain")
166
- else:
167
- timeout = DEFAULT_TIMEOUT
168
- format_type = DEFAULT_OUTPUT_FORMAT
169
- domain = None
170
-
171
- # Get domain with proper precedence
172
- final_domain = get_domain_with_precedence(api, domain)
173
- base_url = build_base_url(final_domain, api)
174
-
175
- try:
176
- # Parse parameters
177
- path_params_dict = _parse_json_param(path_params)
178
- query_params_dict = _parse_json_param(query_params)
179
- body_dict = _parse_json_param(body)
180
-
181
- result = api_factory.call(
182
- api_name=api,
183
- operation_id=operation,
184
- path_params=path_params_dict,
185
- query_params=query_params_dict,
186
- body=body_dict,
187
- timeout=timeout,
188
- base_url=base_url,
189
- )
190
- _print_output(result, format_type)
191
- except JSONParseError as e:
192
- _print_error(str(e))
193
- except EcosystemsCLIError as e:
194
- _print_error(str(e))
195
- except Exception as e:
196
- _print_error(f"Unexpected error: {str(e)}")
197
-
198
-
199
- def _print_output(data: Any, format_type: str = "table"):
200
- """Print data in the specified format."""
201
- print_output(data, format_type, console=console)
202
-
203
-
204
- def _format_value(value: Any) -> str:
205
- """Deprecated: use format_value from helpers.format_value instead."""
206
- return format_value(value)
207
-
208
-
209
- def _print_json(data: Any):
210
- """Print JSON data in a nicely formatted way."""
211
- _print_output(data, "json")
212
-
213
-
214
- def _print_error(error_msg: str):
215
- """Print error message in a nicely formatted way."""
216
- console.print(Panel(f"{ERROR_PREFIX} {error_msg}", border_style=ERROR_PANEL_STYLE))
217
-
218
-
219
- def _print_operations(operations: List[Dict]):
220
- print_operations(operations, console=console)
@@ -4,20 +4,16 @@ from typing import Optional
4
4
 
5
5
  import click
6
6
 
7
- from ecosystems_cli.commands.decorators import common_options
7
+ from ecosystems_cli.commands.decorators import common_options, override_auto_command
8
8
  from ecosystems_cli.commands.execution import execute_api_call, update_context
9
9
  from ecosystems_cli.commands.generator import APICommandGenerator
10
- from ecosystems_cli.helpers.purl_parser import parse_purl
10
+ from ecosystems_cli.helpers.build_kwargs import build_kwargs
11
+ from ecosystems_cli.helpers.purl_parser import apply_purl
11
12
 
12
13
  advisories = APICommandGenerator.create_api_group("advisories")
13
14
 
14
15
 
15
- # Remove auto-generated get_advisories command to replace with custom implementation
16
- if "get_advisories" in advisories.commands:
17
- del advisories.commands["get_advisories"]
18
-
19
-
20
- @advisories.command(name="get_advisories", help="list advisories")
16
+ @override_auto_command(advisories, "get_advisories", help="list advisories")
21
17
  @click.option("--purl", type=str, default=None, help="Package URL (PURL). Example: pkg:npm/fsa")
22
18
  @click.option("--ecosystem", type=str, default=None, help="Ecosystem to filter by")
23
19
  @click.option("--package-name", type=str, default=None, help="Package to filter by")
@@ -73,37 +69,24 @@ def get_advisories(
73
69
  """
74
70
  update_context(ctx, timeout, format, domain, mailto)
75
71
 
76
- # If PURL is provided, parse it and override ecosystem/package_name
77
- if purl:
78
- parsed_ecosystem, parsed_package_name = parse_purl(purl)
79
- if parsed_ecosystem:
80
- ecosystem = parsed_ecosystem
81
- if parsed_package_name:
82
- package_name = parsed_package_name
72
+ # Explicit flags win over PURL-derived values.
73
+ parsed = apply_purl(purl)
74
+ ecosystem = ecosystem or parsed.get("ecosystem")
75
+ package_name = package_name or parsed.get("package_name")
83
76
 
84
77
  # Build kwargs for the API call
85
- kwargs = {}
86
- if ecosystem is not None:
87
- kwargs["ecosystem"] = ecosystem
88
- if package_name is not None:
89
- kwargs["package_name"] = package_name
90
- if severity is not None:
91
- kwargs["severity"] = severity
92
- if repository_url is not None:
93
- kwargs["repository_url"] = repository_url
94
- if page is not None:
95
- kwargs["page"] = page
96
- if per_page is not None:
97
- kwargs["per_page"] = per_page
98
- if created_after is not None:
99
- kwargs["created_after"] = created_after
100
- if updated_after is not None:
101
- kwargs["updated_after"] = updated_after
102
- if sort is not None:
103
- kwargs["sort"] = sort
104
- if order is not None:
105
- kwargs["order"] = order
106
- if source is not None:
107
- kwargs["source"] = source
78
+ kwargs = build_kwargs(
79
+ ecosystem=ecosystem,
80
+ package_name=package_name,
81
+ severity=severity,
82
+ repository_url=repository_url,
83
+ page=page,
84
+ per_page=per_page,
85
+ created_after=created_after,
86
+ updated_after=updated_after,
87
+ sort=sort,
88
+ order=order,
89
+ source=source,
90
+ )
108
91
 
109
92
  execute_api_call(ctx, "advisories", operation_id="getAdvisories", call_args=(), call_kwargs=kwargs)
@@ -0,0 +1,77 @@
1
+ """Decorators for ecosystems CLI commands."""
2
+
3
+ from functools import wraps
4
+
5
+ import click
6
+
7
+ from ecosystems_cli.constants import (
8
+ DEFAULT_OUTPUT_FORMAT,
9
+ DEFAULT_TIMEOUT,
10
+ OUTPUT_FORMATS,
11
+ )
12
+
13
+
14
+ def common_options(f):
15
+ """Decorator to add common options to commands."""
16
+ f = click.option(
17
+ "--format",
18
+ default=DEFAULT_OUTPUT_FORMAT,
19
+ type=click.Choice(OUTPUT_FORMATS),
20
+ help=f"Output format. Default is {DEFAULT_OUTPUT_FORMAT}.",
21
+ )(f)
22
+ f = click.option(
23
+ "--timeout",
24
+ default=DEFAULT_TIMEOUT,
25
+ help=f"Timeout in seconds for API requests. Default is {DEFAULT_TIMEOUT} seconds.",
26
+ )(f)
27
+ f = click.option(
28
+ "--domain",
29
+ default=None,
30
+ help="Override the API domain. Example: api.example.com",
31
+ )(f)
32
+ f = click.option(
33
+ "--mailto",
34
+ default=None,
35
+ help="Email address for polite pool access. Example: you@example.com",
36
+ )(f)
37
+ return f
38
+
39
+
40
+ def override_auto_command(group: click.Group, name: str, **command_kwargs):
41
+ """Register a custom command in place of an auto-generated one.
42
+
43
+ Drops any command already registered on ``group`` under ``name`` (the
44
+ auto-generated version), then registers the decorated function via
45
+ ``group.command(name=name, **command_kwargs)``. Use this as the outermost
46
+ decorator instead of a bare ``del group.commands[name]`` followed by
47
+ ``@group.command(name=name, ...)``.
48
+ """
49
+
50
+ def decorator(func):
51
+ group.commands.pop(name, None)
52
+ return group.command(name=name, **command_kwargs)(func)
53
+
54
+ return decorator
55
+
56
+
57
+ def api_command(api_name: str, operation_id: str):
58
+ """Decorator that wraps a command to execute an API operation.
59
+
60
+ Args:
61
+ api_name: Name of the API (e.g., 'repos', 'packages')
62
+ operation_id: Operation ID to execute
63
+ """
64
+
65
+ def decorator(func):
66
+ @common_options
67
+ @click.pass_context
68
+ @wraps(func)
69
+ def wrapper(ctx, timeout, format, domain, mailto, *args, **kwargs):
70
+ from ecosystems_cli.commands.execution import execute_api_call, update_context
71
+
72
+ update_context(ctx, timeout, format, domain, mailto)
73
+ execute_api_call(ctx, api_name, operation_id=operation_id, call_args=args, call_kwargs=kwargs)
74
+
75
+ return wrapper
76
+
77
+ return decorator
@@ -0,0 +1,16 @@
1
+ """Commands for the dependabot API."""
2
+
3
+ from ecosystems_cli.commands.generator import APICommandGenerator
4
+ from ecosystems_cli.commands.purl_options import attach_purl_option
5
+
6
+ dependabot = APICommandGenerator.create_api_group("dependabot")
7
+
8
+ # getAdvisories filters by ecosystem name and package_name (query options), so the
9
+ # PURL type is passed through unmapped and both parts stay optional filters.
10
+ attach_purl_option(
11
+ dependabot,
12
+ "get_advisories",
13
+ targets=("ecosystem", "package_name"),
14
+ map_types=False,
15
+ example="pkg:npm/fsa. Decomposes into --ecosystem and --package-name.",
16
+ )
@@ -0,0 +1,58 @@
1
+ """Commands for the diff API."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+
7
+ from ecosystems_cli.commands.decorators import common_options, override_auto_command
8
+ from ecosystems_cli.commands.execution import update_context
9
+ from ecosystems_cli.commands.generator import APICommandGenerator
10
+ from ecosystems_cli.constants import DEFAULT_MAX_POLL_WAIT
11
+ from ecosystems_cli.helpers.job_polling import submit_and_poll
12
+
13
+ diff = APICommandGenerator.create_api_group("diff")
14
+
15
+
16
+ @override_auto_command(diff, "create_job", help="Submit a diff job with two URLs to compare")
17
+ @click.argument("url_1", required=True)
18
+ @click.argument("url_2", required=True)
19
+ @click.option(
20
+ "--polling-interval",
21
+ type=float,
22
+ default=None,
23
+ help="Polling interval in seconds. If set, the command will poll the job status until completion.",
24
+ )
25
+ @click.option(
26
+ "--max-wait",
27
+ type=float,
28
+ default=DEFAULT_MAX_POLL_WAIT,
29
+ help=f"Maximum seconds to poll before giving up. Default is {DEFAULT_MAX_POLL_WAIT}.",
30
+ )
31
+ @common_options
32
+ @click.pass_context
33
+ def create_job(
34
+ ctx,
35
+ timeout: int,
36
+ format: str,
37
+ domain: Optional[str],
38
+ mailto: Optional[str],
39
+ url_1: str,
40
+ url_2: str,
41
+ polling_interval: Optional[float],
42
+ max_wait: float,
43
+ ):
44
+ """Submit a diff job with two URLs to compare.
45
+
46
+ Args:
47
+ ctx: Click context
48
+ timeout: Request timeout
49
+ format: Output format
50
+ domain: API domain
51
+ mailto: Email for polite pool access
52
+ url_1: First URL of file or zip/tar archive
53
+ url_2: Second URL of file or zip/tar archive
54
+ polling_interval: Optional polling interval in seconds
55
+ max_wait: Maximum seconds to poll before giving up
56
+ """
57
+ update_context(ctx, timeout, format, domain, mailto)
58
+ submit_and_poll(ctx, "diff", {"url_1": url_1, "url_2": url_2}, polling_interval=polling_interval, max_wait=max_wait)
@@ -16,7 +16,14 @@ console = Console()
16
16
 
17
17
 
18
18
  def update_context(ctx, timeout: int, format: str, domain: Optional[str], mailto: Optional[str] = None):
19
- """Update context with command-level options if they differ from defaults.
19
+ """Merge command-level options into ``ctx.obj``.
20
+
21
+ The single context-precedence mechanism, used by both the API group callback
22
+ and every leaf command. An option is written only when it differs from its
23
+ default (i.e. was explicitly set at this level); otherwise the value already
24
+ in ``ctx.obj`` -- inherited from the parent context -- is left untouched.
25
+ The net precedence is therefore: leaf option > group option > root option >
26
+ default.
20
27
 
21
28
  Args:
22
29
  ctx: Click context
@@ -36,61 +43,69 @@ def update_context(ctx, timeout: int, format: str, domain: Optional[str], mailto
36
43
  ctx.obj["mailto"] = mailto
37
44
 
38
45
 
46
+ def _extract_body(call_kwargs: dict, body_keys: Optional[list]) -> dict:
47
+ """Pull request-body fields out of ``call_kwargs`` into a JSON body dict.
48
+
49
+ Removes each ``body_keys`` entry from ``call_kwargs`` (mutating it) so the
50
+ remaining kwargs map cleanly to path/query parameters. Unset values are
51
+ dropped; Click's repeatable options arrive as tuples and are normalized to
52
+ lists so they serialize as JSON arrays.
53
+ """
54
+ body: dict = {}
55
+ for key in body_keys or []:
56
+ value = call_kwargs.pop(key, None)
57
+ if value is None or value == ():
58
+ continue
59
+ body[key] = list(value) if isinstance(value, tuple) else value
60
+ return body
61
+
62
+
39
63
  def execute_api_call(
40
64
  ctx,
41
65
  api_name: str,
42
- method_name: Optional[str] = None,
43
- operation_id: Optional[str] = None,
66
+ operation_id: str,
44
67
  call_args: tuple = (),
45
68
  call_kwargs: Optional[dict] = None,
69
+ body_keys: Optional[list] = None,
46
70
  ):
47
71
  """Execute an API call with proper error handling.
48
72
 
49
73
  Args:
50
74
  ctx: Click context
51
75
  api_name: Name of the API (e.g., 'repos', 'packages')
52
- method_name: API client method name (for direct method calls)
53
- operation_id: Operation ID (for call method)
76
+ operation_id: Operation ID to call
54
77
  call_args: Positional arguments for the API call
55
78
  call_kwargs: Keyword arguments for the API call
79
+ body_keys: Names of kwargs that belong in the JSON request body rather
80
+ than the path/query parameters
56
81
  """
57
82
  if call_kwargs is None:
58
83
  call_kwargs = {}
59
84
 
85
+ # Separate request-body fields before path/query mapping.
86
+ body = _extract_body(call_kwargs, body_keys)
87
+
60
88
  # Get domain with proper precedence
61
89
  domain = get_domain_with_precedence(api_name, ctx.obj.get("domain"))
62
90
  base_url = build_base_url(domain, api_name)
63
91
 
64
92
  try:
65
- if operation_id:
66
- if call_args or call_kwargs:
67
- # Use operation handler to build parameters
68
- handler = OperationHandlerFactory.get_handler(api_name)
69
- path_params, query_params = handler.build_params(operation_id, call_args, call_kwargs)
70
- result = api_factory.call(
71
- api_name,
72
- operation_id,
73
- path_params=path_params,
74
- query_params=query_params,
75
- timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
76
- mailto=ctx.obj.get("mailto"),
77
- base_url=base_url,
78
- )
79
- else:
80
- # Simple operation call without parameters
81
- result = api_factory.call(
82
- api_name,
83
- operation_id,
84
- path_params={},
85
- query_params={},
86
- timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
87
- mailto=ctx.obj.get("mailto"),
88
- base_url=base_url,
89
- )
90
- elif method_name:
91
- raise ValueError("Direct method calls not supported")
92
- else:
93
- raise ValueError("Either method_name or operation_id must be provided")
93
+ handler = OperationHandlerFactory.get_handler(api_name)
94
+ path_params, query_params = handler.build_params(operation_id, call_args, call_kwargs)
95
+
96
+ call_params = {
97
+ "path_params": path_params,
98
+ "query_params": query_params,
99
+ "timeout": ctx.obj.get("timeout", DEFAULT_TIMEOUT),
100
+ "mailto": ctx.obj.get("mailto"),
101
+ "base_url": base_url,
102
+ }
103
+ # Only send a body when there is one, so body-less calls keep their
104
+ # existing call signature.
105
+ if body:
106
+ call_params["body"] = body
107
+
108
+ result = api_factory.call(api_name, operation_id, **call_params)
94
109
 
95
110
  print_output(result, ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT), console=console)
96
111
  except EcosystemsCLIError as e:
@@ -5,9 +5,8 @@ from typing import List
5
5
 
6
6
  import click
7
7
 
8
- from ecosystems_cli.commands.decorators import common_options, resolve_context_value
9
- from ecosystems_cli.constants import DEFAULT_OUTPUT_FORMAT, DEFAULT_TIMEOUT
10
- from ecosystems_cli.helpers.click_params import build_click_decorators
8
+ from ecosystems_cli.commands.decorators import common_options
9
+ from ecosystems_cli.helpers.click_params import build_body_decorators, build_click_decorators
11
10
  from ecosystems_cli.helpers.load_api_spec import load_api_spec
12
11
 
13
12
 
@@ -40,17 +39,11 @@ class APICommandGenerator:
40
39
  @click.pass_context
41
40
  def api_group(ctx, timeout, format, domain, mailto):
42
41
  f"""Commands for the {api_name} API."""
43
- ctx.ensure_object(dict)
42
+ from ecosystems_cli.commands.execution import update_context
44
43
 
45
- timeout = resolve_context_value(ctx, "timeout", timeout, DEFAULT_TIMEOUT)
46
- format = resolve_context_value(ctx, "format", format, DEFAULT_OUTPUT_FORMAT)
47
- domain = resolve_context_value(ctx, "domain", domain, None)
48
- mailto = resolve_context_value(ctx, "mailto", mailto, None)
49
-
50
- ctx.obj["timeout"] = timeout
51
- ctx.obj["format"] = format
52
- ctx.obj["domain"] = domain
53
- ctx.obj["mailto"] = mailto
44
+ # Same merge rule as leaf commands: a value set at this level wins;
45
+ # otherwise the value inherited from the parent context is kept.
46
+ update_context(ctx, timeout, format, domain, mailto)
54
47
 
55
48
  api_group.name = api_name
56
49
  return api_group
@@ -70,12 +63,13 @@ class APICommandGenerator:
70
63
  command_name = APICommandGenerator.operation_id_to_command_name(operation_id)
71
64
  description = operation.get("summary", f"Execute {operation_id}")
72
65
  parameters = operation.get("parameters", [])
66
+ request_body = operation.get("requestBody")
73
67
 
74
- if not parameters:
68
+ if not parameters and not request_body:
75
69
  APICommandGenerator._create_simple_command(api_group, command_name, description, api_name, operation_id)
76
70
  else:
77
71
  APICommandGenerator._create_parameterized_command(
78
- api_group, command_name, description, api_name, operation_id, parameters
72
+ api_group, command_name, description, api_name, operation_id, parameters, request_body
79
73
  )
80
74
 
81
75
  @staticmethod
@@ -90,12 +84,22 @@ class APICommandGenerator:
90
84
 
91
85
  @staticmethod
92
86
  def _create_parameterized_command(
93
- api_group: click.Group, command_name: str, description: str, api_name: str, operation_id: str, parameters: List[dict]
87
+ api_group: click.Group,
88
+ command_name: str,
89
+ description: str,
90
+ api_name: str,
91
+ operation_id: str,
92
+ parameters: List[dict],
93
+ request_body: dict = None,
94
94
  ):
95
- """Create a command with parameters."""
95
+ """Create a command with parameters and/or a JSON request body."""
96
96
  click_decorators = APICommandGenerator._build_click_decorators(parameters)
97
+ body_params: List[str] = []
98
+ if request_body:
99
+ body_decorators, body_params = build_body_decorators(request_body)
100
+ click_decorators = click_decorators + body_decorators
97
101
 
98
- def make_command(op_id):
102
+ def make_command(op_id, body_keys):
99
103
  @api_group.command(name=command_name, help=description)
100
104
  @common_options
101
105
  @click.pass_context
@@ -103,14 +107,14 @@ class APICommandGenerator:
103
107
  from ecosystems_cli.commands.execution import execute_api_call, update_context
104
108
 
105
109
  update_context(ctx, timeout, format, domain, mailto)
106
- execute_api_call(ctx, api_name, operation_id=op_id, call_args=args, call_kwargs=kwargs)
110
+ execute_api_call(ctx, api_name, operation_id=op_id, call_args=args, call_kwargs=kwargs, body_keys=body_keys)
107
111
 
108
112
  for decorator in reversed(click_decorators):
109
113
  command_impl = decorator(command_impl)
110
114
 
111
115
  return command_impl
112
116
 
113
- make_command(operation_id)
117
+ make_command(operation_id, body_params)
114
118
 
115
119
  @staticmethod
116
120
  def create_api_group(api_name: str) -> click.Group: