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.
@@ -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,15 @@
1
+ # Convexity CLI
2
+
3
+ Command-line interface for the Convexity platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install convexity-cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ convexity-cli --help
15
+ ```
@@ -0,0 +1,5 @@
1
+ """Convexity CLI - Command-line interface for managing Convexity projects."""
2
+
3
+ from importlib.metadata import version as _v
4
+
5
+ __version__ = _v("convexity-cli")
@@ -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}")