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.
Files changed (84) hide show
  1. ecosyste_ms_cli-1.3.2.dist-info/METADATA +133 -0
  2. ecosyste_ms_cli-1.3.2.dist-info/RECORD +84 -0
  3. ecosyste_ms_cli-1.3.2.dist-info/WHEEL +5 -0
  4. ecosyste_ms_cli-1.3.2.dist-info/entry_points.txt +2 -0
  5. ecosyste_ms_cli-1.3.2.dist-info/licenses/LICENSE +21 -0
  6. ecosyste_ms_cli-1.3.2.dist-info/top_level.txt +1 -0
  7. ecosystems_cli/__init__.py +3 -0
  8. ecosystems_cli/__main__.py +4 -0
  9. ecosystems_cli/apis/__init__.py +0 -0
  10. ecosystems_cli/apis/advisories.openapi.yaml +347 -0
  11. ecosystems_cli/apis/archives.openapi.yaml +193 -0
  12. ecosystems_cli/apis/commits.openapi.yaml +391 -0
  13. ecosystems_cli/apis/dependabot.openapi.yaml +887 -0
  14. ecosystems_cli/apis/diff.openapi.yaml +90 -0
  15. ecosystems_cli/apis/docker.openapi.yaml +534 -0
  16. ecosystems_cli/apis/issues.openapi.yaml +839 -0
  17. ecosystems_cli/apis/licenses.openapi.yaml +80 -0
  18. ecosystems_cli/apis/opencollective.openapi.yaml +247 -0
  19. ecosystems_cli/apis/packages.openapi.yaml +2522 -0
  20. ecosystems_cli/apis/parser.openapi.yaml +97 -0
  21. ecosystems_cli/apis/registries.yaml +155 -0
  22. ecosystems_cli/apis/repos.openapi.yaml +1521 -0
  23. ecosystems_cli/apis/resolve.openapi.yaml +130 -0
  24. ecosystems_cli/apis/sbom.openapi.yaml +79 -0
  25. ecosystems_cli/apis/sponsors.openapi.yaml +283 -0
  26. ecosystems_cli/apis/summary.openapi.yaml +239 -0
  27. ecosystems_cli/apis/timeline.openapi.yaml +91 -0
  28. ecosystems_cli/cli.py +213 -0
  29. ecosystems_cli/commands/__init__.py +1 -0
  30. ecosystems_cli/commands/advisories.py +109 -0
  31. ecosystems_cli/commands/archives.py +5 -0
  32. ecosystems_cli/commands/commits.py +5 -0
  33. ecosystems_cli/commands/decorators.py +101 -0
  34. ecosystems_cli/commands/dependabot.py +5 -0
  35. ecosystems_cli/commands/diff.py +144 -0
  36. ecosystems_cli/commands/docker.py +5 -0
  37. ecosystems_cli/commands/execution.py +99 -0
  38. ecosystems_cli/commands/generator.py +127 -0
  39. ecosystems_cli/commands/handlers/__init__.py +45 -0
  40. ecosystems_cli/commands/handlers/advisories.py +73 -0
  41. ecosystems_cli/commands/handlers/archives.py +40 -0
  42. ecosystems_cli/commands/handlers/base.py +38 -0
  43. ecosystems_cli/commands/handlers/commits.py +76 -0
  44. ecosystems_cli/commands/handlers/default.py +40 -0
  45. ecosystems_cli/commands/handlers/dependabot.py +205 -0
  46. ecosystems_cli/commands/handlers/diff.py +72 -0
  47. ecosystems_cli/commands/handlers/docker.py +142 -0
  48. ecosystems_cli/commands/handlers/factory.py +60 -0
  49. ecosystems_cli/commands/handlers/issues.py +87 -0
  50. ecosystems_cli/commands/handlers/licenses.py +52 -0
  51. ecosystems_cli/commands/handlers/opencollective.py +86 -0
  52. ecosystems_cli/commands/handlers/packages.py +103 -0
  53. ecosystems_cli/commands/handlers/parser.py +57 -0
  54. ecosystems_cli/commands/handlers/repos.py +97 -0
  55. ecosystems_cli/commands/handlers/resolve.py +68 -0
  56. ecosystems_cli/commands/handlers/sbom.py +52 -0
  57. ecosystems_cli/commands/handlers/sponsors.py +52 -0
  58. ecosystems_cli/commands/handlers/summary.py +81 -0
  59. ecosystems_cli/commands/handlers/timeline.py +45 -0
  60. ecosystems_cli/commands/issues.py +5 -0
  61. ecosystems_cli/commands/licenses.py +135 -0
  62. ecosystems_cli/commands/mcp.py +54 -0
  63. ecosystems_cli/commands/opencollective.py +5 -0
  64. ecosystems_cli/commands/packages.py +151 -0
  65. ecosystems_cli/commands/parser.py +135 -0
  66. ecosystems_cli/commands/repos.py +5 -0
  67. ecosystems_cli/commands/resolve.py +160 -0
  68. ecosystems_cli/commands/sbom.py +135 -0
  69. ecosystems_cli/commands/sponsors.py +5 -0
  70. ecosystems_cli/commands/summary.py +5 -0
  71. ecosystems_cli/commands/timeline.py +5 -0
  72. ecosystems_cli/constants.py +92 -0
  73. ecosystems_cli/exceptions.py +149 -0
  74. ecosystems_cli/helpers/click_params.py +49 -0
  75. ecosystems_cli/helpers/flatten_dict.py +15 -0
  76. ecosystems_cli/helpers/format_value.py +30 -0
  77. ecosystems_cli/helpers/get_domain.py +68 -0
  78. ecosystems_cli/helpers/load_api_spec.py +31 -0
  79. ecosystems_cli/helpers/print_error.py +15 -0
  80. ecosystems_cli/helpers/print_operations.py +73 -0
  81. ecosystems_cli/helpers/print_output.py +183 -0
  82. ecosystems_cli/helpers/purl_parser.py +121 -0
  83. ecosystems_cli/mcp_server.py +267 -0
  84. 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,5 @@
1
+ """Commands for the sponsors API."""
2
+
3
+ from ecosystems_cli.commands.generator import APICommandGenerator
4
+
5
+ sponsors = APICommandGenerator.create_api_group("sponsors")
@@ -0,0 +1,5 @@
1
+ """Commands for the summary API."""
2
+
3
+ from ecosystems_cli.commands.generator import APICommandGenerator
4
+
5
+ summary = APICommandGenerator.create_api_group("summary")
@@ -0,0 +1,5 @@
1
+ """Commands for the timeline API."""
2
+
3
+ from ecosystems_cli.commands.generator import APICommandGenerator
4
+
5
+ timeline = APICommandGenerator.create_api_group("timeline")
@@ -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)