convexity-cli 0.4.0.dev146__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.
- convexity_cli-0.4.0.dev146/.gitignore +46 -0
- convexity_cli-0.4.0.dev146/PKG-INFO +30 -0
- convexity_cli-0.4.0.dev146/README.md +15 -0
- convexity_cli-0.4.0.dev146/convexity_cli/__init__.py +5 -0
- convexity_cli-0.4.0.dev146/convexity_cli/client.py +200 -0
- convexity_cli-0.4.0.dev146/convexity_cli/commands/__init__.py +1 -0
- convexity_cli-0.4.0.dev146/convexity_cli/commands/org.py +117 -0
- convexity_cli-0.4.0.dev146/convexity_cli/commands/project.py +1299 -0
- convexity_cli-0.4.0.dev146/convexity_cli/commands/utils.py +57 -0
- convexity_cli-0.4.0.dev146/convexity_cli/config.py +203 -0
- convexity_cli-0.4.0.dev146/convexity_cli/exceptions.py +154 -0
- convexity_cli-0.4.0.dev146/convexity_cli/main.py +218 -0
- convexity_cli-0.4.0.dev146/convexity_cli/output.py +145 -0
- convexity_cli-0.4.0.dev146/pyproject.toml +30 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Monorepo specific ignores
|
|
2
|
+
node_modules/
|
|
3
|
+
.venv
|
|
4
|
+
*.egg-info/
|
|
5
|
+
back.egg-info/
|
|
6
|
+
/Include
|
|
7
|
+
/share
|
|
8
|
+
/logs
|
|
9
|
+
dist
|
|
10
|
+
/out
|
|
11
|
+
/.mypy_cache
|
|
12
|
+
/.ruff_cache
|
|
13
|
+
/.binaries
|
|
14
|
+
.cache
|
|
15
|
+
.cache_oai
|
|
16
|
+
/uploaded_files
|
|
17
|
+
/code
|
|
18
|
+
db
|
|
19
|
+
/output
|
|
20
|
+
output/
|
|
21
|
+
/coverage
|
|
22
|
+
**/build/
|
|
23
|
+
/.pnp
|
|
24
|
+
.pnp.js
|
|
25
|
+
.DS_Store
|
|
26
|
+
npm-debug.log*
|
|
27
|
+
yarn-debug.log*
|
|
28
|
+
yarn-error.log*
|
|
29
|
+
.vercel
|
|
30
|
+
*.css.d.ts
|
|
31
|
+
.env*
|
|
32
|
+
!.env.example
|
|
33
|
+
.env.local
|
|
34
|
+
__pycache__
|
|
35
|
+
coverage.xml
|
|
36
|
+
.coverage
|
|
37
|
+
|
|
38
|
+
sales-analytics/
|
|
39
|
+
# ducklake
|
|
40
|
+
ducklake_catalog/
|
|
41
|
+
ducklake_data/
|
|
42
|
+
.playwright-mcp
|
|
43
|
+
|
|
44
|
+
# Generated CLI API client (regenerated via `npm run generate:api`)
|
|
45
|
+
packages/.generated/
|
|
46
|
+
.wrangler
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: convexity-cli
|
|
3
|
+
Version: 0.4.0.dev146
|
|
4
|
+
Summary: Convexity CLI - command-line interface for the Convexity platform
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: convexity-api-client<0.5.0,>=0.4.0.dev0
|
|
7
|
+
Requires-Dist: convexity-sdk<0.5.0,>=0.4.0.dev0
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
10
|
+
Requires-Dist: pydantic>=2.10.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
12
|
+
Requires-Dist: rich>=13.0.0
|
|
13
|
+
Requires-Dist: typer>=0.12.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Convexity CLI
|
|
17
|
+
|
|
18
|
+
Command-line interface for the Convexity platform.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install convexity-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
convexity-cli --help
|
|
30
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""HTTP client for Convexity API.
|
|
2
|
+
|
|
3
|
+
Provides a wrapper around httpx with API key authentication
|
|
4
|
+
and CLI-specific error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from convexity_cli.config import get_config
|
|
12
|
+
from convexity_cli.exceptions import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
ConflictError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
PermissionError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
ServerError,
|
|
21
|
+
ValidationError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConvexityClient:
|
|
26
|
+
"""HTTP client for Convexity API with API key authentication."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
base_url: str | None = None,
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
timeout: float | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""Initialize the client.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
base_url: API base URL (defaults to config)
|
|
38
|
+
api_key: API key for authentication (defaults to config)
|
|
39
|
+
timeout: Request timeout in seconds (defaults to config)
|
|
40
|
+
"""
|
|
41
|
+
config = get_config()
|
|
42
|
+
|
|
43
|
+
self.base_url = (base_url or config.get_base_url()).rstrip("/")
|
|
44
|
+
self.api_key = api_key or config.get_api_key()
|
|
45
|
+
self.timeout = timeout or config.get_timeout()
|
|
46
|
+
self._client: httpx.Client | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def client(self) -> httpx.Client:
|
|
50
|
+
"""Lazy initialization of HTTP client."""
|
|
51
|
+
if self._client is None:
|
|
52
|
+
if not self.api_key:
|
|
53
|
+
raise AuthenticationError("No API key configured. Run 'convexity auth login' or set CONVEXITY_API_KEY.")
|
|
54
|
+
|
|
55
|
+
self._client = httpx.Client(
|
|
56
|
+
base_url=self.base_url,
|
|
57
|
+
timeout=self.timeout,
|
|
58
|
+
headers={
|
|
59
|
+
"X-API-Key": self.api_key,
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"Accept": "application/json",
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
return self._client
|
|
65
|
+
|
|
66
|
+
def close(self) -> None:
|
|
67
|
+
"""Close the HTTP client."""
|
|
68
|
+
if self._client is not None:
|
|
69
|
+
self._client.close()
|
|
70
|
+
self._client = None
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> "ConvexityClient":
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, *args: Any) -> None:
|
|
76
|
+
self.close()
|
|
77
|
+
|
|
78
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
79
|
+
"""Handle API response and raise appropriate exceptions.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
response: HTTP response
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Parsed JSON response data
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
Various CLI exceptions based on status code
|
|
89
|
+
"""
|
|
90
|
+
if response.status_code >= 200 and response.status_code < 300:
|
|
91
|
+
if response.status_code == 204:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
return response.json()
|
|
95
|
+
except Exception:
|
|
96
|
+
return response.text
|
|
97
|
+
|
|
98
|
+
# Parse error response
|
|
99
|
+
try:
|
|
100
|
+
error_data = response.json()
|
|
101
|
+
error_message = error_data.get("detail", str(error_data))
|
|
102
|
+
except Exception:
|
|
103
|
+
error_message = response.text or f"HTTP {response.status_code}"
|
|
104
|
+
|
|
105
|
+
# Map status codes to exceptions
|
|
106
|
+
status = response.status_code
|
|
107
|
+
|
|
108
|
+
if status == 401:
|
|
109
|
+
raise AuthenticationError(error_message)
|
|
110
|
+
elif status == 403:
|
|
111
|
+
raise PermissionError(error_message)
|
|
112
|
+
elif status == 404:
|
|
113
|
+
raise NotFoundError(message=error_message)
|
|
114
|
+
elif status == 409:
|
|
115
|
+
raise ConflictError(error_message)
|
|
116
|
+
elif status == 422:
|
|
117
|
+
errors = []
|
|
118
|
+
if isinstance(error_data, dict) and "detail" in error_data:
|
|
119
|
+
detail = error_data["detail"]
|
|
120
|
+
if isinstance(detail, list):
|
|
121
|
+
errors = detail
|
|
122
|
+
raise ValidationError(error_message, errors=errors)
|
|
123
|
+
elif status == 429:
|
|
124
|
+
retry_after = response.headers.get("Retry-After")
|
|
125
|
+
raise RateLimitError(
|
|
126
|
+
error_message,
|
|
127
|
+
retry_after=int(retry_after) if retry_after else None,
|
|
128
|
+
)
|
|
129
|
+
elif status >= 500:
|
|
130
|
+
raise ServerError(error_message, status_code=status)
|
|
131
|
+
else:
|
|
132
|
+
raise APIError(error_message, status_code=status)
|
|
133
|
+
|
|
134
|
+
def _request(
|
|
135
|
+
self,
|
|
136
|
+
method: str,
|
|
137
|
+
path: str,
|
|
138
|
+
params: dict[str, Any] | None = None,
|
|
139
|
+
json_data: dict[str, Any] | None = None,
|
|
140
|
+
) -> Any:
|
|
141
|
+
"""Make an HTTP request.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
method: HTTP method (GET, POST, PATCH, DELETE)
|
|
145
|
+
path: API path (will be appended to base_url)
|
|
146
|
+
params: Query parameters
|
|
147
|
+
json_data: JSON body data
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Parsed response data
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
response = self.client.request(
|
|
154
|
+
method=method,
|
|
155
|
+
url=path,
|
|
156
|
+
params=params,
|
|
157
|
+
json=json_data,
|
|
158
|
+
)
|
|
159
|
+
return self._handle_response(response)
|
|
160
|
+
except httpx.ConnectError as e:
|
|
161
|
+
raise NetworkError(f"Failed to connect to {self.base_url}: {e}")
|
|
162
|
+
except httpx.TimeoutException as e:
|
|
163
|
+
raise NetworkError(f"Request timed out: {e}")
|
|
164
|
+
except httpx.RequestError as e:
|
|
165
|
+
raise NetworkError(f"Request failed: {e}")
|
|
166
|
+
|
|
167
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
168
|
+
"""Make a GET request."""
|
|
169
|
+
return self._request("GET", path, params=params)
|
|
170
|
+
|
|
171
|
+
def post(
|
|
172
|
+
self,
|
|
173
|
+
path: str,
|
|
174
|
+
data: dict[str, Any] | None = None,
|
|
175
|
+
params: dict[str, Any] | None = None,
|
|
176
|
+
) -> Any:
|
|
177
|
+
"""Make a POST request."""
|
|
178
|
+
return self._request("POST", path, params=params, json_data=data)
|
|
179
|
+
|
|
180
|
+
def patch(
|
|
181
|
+
self,
|
|
182
|
+
path: str,
|
|
183
|
+
data: dict[str, Any] | None = None,
|
|
184
|
+
params: dict[str, Any] | None = None,
|
|
185
|
+
) -> Any:
|
|
186
|
+
"""Make a PATCH request."""
|
|
187
|
+
return self._request("PATCH", path, params=params, json_data=data)
|
|
188
|
+
|
|
189
|
+
def delete(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
190
|
+
"""Make a DELETE request."""
|
|
191
|
+
return self._request("DELETE", path, params=params)
|
|
192
|
+
|
|
193
|
+
# =========================================================================
|
|
194
|
+
# Organization methods
|
|
195
|
+
# =========================================================================
|
|
196
|
+
|
|
197
|
+
def list_organizations(self) -> list[dict[str, Any]]:
|
|
198
|
+
"""List all organizations the user has access to."""
|
|
199
|
+
response = self.get("/v1/organizations")
|
|
200
|
+
return response.get("items", []) if isinstance(response, dict) else response
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands for Convexity."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Organization management commands for Convexity CLI.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
list - List all organizations the user has access to
|
|
5
|
+
create - Create a new organization
|
|
6
|
+
delete - Delete an organization
|
|
7
|
+
show - Show details of an organization
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
from convexity_api_client.api.v1 import (
|
|
13
|
+
create_organization_v1_organizations_post,
|
|
14
|
+
delete_organization_v1_organizations_organization_id_delete,
|
|
15
|
+
get_organization_v1_organizations_organization_id_get,
|
|
16
|
+
list_organizations_v1_organizations_get,
|
|
17
|
+
)
|
|
18
|
+
from convexity_api_client.models import CreateOrganizationRequest
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
from convexity_cli.commands.utils import get_authenticated_client
|
|
22
|
+
from convexity_cli.exceptions import APIError
|
|
23
|
+
from convexity_cli.output import output_json, output_list, output_success
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="org",
|
|
27
|
+
help="Manage organizations.",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
rich_markup_mode=None,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("list")
|
|
34
|
+
def list_organizations() -> None:
|
|
35
|
+
"""List all organizations the user has access to."""
|
|
36
|
+
with get_authenticated_client() as client:
|
|
37
|
+
response = list_organizations_v1_organizations_get.sync_detailed(client=client)
|
|
38
|
+
|
|
39
|
+
if response.status_code == 200 and response.parsed:
|
|
40
|
+
orgs = [org.to_dict() for org in response.parsed.organizations]
|
|
41
|
+
output_list(orgs)
|
|
42
|
+
else:
|
|
43
|
+
raise APIError(f"Failed to list organizations: {response.status_code}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("create")
|
|
47
|
+
def create_organization(
|
|
48
|
+
name: Annotated[str, typer.Argument(help="Organization name")],
|
|
49
|
+
slug: Annotated[
|
|
50
|
+
str,
|
|
51
|
+
typer.Option("--slug", "-s", help="Organization slug (URL-friendly identifier)"),
|
|
52
|
+
],
|
|
53
|
+
description: Annotated[
|
|
54
|
+
str | None,
|
|
55
|
+
typer.Option("--description", "-d", help="Organization description"),
|
|
56
|
+
] = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Create a new organization."""
|
|
59
|
+
with get_authenticated_client() as client:
|
|
60
|
+
request = CreateOrganizationRequest(
|
|
61
|
+
name=name,
|
|
62
|
+
slug=slug,
|
|
63
|
+
description=description,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
response = create_organization_v1_organizations_post.sync_detailed(
|
|
67
|
+
client=client,
|
|
68
|
+
body=request,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if response.status_code == 201 and response.parsed:
|
|
72
|
+
output_json(response.parsed.to_dict())
|
|
73
|
+
else:
|
|
74
|
+
raise APIError(f"Failed to create organization: {response.status_code}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command("delete")
|
|
78
|
+
def delete_organization(
|
|
79
|
+
org_id: Annotated[str, typer.Argument(help="Organization ID to delete")],
|
|
80
|
+
force: Annotated[
|
|
81
|
+
bool,
|
|
82
|
+
typer.Option("--force", "-f", help="Skip confirmation prompt"),
|
|
83
|
+
] = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Delete an organization."""
|
|
86
|
+
if not force:
|
|
87
|
+
confirm = typer.confirm(f"Are you sure you want to delete organization '{org_id}'? This cannot be undone.")
|
|
88
|
+
if not confirm:
|
|
89
|
+
raise typer.Abort()
|
|
90
|
+
|
|
91
|
+
with get_authenticated_client() as client:
|
|
92
|
+
response = delete_organization_v1_organizations_organization_id_delete.sync_detailed(
|
|
93
|
+
organization_id=org_id,
|
|
94
|
+
client=client,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if response.status_code in (200, 204):
|
|
98
|
+
output_success(f"Organization '{org_id}' deleted successfully")
|
|
99
|
+
else:
|
|
100
|
+
raise APIError(f"Failed to delete organization: {response.status_code}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("show")
|
|
104
|
+
def show_organization(
|
|
105
|
+
org_id: Annotated[str, typer.Argument(help="Organization ID")],
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Show details of an organization."""
|
|
108
|
+
with get_authenticated_client() as client:
|
|
109
|
+
response = get_organization_v1_organizations_organization_id_get.sync_detailed(
|
|
110
|
+
organization_id=org_id,
|
|
111
|
+
client=client,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if response.status_code == 200 and response.parsed:
|
|
115
|
+
output_json(response.parsed.to_dict())
|
|
116
|
+
else:
|
|
117
|
+
raise APIError(f"Failed to get organization: {response.status_code}")
|