ecosyste-ms-cli 1.3.2__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.
- ecosyste_ms_cli-1.3.2.dist-info/METADATA +133 -0
- ecosyste_ms_cli-1.3.2.dist-info/RECORD +84 -0
- ecosyste_ms_cli-1.3.2.dist-info/WHEEL +5 -0
- ecosyste_ms_cli-1.3.2.dist-info/entry_points.txt +2 -0
- ecosyste_ms_cli-1.3.2.dist-info/licenses/LICENSE +21 -0
- ecosyste_ms_cli-1.3.2.dist-info/top_level.txt +1 -0
- ecosystems_cli/__init__.py +3 -0
- ecosystems_cli/__main__.py +4 -0
- ecosystems_cli/apis/__init__.py +0 -0
- ecosystems_cli/apis/advisories.openapi.yaml +347 -0
- ecosystems_cli/apis/archives.openapi.yaml +193 -0
- ecosystems_cli/apis/commits.openapi.yaml +391 -0
- ecosystems_cli/apis/dependabot.openapi.yaml +887 -0
- ecosystems_cli/apis/diff.openapi.yaml +90 -0
- ecosystems_cli/apis/docker.openapi.yaml +534 -0
- ecosystems_cli/apis/issues.openapi.yaml +839 -0
- ecosystems_cli/apis/licenses.openapi.yaml +80 -0
- ecosystems_cli/apis/opencollective.openapi.yaml +247 -0
- ecosystems_cli/apis/packages.openapi.yaml +2522 -0
- ecosystems_cli/apis/parser.openapi.yaml +97 -0
- ecosystems_cli/apis/registries.yaml +155 -0
- ecosystems_cli/apis/repos.openapi.yaml +1521 -0
- ecosystems_cli/apis/resolve.openapi.yaml +130 -0
- ecosystems_cli/apis/sbom.openapi.yaml +79 -0
- ecosystems_cli/apis/sponsors.openapi.yaml +283 -0
- ecosystems_cli/apis/summary.openapi.yaml +239 -0
- ecosystems_cli/apis/timeline.openapi.yaml +91 -0
- ecosystems_cli/cli.py +213 -0
- ecosystems_cli/commands/__init__.py +1 -0
- ecosystems_cli/commands/advisories.py +109 -0
- ecosystems_cli/commands/archives.py +5 -0
- ecosystems_cli/commands/commits.py +5 -0
- ecosystems_cli/commands/decorators.py +101 -0
- ecosystems_cli/commands/dependabot.py +5 -0
- ecosystems_cli/commands/diff.py +144 -0
- ecosystems_cli/commands/docker.py +5 -0
- ecosystems_cli/commands/execution.py +99 -0
- ecosystems_cli/commands/generator.py +127 -0
- ecosystems_cli/commands/handlers/__init__.py +45 -0
- ecosystems_cli/commands/handlers/advisories.py +73 -0
- ecosystems_cli/commands/handlers/archives.py +40 -0
- ecosystems_cli/commands/handlers/base.py +38 -0
- ecosystems_cli/commands/handlers/commits.py +76 -0
- ecosystems_cli/commands/handlers/default.py +40 -0
- ecosystems_cli/commands/handlers/dependabot.py +205 -0
- ecosystems_cli/commands/handlers/diff.py +72 -0
- ecosystems_cli/commands/handlers/docker.py +142 -0
- ecosystems_cli/commands/handlers/factory.py +60 -0
- ecosystems_cli/commands/handlers/issues.py +87 -0
- ecosystems_cli/commands/handlers/licenses.py +52 -0
- ecosystems_cli/commands/handlers/opencollective.py +86 -0
- ecosystems_cli/commands/handlers/packages.py +103 -0
- ecosystems_cli/commands/handlers/parser.py +57 -0
- ecosystems_cli/commands/handlers/repos.py +97 -0
- ecosystems_cli/commands/handlers/resolve.py +68 -0
- ecosystems_cli/commands/handlers/sbom.py +52 -0
- ecosystems_cli/commands/handlers/sponsors.py +52 -0
- ecosystems_cli/commands/handlers/summary.py +81 -0
- ecosystems_cli/commands/handlers/timeline.py +45 -0
- ecosystems_cli/commands/issues.py +5 -0
- ecosystems_cli/commands/licenses.py +135 -0
- ecosystems_cli/commands/mcp.py +54 -0
- ecosystems_cli/commands/opencollective.py +5 -0
- ecosystems_cli/commands/packages.py +151 -0
- ecosystems_cli/commands/parser.py +135 -0
- ecosystems_cli/commands/repos.py +5 -0
- ecosystems_cli/commands/resolve.py +160 -0
- ecosystems_cli/commands/sbom.py +135 -0
- ecosystems_cli/commands/sponsors.py +5 -0
- ecosystems_cli/commands/summary.py +5 -0
- ecosystems_cli/commands/timeline.py +5 -0
- ecosystems_cli/constants.py +92 -0
- ecosystems_cli/exceptions.py +149 -0
- ecosystems_cli/helpers/click_params.py +49 -0
- ecosystems_cli/helpers/flatten_dict.py +15 -0
- ecosystems_cli/helpers/format_value.py +30 -0
- ecosystems_cli/helpers/get_domain.py +68 -0
- ecosystems_cli/helpers/load_api_spec.py +31 -0
- ecosystems_cli/helpers/print_error.py +15 -0
- ecosystems_cli/helpers/print_operations.py +73 -0
- ecosystems_cli/helpers/print_output.py +183 -0
- ecosystems_cli/helpers/purl_parser.py +121 -0
- ecosystems_cli/mcp_server.py +267 -0
- ecosystems_cli/openapi_client.py +461 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Commands for the resolve API."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from ecosystems_cli.commands.decorators import common_options
|
|
10
|
+
from ecosystems_cli.commands.execution import update_context
|
|
11
|
+
from ecosystems_cli.commands.generator import APICommandGenerator
|
|
12
|
+
from ecosystems_cli.constants import DEFAULT_OUTPUT_FORMAT, DEFAULT_TIMEOUT
|
|
13
|
+
from ecosystems_cli.exceptions import EcosystemsCLIError
|
|
14
|
+
from ecosystems_cli.helpers.get_domain import build_base_url, get_domain_with_precedence
|
|
15
|
+
from ecosystems_cli.helpers.print_error import print_error
|
|
16
|
+
from ecosystems_cli.helpers.print_output import print_output
|
|
17
|
+
from ecosystems_cli.openapi_client import _factory as api_factory
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
resolve = APICommandGenerator.create_api_group("resolve")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Remove auto-generated create_job command to replace with custom implementation
|
|
25
|
+
if "create_job" in resolve.commands:
|
|
26
|
+
del resolve.commands["create_job"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@resolve.command(name="create_job", help="Submit a resolve job")
|
|
30
|
+
@click.argument("package_name", required=True)
|
|
31
|
+
@click.argument("registry", required=True)
|
|
32
|
+
@click.option("--version", default=None, help="Resolve only with version within this range")
|
|
33
|
+
@click.option("--before", default=None, help="Resolve only with dependencies before this date")
|
|
34
|
+
@click.option(
|
|
35
|
+
"--polling-interval",
|
|
36
|
+
type=float,
|
|
37
|
+
default=None,
|
|
38
|
+
help="Polling interval in seconds. If set, the command will poll the job status until completion.",
|
|
39
|
+
)
|
|
40
|
+
@common_options
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def create_job(
|
|
43
|
+
ctx,
|
|
44
|
+
timeout: int,
|
|
45
|
+
format: str,
|
|
46
|
+
domain: Optional[str],
|
|
47
|
+
mailto: Optional[str],
|
|
48
|
+
package_name: str,
|
|
49
|
+
registry: str,
|
|
50
|
+
version: Optional[str],
|
|
51
|
+
before: Optional[str],
|
|
52
|
+
polling_interval: Optional[float],
|
|
53
|
+
):
|
|
54
|
+
"""Submit a resolve job.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
ctx: Click context
|
|
58
|
+
timeout: Request timeout
|
|
59
|
+
format: Output format
|
|
60
|
+
domain: API domain
|
|
61
|
+
mailto: Email for polite pool access
|
|
62
|
+
package_name: Name of the package
|
|
63
|
+
registry: Name of the package registry
|
|
64
|
+
version: Optional version range
|
|
65
|
+
before: Optional date to resolve dependencies before
|
|
66
|
+
polling_interval: Optional polling interval in seconds
|
|
67
|
+
"""
|
|
68
|
+
update_context(ctx, timeout, format, domain, mailto)
|
|
69
|
+
|
|
70
|
+
# Get domain with proper precedence
|
|
71
|
+
api_domain = get_domain_with_precedence("resolve", ctx.obj.get("domain"))
|
|
72
|
+
base_url = build_base_url(api_domain, "resolve")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Create the job
|
|
76
|
+
from ecosystems_cli.commands.handlers import OperationHandlerFactory
|
|
77
|
+
|
|
78
|
+
handler = OperationHandlerFactory.get_handler("resolve")
|
|
79
|
+
|
|
80
|
+
kwargs = {
|
|
81
|
+
"package_name": package_name,
|
|
82
|
+
"registry": registry,
|
|
83
|
+
}
|
|
84
|
+
if version:
|
|
85
|
+
kwargs["version"] = version
|
|
86
|
+
if before:
|
|
87
|
+
kwargs["before"] = before
|
|
88
|
+
|
|
89
|
+
path_params, query_params = handler.build_params("createJob", (), kwargs)
|
|
90
|
+
|
|
91
|
+
result = api_factory.call(
|
|
92
|
+
"resolve",
|
|
93
|
+
"createJob",
|
|
94
|
+
path_params=path_params,
|
|
95
|
+
query_params=query_params,
|
|
96
|
+
timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
|
|
97
|
+
mailto=ctx.obj.get("mailto"),
|
|
98
|
+
base_url=base_url,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# If polling is enabled, poll for job completion
|
|
102
|
+
if polling_interval is not None:
|
|
103
|
+
# Try to get job ID from direct response or from location URL
|
|
104
|
+
job_id = result.get("id")
|
|
105
|
+
|
|
106
|
+
if not job_id:
|
|
107
|
+
# Try to extract job ID from location URL
|
|
108
|
+
location = result.get("location", "")
|
|
109
|
+
if location:
|
|
110
|
+
# Extract job ID from URL like: https://resolve.ecosyste.ms/api/v1/jobs/{job_id}
|
|
111
|
+
parts = location.rstrip("/").split("/")
|
|
112
|
+
if len(parts) > 0:
|
|
113
|
+
job_id = parts[-1]
|
|
114
|
+
|
|
115
|
+
if not job_id:
|
|
116
|
+
print_error("No job ID in response, cannot poll for completion", console=console)
|
|
117
|
+
print_output(result, ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT), console=console)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Only show progress messages in interactive mode (table format)
|
|
121
|
+
output_format = ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT)
|
|
122
|
+
is_interactive = output_format == "table"
|
|
123
|
+
|
|
124
|
+
if is_interactive:
|
|
125
|
+
console.print(f"[yellow]Job created with ID: {job_id}[/yellow]")
|
|
126
|
+
console.print(f"[yellow]Polling every {polling_interval} seconds...[/yellow]")
|
|
127
|
+
|
|
128
|
+
while True:
|
|
129
|
+
time.sleep(polling_interval)
|
|
130
|
+
|
|
131
|
+
# Get job status
|
|
132
|
+
handler_get = OperationHandlerFactory.get_handler("resolve")
|
|
133
|
+
path_params_get, query_params_get = handler_get.build_params("getJob", (), {"job_id": job_id})
|
|
134
|
+
|
|
135
|
+
job_status = api_factory.call(
|
|
136
|
+
"resolve",
|
|
137
|
+
"getJob",
|
|
138
|
+
path_params=path_params_get,
|
|
139
|
+
query_params=query_params_get,
|
|
140
|
+
timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
|
|
141
|
+
mailto=ctx.obj.get("mailto"),
|
|
142
|
+
base_url=base_url,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
status = job_status.get("status", "unknown")
|
|
146
|
+
if is_interactive:
|
|
147
|
+
console.print(f"[cyan]Job status: {status}[/cyan]")
|
|
148
|
+
|
|
149
|
+
# Check if job is complete
|
|
150
|
+
if status in ["completed", "complete", "success", "failed", "error"]:
|
|
151
|
+
print_output(job_status, output_format, console=console)
|
|
152
|
+
break
|
|
153
|
+
else:
|
|
154
|
+
# No polling, just print the result
|
|
155
|
+
print_output(result, ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT), console=console)
|
|
156
|
+
|
|
157
|
+
except EcosystemsCLIError as e:
|
|
158
|
+
print_error(str(e), console=console)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print_error(f"Unexpected error: {str(e)}", console=console)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Commands for the sbom API."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from ecosystems_cli.commands.decorators import common_options
|
|
10
|
+
from ecosystems_cli.commands.execution import update_context
|
|
11
|
+
from ecosystems_cli.commands.generator import APICommandGenerator
|
|
12
|
+
from ecosystems_cli.constants import DEFAULT_OUTPUT_FORMAT, DEFAULT_TIMEOUT
|
|
13
|
+
from ecosystems_cli.exceptions import EcosystemsCLIError
|
|
14
|
+
from ecosystems_cli.helpers.get_domain import build_base_url, get_domain_with_precedence
|
|
15
|
+
from ecosystems_cli.helpers.print_error import print_error
|
|
16
|
+
from ecosystems_cli.helpers.print_output import print_output
|
|
17
|
+
from ecosystems_cli.openapi_client import _factory as api_factory
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
sbom = APICommandGenerator.create_api_group("sbom")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Remove auto-generated create_job command to replace with custom implementation
|
|
25
|
+
if "create_job" in sbom.commands:
|
|
26
|
+
del sbom.commands["create_job"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@sbom.command(name="create_job", help="Submit a dependency parsing job")
|
|
30
|
+
@click.argument("url", required=True)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--polling-interval",
|
|
33
|
+
type=float,
|
|
34
|
+
default=None,
|
|
35
|
+
help="Polling interval in seconds. If set, the command will poll the job status until completion.",
|
|
36
|
+
)
|
|
37
|
+
@common_options
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def create_job(
|
|
40
|
+
ctx, timeout: int, format: str, domain: Optional[str], mailto: Optional[str], url: str, polling_interval: Optional[float]
|
|
41
|
+
):
|
|
42
|
+
"""Submit a dependency parsing job.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
ctx: Click context
|
|
46
|
+
timeout: Request timeout
|
|
47
|
+
format: Output format
|
|
48
|
+
domain: API domain
|
|
49
|
+
mailto: Email for polite pool access
|
|
50
|
+
url: URL of file or zip/tar archive
|
|
51
|
+
polling_interval: Optional polling interval in seconds
|
|
52
|
+
"""
|
|
53
|
+
update_context(ctx, timeout, format, domain, mailto)
|
|
54
|
+
|
|
55
|
+
# Get domain with proper precedence
|
|
56
|
+
api_domain = get_domain_with_precedence("sbom", ctx.obj.get("domain"))
|
|
57
|
+
base_url = build_base_url(api_domain, "sbom")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Create the job
|
|
61
|
+
from ecosystems_cli.commands.handlers import OperationHandlerFactory
|
|
62
|
+
|
|
63
|
+
handler = OperationHandlerFactory.get_handler("sbom")
|
|
64
|
+
path_params, query_params = handler.build_params("createJob", (), {"url": url})
|
|
65
|
+
|
|
66
|
+
result = api_factory.call(
|
|
67
|
+
"sbom",
|
|
68
|
+
"createJob",
|
|
69
|
+
path_params=path_params,
|
|
70
|
+
query_params=query_params,
|
|
71
|
+
timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
|
|
72
|
+
mailto=ctx.obj.get("mailto"),
|
|
73
|
+
base_url=base_url,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# If polling is enabled, poll for job completion
|
|
77
|
+
if polling_interval is not None:
|
|
78
|
+
# Try to get job ID from direct response or from location URL
|
|
79
|
+
job_id = result.get("id")
|
|
80
|
+
|
|
81
|
+
if not job_id:
|
|
82
|
+
# Try to extract job ID from location URL
|
|
83
|
+
location = result.get("location", "")
|
|
84
|
+
if location:
|
|
85
|
+
# Extract job ID from URL like: https://sbom.ecosyste.ms/api/v1/jobs/{job_id}
|
|
86
|
+
parts = location.rstrip("/").split("/")
|
|
87
|
+
if len(parts) > 0:
|
|
88
|
+
job_id = parts[-1]
|
|
89
|
+
|
|
90
|
+
if not job_id:
|
|
91
|
+
print_error("No job ID in response, cannot poll for completion", console=console)
|
|
92
|
+
print_output(result, ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT), console=console)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Only show progress messages in interactive mode (table format)
|
|
96
|
+
output_format = ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT)
|
|
97
|
+
is_interactive = output_format == "table"
|
|
98
|
+
|
|
99
|
+
if is_interactive:
|
|
100
|
+
console.print(f"[yellow]Job created with ID: {job_id}[/yellow]")
|
|
101
|
+
console.print(f"[yellow]Polling every {polling_interval} seconds...[/yellow]")
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
time.sleep(polling_interval)
|
|
105
|
+
|
|
106
|
+
# Get job status
|
|
107
|
+
handler_get = OperationHandlerFactory.get_handler("sbom")
|
|
108
|
+
path_params_get, query_params_get = handler_get.build_params("getJob", (), {"job_id": job_id})
|
|
109
|
+
|
|
110
|
+
job_status = api_factory.call(
|
|
111
|
+
"sbom",
|
|
112
|
+
"getJob",
|
|
113
|
+
path_params=path_params_get,
|
|
114
|
+
query_params=query_params_get,
|
|
115
|
+
timeout=ctx.obj.get("timeout", DEFAULT_TIMEOUT),
|
|
116
|
+
mailto=ctx.obj.get("mailto"),
|
|
117
|
+
base_url=base_url,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
status = job_status.get("status", "unknown")
|
|
121
|
+
if is_interactive:
|
|
122
|
+
console.print(f"[cyan]Job status: {status}[/cyan]")
|
|
123
|
+
|
|
124
|
+
# Check if job is complete
|
|
125
|
+
if status in ["completed", "complete", "success", "failed", "error"]:
|
|
126
|
+
print_output(job_status, output_format, console=console)
|
|
127
|
+
break
|
|
128
|
+
else:
|
|
129
|
+
# No polling, just print the result
|
|
130
|
+
print_output(result, ctx.obj.get("format", DEFAULT_OUTPUT_FORMAT), console=console)
|
|
131
|
+
|
|
132
|
+
except EcosystemsCLIError as e:
|
|
133
|
+
print_error(str(e), console=console)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print_error(f"Unexpected error: {str(e)}", console=console)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Constants for the ecosystems CLI."""
|
|
2
|
+
|
|
3
|
+
# API Configuration
|
|
4
|
+
API_BASE_URL_TEMPLATE = "https://{api_name}.ecosyste.ms/api/v1"
|
|
5
|
+
DEFAULT_TIMEOUT = 20
|
|
6
|
+
DEFAULT_CONTENT_TYPE = "application/json"
|
|
7
|
+
|
|
8
|
+
# Supported APIs
|
|
9
|
+
SUPPORTED_APIS = [
|
|
10
|
+
"advisories",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
# Output Formats
|
|
14
|
+
OUTPUT_FORMATS = ["table", "json", "tsv", "jsonl"]
|
|
15
|
+
DEFAULT_OUTPUT_FORMAT = "table"
|
|
16
|
+
|
|
17
|
+
# Display Configuration
|
|
18
|
+
JSON_THEME = "monokai"
|
|
19
|
+
JSON_SYNTAX = "json"
|
|
20
|
+
MAX_SELECTED_FIELDS = 2
|
|
21
|
+
DEFAULT_TABLE_TITLE = "API Response"
|
|
22
|
+
TABLE_HEADER_STYLE = "bold cyan"
|
|
23
|
+
|
|
24
|
+
# Rich Console Styles
|
|
25
|
+
STYLE_BOLD_CYAN = "bold cyan"
|
|
26
|
+
STYLE_BOLD_GREEN = "bold green"
|
|
27
|
+
STYLE_BOLD_YELLOW = "bold yellow"
|
|
28
|
+
STYLE_BOLD_MAGENTA = "bold magenta"
|
|
29
|
+
STYLE_CYAN = "cyan"
|
|
30
|
+
STYLE_GREEN = "green"
|
|
31
|
+
STYLE_YELLOW = "yellow"
|
|
32
|
+
STYLE_MAGENTA = "magenta"
|
|
33
|
+
STYLE_RED = "red"
|
|
34
|
+
STYLE_BOLD_RED = "bold red"
|
|
35
|
+
STYLE_BLUE = "blue"
|
|
36
|
+
|
|
37
|
+
# Table Display Priority Fields
|
|
38
|
+
PRIORITY_FIELDS = {
|
|
39
|
+
"advisories": ["uuid", "title", "severity", "published_at", "cvss_score"],
|
|
40
|
+
"archives": ["name", "directory", "contents"],
|
|
41
|
+
"commits": ["sha", "author", "message", "timestamp", "merge"],
|
|
42
|
+
"repos": ["full_name", "name", "description", "stars", "language"],
|
|
43
|
+
"packages": ["name", "platform", "description", "downloads", "language"],
|
|
44
|
+
"summary": ["name", "type", "count", "total", "description"],
|
|
45
|
+
"awesome": ["name", "title", "url", "description", "category"],
|
|
46
|
+
"papers": ["doi", "title", "publication_date", "mentions_count", "openalex_id"],
|
|
47
|
+
"ost": ["id", "url", "category", "language", "score"],
|
|
48
|
+
"parser": ["id", "url", "status", "created_at", "sha256"],
|
|
49
|
+
"resolver": ["id", "package_name", "registry", "status", "created_at"],
|
|
50
|
+
"sbom": ["id", "url", "status", "created_at", "sha256"],
|
|
51
|
+
"licenses": ["id", "url", "status", "created_at", "sha256"],
|
|
52
|
+
"timeline": ["actor", "event_type", "repository", "owner", "payload"],
|
|
53
|
+
"issues": ["number", "title", "state", "user", "created_at"],
|
|
54
|
+
"sponsors": ["login", "has_sponsors_listing", "sponsors_count", "sponsorships_count", "minimum_sponsorship_amount"],
|
|
55
|
+
"opencollective": ["id", "url", "name", "description", "created_at"],
|
|
56
|
+
"docker": ["id", "url", "name", "description", "created_at"],
|
|
57
|
+
"diff": ["id", "url", "name", "description", "created_at"],
|
|
58
|
+
"ruby": ["id", "url", "language", "score", "monthly_downloads"],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Operations Display
|
|
62
|
+
OPERATIONS_PANEL_TITLE = "Operations"
|
|
63
|
+
OPERATIONS_PANEL_STYLE = "yellow"
|
|
64
|
+
AVAILABLE_OPERATIONS_TITLE = "Available Operations"
|
|
65
|
+
AVAILABLE_OPERATIONS_STYLE = "blue"
|
|
66
|
+
OPERATION_HEADERS = {"operation": "OPERATION", "method": "METHOD", "path": "PATH", "description": "DESCRIPTION"}
|
|
67
|
+
DIVIDER_WIDTH_OFFSET = 40
|
|
68
|
+
SUMMARY_TRUNCATE_LENGTH = 50
|
|
69
|
+
|
|
70
|
+
# Error Display
|
|
71
|
+
ERROR_PREFIX = "[bold red]Error:[/bold red]"
|
|
72
|
+
ERROR_PANEL_STYLE = "red"
|
|
73
|
+
|
|
74
|
+
# File Patterns
|
|
75
|
+
OPENAPI_FILE_EXTENSION = ".openapi.yaml"
|
|
76
|
+
|
|
77
|
+
# HTTP Configuration
|
|
78
|
+
HTTP_METHODS = ["get", "post", "put", "delete", "patch"]
|
|
79
|
+
|
|
80
|
+
# Value Formatting
|
|
81
|
+
EMPTY_DICT_DISPLAY = "{}"
|
|
82
|
+
EMPTY_LIST_DISPLAY = "[]"
|
|
83
|
+
MAX_INLINE_ITEMS = 3
|
|
84
|
+
DICT_TRUNCATE_FORMAT = "{{...}} ({count} items)"
|
|
85
|
+
LIST_TRUNCATE_FORMAT = "[...] ({count} items)"
|
|
86
|
+
|
|
87
|
+
# Separators
|
|
88
|
+
DEFAULT_SEPARATOR = "_"
|
|
89
|
+
|
|
90
|
+
# Error Message Templates
|
|
91
|
+
ERROR_API_ONLY = "This method is only available for the {api_name} API"
|
|
92
|
+
ERROR_OPERATION_NOT_FOUND = "Operation '{operation_id}' not found in {api_name} API"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Custom exceptions for the Ecosystems CLI."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EcosystemsCLIError(Exception):
|
|
5
|
+
"""Base exception for all ecosystems CLI errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIError(EcosystemsCLIError):
|
|
11
|
+
"""Base exception for API-related errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIConnectionError(APIError):
|
|
17
|
+
"""Raised when unable to connect to the API."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class APITimeoutError(APIError):
|
|
23
|
+
"""Raised when an API request times out."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class APIHTTPError(APIError):
|
|
29
|
+
"""Base exception for HTTP errors from the API."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, status_code: int, message: str = None):
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
super().__init__(message or f"HTTP {status_code} error")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class APIAuthenticationError(APIHTTPError):
|
|
37
|
+
"""Raised when authentication fails (401)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str = None):
|
|
40
|
+
super().__init__(401, message or "Authentication failed")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class APINotFoundError(APIHTTPError):
|
|
44
|
+
"""Raised when a resource is not found (404)."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, message: str = None):
|
|
47
|
+
super().__init__(404, message or "Resource not found")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class APIServerError(APIHTTPError):
|
|
51
|
+
"""Raised when the server encounters an error (5xx)."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, status_code: int = 500, message: str = None):
|
|
54
|
+
super().__init__(status_code, message or f"Server error (HTTP {status_code})")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class APIRateLimitError(APIHTTPError):
|
|
58
|
+
"""Raised when API rate limit is exceeded (429)."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, limit: int = None, remaining: int = None, reset_time: str = None, retry_after: int = None):
|
|
61
|
+
self.limit = limit
|
|
62
|
+
self.remaining = remaining
|
|
63
|
+
self.reset_time = reset_time
|
|
64
|
+
self.retry_after = retry_after
|
|
65
|
+
|
|
66
|
+
message = "Rate limit exceeded."
|
|
67
|
+
if limit is not None:
|
|
68
|
+
message += f"\nLimit: {limit} requests per window"
|
|
69
|
+
if remaining is not None:
|
|
70
|
+
message += f"\nRemaining: {remaining} requests"
|
|
71
|
+
if reset_time:
|
|
72
|
+
message += f"\nReset: {reset_time}"
|
|
73
|
+
if retry_after is not None:
|
|
74
|
+
message += f"\nRetry after: {retry_after} seconds"
|
|
75
|
+
message += "\nPlease wait before retrying."
|
|
76
|
+
|
|
77
|
+
super().__init__(429, message)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConfigurationError(EcosystemsCLIError):
|
|
81
|
+
"""Base exception for configuration-related errors."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InvalidAPIError(ConfigurationError):
|
|
87
|
+
"""Raised when an invalid API is specified."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, api_name: str):
|
|
90
|
+
super().__init__(f"Invalid API: {api_name}")
|
|
91
|
+
self.api_name = api_name
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MissingSpecificationError(ConfigurationError):
|
|
95
|
+
"""Raised when an API specification file is missing."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, api_name: str):
|
|
98
|
+
super().__init__(f"Missing specification for API: {api_name}")
|
|
99
|
+
self.api_name = api_name
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OperationError(EcosystemsCLIError):
|
|
103
|
+
"""Base exception for operation-related errors."""
|
|
104
|
+
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class InvalidOperationError(OperationError):
|
|
109
|
+
"""Raised when an invalid operation is requested."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, operation_id: str):
|
|
112
|
+
super().__init__(f"Invalid operation: {operation_id}")
|
|
113
|
+
self.operation_id = operation_id
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MissingParameterError(OperationError):
|
|
117
|
+
"""Raised when a required parameter is missing."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, parameter_name: str):
|
|
120
|
+
super().__init__(f"Missing required parameter: {parameter_name}")
|
|
121
|
+
self.parameter_name = parameter_name
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class InvalidParameterError(OperationError):
|
|
125
|
+
"""Raised when a parameter has an invalid value."""
|
|
126
|
+
|
|
127
|
+
def __init__(self, parameter_name: str, message: str = None):
|
|
128
|
+
super().__init__(message or f"Invalid parameter: {parameter_name}")
|
|
129
|
+
self.parameter_name = parameter_name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DataError(EcosystemsCLIError):
|
|
133
|
+
"""Base exception for data processing errors."""
|
|
134
|
+
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class JSONParseError(DataError):
|
|
139
|
+
"""Raised when JSON parsing fails."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, message: str = None):
|
|
142
|
+
super().__init__(message or "Failed to parse JSON")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class InvalidResponseFormatError(DataError):
|
|
146
|
+
"""Raised when the API response has an unexpected format."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, message: str = None):
|
|
149
|
+
super().__init__(message or "Invalid response format")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Shared utilities for building Click parameter decorators from OpenAPI parameters."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_click_decorators(parameters: List[dict]) -> List:
|
|
9
|
+
"""Build click decorators from OpenAPI parameters.
|
|
10
|
+
|
|
11
|
+
This utility function converts OpenAPI parameter definitions into Click
|
|
12
|
+
decorators for command-line arguments and options.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
parameters: List of OpenAPI parameter definitions
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List of Click parameter decorators
|
|
19
|
+
"""
|
|
20
|
+
click_decorators = []
|
|
21
|
+
for param in parameters:
|
|
22
|
+
param_name = param.get("name")
|
|
23
|
+
param_in = param.get("in")
|
|
24
|
+
param_description = param.get("description", "")
|
|
25
|
+
param_required = param.get("required", False)
|
|
26
|
+
param_schema = param.get("schema", {})
|
|
27
|
+
param_type = param_schema.get("type", "string")
|
|
28
|
+
|
|
29
|
+
python_param_name = param_name.replace("_", "-")
|
|
30
|
+
|
|
31
|
+
if param_in == "path":
|
|
32
|
+
click_decorators.append(click.argument(param_name))
|
|
33
|
+
elif param_in == "query":
|
|
34
|
+
click_type = None
|
|
35
|
+
if param_type == "integer":
|
|
36
|
+
click_type = int
|
|
37
|
+
elif param_type == "boolean":
|
|
38
|
+
click_type = bool
|
|
39
|
+
|
|
40
|
+
option_decorator = click.option(
|
|
41
|
+
f"--{python_param_name}",
|
|
42
|
+
param_name.replace("-", "_"),
|
|
43
|
+
type=click_type,
|
|
44
|
+
help=param_description,
|
|
45
|
+
required=param_required,
|
|
46
|
+
)
|
|
47
|
+
click_decorators.append(option_decorator)
|
|
48
|
+
|
|
49
|
+
return click_decorators
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Tuple
|
|
2
|
+
|
|
3
|
+
from ecosystems_cli.constants import DEFAULT_SEPARATOR
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def flatten_dict(d: Dict[str, Any], parent_key: str = "", sep: str = DEFAULT_SEPARATOR) -> Dict[str, Any]:
|
|
7
|
+
"""Flatten a nested dictionary for TSV output."""
|
|
8
|
+
items: List[Tuple[str, Any]] = []
|
|
9
|
+
for k, v in d.items():
|
|
10
|
+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
|
11
|
+
if isinstance(v, dict):
|
|
12
|
+
items.extend(flatten_dict(v, new_key, sep=sep).items())
|
|
13
|
+
else:
|
|
14
|
+
items.append((new_key, v))
|
|
15
|
+
return dict(items)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ecosystems_cli.constants import (
|
|
4
|
+
DICT_TRUNCATE_FORMAT,
|
|
5
|
+
EMPTY_DICT_DISPLAY,
|
|
6
|
+
EMPTY_LIST_DISPLAY,
|
|
7
|
+
LIST_TRUNCATE_FORMAT,
|
|
8
|
+
MAX_INLINE_ITEMS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_value(value: Any) -> str:
|
|
13
|
+
"""Format a value for display in a table or TSV."""
|
|
14
|
+
if isinstance(value, (dict, list)):
|
|
15
|
+
# For complex objects, show a simplified representation
|
|
16
|
+
if isinstance(value, dict):
|
|
17
|
+
if len(value) == 0:
|
|
18
|
+
return EMPTY_DICT_DISPLAY
|
|
19
|
+
elif len(value) <= MAX_INLINE_ITEMS:
|
|
20
|
+
return ", ".join(f"{k}: {format_value(v)}" for k, v in value.items())
|
|
21
|
+
else:
|
|
22
|
+
return DICT_TRUNCATE_FORMAT.format(count=len(value))
|
|
23
|
+
elif isinstance(value, list):
|
|
24
|
+
if len(value) == 0:
|
|
25
|
+
return EMPTY_LIST_DISPLAY
|
|
26
|
+
elif len(value) <= MAX_INLINE_ITEMS:
|
|
27
|
+
return ", ".join(format_value(v) for v in value)
|
|
28
|
+
else:
|
|
29
|
+
return LIST_TRUNCATE_FORMAT.format(count=len(value))
|
|
30
|
+
return str(value)
|