recce-cloud 1.31.0__tar.gz → 1.33.1__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.
- {recce_cloud-1.31.0 → recce_cloud-1.33.1}/.gitignore +6 -3
- {recce_cloud-1.31.0 → recce_cloud-1.33.1}/PKG-INFO +115 -2
- {recce_cloud-1.31.0 → recce_cloud-1.33.1}/pyproject.toml +17 -4
- recce_cloud-1.33.1/recce_cloud/VERSION +1 -0
- recce_cloud-1.33.1/recce_cloud/__init__.py +28 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/client.py +281 -2
- recce_cloud-1.33.1/recce_cloud/auth/__init__.py +21 -0
- recce_cloud-1.33.1/recce_cloud/auth/callback_server.py +128 -0
- recce_cloud-1.33.1/recce_cloud/auth/login.py +281 -0
- recce_cloud-1.33.1/recce_cloud/auth/profile.py +131 -0
- recce_cloud-1.33.1/recce_cloud/auth/templates/error.html +58 -0
- recce_cloud-1.33.1/recce_cloud/auth/templates/success.html +58 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/cli.py +661 -33
- recce_cloud-1.33.1/recce_cloud/commands/__init__.py +1 -0
- recce_cloud-1.33.1/recce_cloud/commands/diagnostics.py +174 -0
- recce_cloud-1.33.1/recce_cloud/config/__init__.py +19 -0
- recce_cloud-1.33.1/recce_cloud/config/project_config.py +187 -0
- recce_cloud-1.33.1/recce_cloud/config/resolver.py +137 -0
- recce_cloud-1.33.1/recce_cloud/services/__init__.py +1 -0
- recce_cloud-1.33.1/recce_cloud/services/diagnostic_service.py +380 -0
- recce_cloud-1.33.1/recce_cloud/upload.py +438 -0
- recce_cloud-1.31.0/README.md +0 -780
- recce_cloud-1.31.0/VERSION +0 -1
- recce_cloud-1.31.0/__init__.py +0 -24
- recce_cloud-1.31.0/upload.py +0 -214
- {recce_cloud-1.31.0 → recce_cloud-1.33.1}/hatch_build.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/__init__.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/base.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/exceptions.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/factory.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/github.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/api/gitlab.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/artifact.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/ci_providers/__init__.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/ci_providers/base.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/ci_providers/detector.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/ci_providers/github_actions.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/ci_providers/gitlab_ci.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/delete.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/download.py +0 -0
- {recce_cloud-1.31.0 → recce_cloud-1.33.1/recce_cloud}/report.py +0 -0
|
@@ -20,7 +20,10 @@ recce.yml
|
|
|
20
20
|
|
|
21
21
|
# Ignore build artifacts from frontend
|
|
22
22
|
recce/data
|
|
23
|
-
.worktrees
|
|
24
23
|
|
|
25
|
-
# ignore Claude logs and plans
|
|
26
|
-
docs/plans
|
|
24
|
+
# ignore Claude logs and plans at any nesting
|
|
25
|
+
**/docs/plans/**/*.md
|
|
26
|
+
**/docs/plans/**/*.json
|
|
27
|
+
|
|
28
|
+
# Git worktrees for isolated development
|
|
29
|
+
.worktrees/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: recce-cloud
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.33.1
|
|
4
4
|
Summary: Lightweight CLI for Recce Cloud operations
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/InfuseAI/recce/issues
|
|
6
6
|
Author-email: InfuseAI Dev Team <dev@infuseai.io>
|
|
@@ -15,8 +15,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Requires-Dist: click>=7.1
|
|
18
|
+
Requires-Dist: cryptography>=3.4
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
18
20
|
Requires-Dist: requests>=2.28.1
|
|
19
21
|
Requires-Dist: rich>=12.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: flake8>=7.2.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=4.6; extra == 'dev'
|
|
20
25
|
Description-Content-Type: text/markdown
|
|
21
26
|
|
|
22
27
|
# Recce Cloud CLI
|
|
@@ -33,7 +38,8 @@ The Recce Cloud CLI (`recce-cloud`) is a standalone tool designed for CI/CD pipe
|
|
|
33
38
|
- 🤖 Auto-detection - automatically detects CI platform, repository, and PR/MR context
|
|
34
39
|
- ⬆️ Upload - push dbt artifacts to Recce Cloud sessions
|
|
35
40
|
- ⬇️ Download - pull dbt artifacts from Recce Cloud sessions
|
|
36
|
-
- 🔐 Flexible authentication -
|
|
41
|
+
- 🔐 Flexible authentication - browser-based login, token-based auth, or CI tokens
|
|
42
|
+
- 🔗 Project binding - link local projects to Recce Cloud organizations
|
|
37
43
|
- ✅ Platform-specific - optimized for GitHub Actions and GitLab CI
|
|
38
44
|
|
|
39
45
|
## Installation
|
|
@@ -57,6 +63,34 @@ install:
|
|
|
57
63
|
|
|
58
64
|
## Quick Start
|
|
59
65
|
|
|
66
|
+
### Local Development
|
|
67
|
+
|
|
68
|
+
**Login to Recce Cloud:**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Browser-based login (recommended)
|
|
72
|
+
recce-cloud login
|
|
73
|
+
|
|
74
|
+
# Token-based login (for headless environments)
|
|
75
|
+
recce-cloud login --token <your-api-token>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Initialize project binding:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Interactive project selection
|
|
82
|
+
recce-cloud init
|
|
83
|
+
|
|
84
|
+
# Check current status
|
|
85
|
+
recce-cloud status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Logout:**
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
recce-cloud logout
|
|
92
|
+
```
|
|
93
|
+
|
|
60
94
|
### GitHub Actions
|
|
61
95
|
|
|
62
96
|
**Upload artifacts:**
|
|
@@ -232,6 +266,85 @@ recce-cloud download --session-id abc123 --force
|
|
|
232
266
|
|
|
233
267
|
## Command Reference
|
|
234
268
|
|
|
269
|
+
### `recce-cloud login`
|
|
270
|
+
|
|
271
|
+
Authenticate with Recce Cloud.
|
|
272
|
+
|
|
273
|
+
**Options:**
|
|
274
|
+
|
|
275
|
+
| Option | Type | Default | Description |
|
|
276
|
+
| --------- | ------ | ------- | ------------------------------------------------ |
|
|
277
|
+
| `--token` | string | - | API token for direct authentication (optional) |
|
|
278
|
+
|
|
279
|
+
**Usage:**
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Browser-based login (opens browser for OAuth)
|
|
283
|
+
recce-cloud login
|
|
284
|
+
|
|
285
|
+
# Direct token authentication (for CI/headless environments)
|
|
286
|
+
recce-cloud login --token <your-api-token>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Notes:**
|
|
290
|
+
|
|
291
|
+
- Browser login opens Recce Cloud in your default browser for secure OAuth authentication
|
|
292
|
+
- If the browser doesn't open automatically, the URL is displayed for manual access
|
|
293
|
+
- Token-based login is useful for CI/CD environments or when browser access is unavailable
|
|
294
|
+
- Credentials are saved to `~/.recce/profile.yml`
|
|
295
|
+
|
|
296
|
+
### `recce-cloud logout`
|
|
297
|
+
|
|
298
|
+
Clear stored Recce Cloud credentials.
|
|
299
|
+
|
|
300
|
+
**Usage:**
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
recce-cloud logout
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### `recce-cloud init`
|
|
307
|
+
|
|
308
|
+
Initialize project binding to link a local project with a Recce Cloud organization and project.
|
|
309
|
+
|
|
310
|
+
**Usage:**
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
recce-cloud init
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Workflow:**
|
|
317
|
+
|
|
318
|
+
1. Verifies you are logged in (prompts for login if not)
|
|
319
|
+
2. Lists available organizations (shows display names)
|
|
320
|
+
3. Prompts you to select an organization
|
|
321
|
+
4. Lists available projects in selected organization (excludes archived projects)
|
|
322
|
+
5. Prompts you to select a project
|
|
323
|
+
6. Saves binding to `.recce/config`
|
|
324
|
+
7. Optionally adds `.recce/` to `.gitignore`
|
|
325
|
+
|
|
326
|
+
**Notes:**
|
|
327
|
+
|
|
328
|
+
- Requires authentication (run `recce-cloud login` first)
|
|
329
|
+
- Creates `.recce/config` file in current directory
|
|
330
|
+
- Binding can be overridden with environment variables (`RECCE_ORG`, `RECCE_PROJECT`)
|
|
331
|
+
|
|
332
|
+
### `recce-cloud status`
|
|
333
|
+
|
|
334
|
+
Display current authentication and project binding status.
|
|
335
|
+
|
|
336
|
+
**Usage:**
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
recce-cloud status
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Output:**
|
|
343
|
+
|
|
344
|
+
- Shows logged-in user email (or "Not logged in")
|
|
345
|
+
- Shows current project binding (organization/project or "Not bound")
|
|
346
|
+
- Indicates configuration source (CLI flags, environment variables, or local config)
|
|
347
|
+
|
|
235
348
|
### `recce-cloud upload`
|
|
236
349
|
|
|
237
350
|
Upload dbt artifacts to Recce Cloud session.
|
|
@@ -8,6 +8,8 @@ license = {text = "Apache-2.0"}
|
|
|
8
8
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"click>=7.1",
|
|
11
|
+
"cryptography>=3.4",
|
|
12
|
+
"pyyaml>=6.0",
|
|
11
13
|
"requests>=2.28.1",
|
|
12
14
|
"rich>=12.0.0",
|
|
13
15
|
]
|
|
@@ -23,6 +25,12 @@ classifiers = [
|
|
|
23
25
|
"Development Status :: 4 - Beta",
|
|
24
26
|
]
|
|
25
27
|
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=4.6",
|
|
31
|
+
"flake8>=7.2.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
26
34
|
[project.scripts]
|
|
27
35
|
recce-cloud = "recce_cloud.cli:cloud_cli"
|
|
28
36
|
|
|
@@ -34,7 +42,7 @@ requires = ["hatchling"]
|
|
|
34
42
|
build-backend = "hatchling.build"
|
|
35
43
|
|
|
36
44
|
[tool.hatch.version]
|
|
37
|
-
path = "VERSION"
|
|
45
|
+
path = "recce_cloud/VERSION"
|
|
38
46
|
pattern = "(?P<version>.+)"
|
|
39
47
|
|
|
40
48
|
[tool.hatch.metadata]
|
|
@@ -43,7 +51,12 @@ allow-direct-references = true
|
|
|
43
51
|
[tool.hatch.metadata.hooks.custom]
|
|
44
52
|
|
|
45
53
|
[tool.hatch.build.targets.wheel]
|
|
46
|
-
packages = ["
|
|
54
|
+
packages = ["recce_cloud"]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.sdist]
|
|
57
|
+
include = [
|
|
58
|
+
"recce_cloud/**/*.py",
|
|
59
|
+
"recce_cloud/**/*.html",
|
|
60
|
+
"recce_cloud/VERSION",
|
|
61
|
+
]
|
|
47
62
|
|
|
48
|
-
[tool.hatch.build.targets.wheel.sources]
|
|
49
|
-
"." = "recce_cloud"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.33.1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Recce Cloud - Lightweight CLI for Recce Cloud operations."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_version():
|
|
8
|
+
"""Get version from package metadata or VERSION file."""
|
|
9
|
+
# Try importlib.metadata first (works for installed packages)
|
|
10
|
+
# Try both package names (nightly and official)
|
|
11
|
+
for pkg_name in ["recce-cloud-nightly", "recce-cloud"]:
|
|
12
|
+
try:
|
|
13
|
+
return version(pkg_name)
|
|
14
|
+
except PackageNotFoundError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
# Fallback to VERSION file (for development with editable install)
|
|
18
|
+
# VERSION is now at recce_cloud/recce_cloud/VERSION (same dir as __init__.py)
|
|
19
|
+
version_file = os.path.join(os.path.dirname(__file__), "VERSION")
|
|
20
|
+
if os.path.exists(version_file):
|
|
21
|
+
with open(version_file) as fh:
|
|
22
|
+
return fh.read().strip()
|
|
23
|
+
|
|
24
|
+
# Last resort
|
|
25
|
+
return "unknown"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__version__ = get_version()
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Recce Cloud API clients for lightweight operations.
|
|
3
3
|
|
|
4
|
-
Provides clients for session management and report generation.
|
|
4
|
+
Provides clients for session management, organization/project listing, and report generation.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
12
|
import requests
|
|
13
13
|
|
|
@@ -224,6 +224,285 @@ class RecceCloudClient:
|
|
|
224
224
|
status_code=response.status_code,
|
|
225
225
|
)
|
|
226
226
|
|
|
227
|
+
def upload_completed(self, session_id: str) -> dict:
|
|
228
|
+
"""
|
|
229
|
+
Notify Recce Cloud that upload is complete for a session.
|
|
230
|
+
|
|
231
|
+
This triggers post-upload processing such as AI summary generation.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
session_id: Session ID to notify completion for
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
dict containing acknowledgement or empty dict
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
RecceCloudException: If the request fails
|
|
241
|
+
"""
|
|
242
|
+
api_url = f"{self.base_url_v2}/sessions/{session_id}/upload-completed"
|
|
243
|
+
response = self._request("POST", api_url)
|
|
244
|
+
if response.status_code in [200, 204]:
|
|
245
|
+
if response.status_code == 204 or not response.content:
|
|
246
|
+
return {}
|
|
247
|
+
return response.json()
|
|
248
|
+
if response.status_code == 403:
|
|
249
|
+
raise RecceCloudException(
|
|
250
|
+
reason=response.json().get("detail", "Permission denied"),
|
|
251
|
+
status_code=response.status_code,
|
|
252
|
+
)
|
|
253
|
+
if response.status_code == 404:
|
|
254
|
+
raise RecceCloudException(
|
|
255
|
+
reason="Session not found",
|
|
256
|
+
status_code=response.status_code,
|
|
257
|
+
)
|
|
258
|
+
raise RecceCloudException(
|
|
259
|
+
reason=response.text,
|
|
260
|
+
status_code=response.status_code,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def list_organizations(self) -> List[Dict[str, Any]]:
|
|
264
|
+
"""
|
|
265
|
+
List all organizations the user has access to.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of organization dictionaries with id, name, slug fields.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
RecceCloudException: If the API call fails.
|
|
272
|
+
"""
|
|
273
|
+
api_url = f"{self.base_url_v2}/organizations"
|
|
274
|
+
response = self._request("GET", api_url)
|
|
275
|
+
|
|
276
|
+
if response.status_code != 200:
|
|
277
|
+
raise RecceCloudException(
|
|
278
|
+
reason=response.text,
|
|
279
|
+
status_code=response.status_code,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
data = response.json()
|
|
283
|
+
return data.get("organizations", [])
|
|
284
|
+
|
|
285
|
+
def list_projects(self, org_id: str) -> List[Dict[str, Any]]:
|
|
286
|
+
"""
|
|
287
|
+
List all projects in an organization.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
org_id: Organization ID or slug.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
List of project dictionaries with id, name, slug fields.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
RecceCloudException: If the API call fails.
|
|
297
|
+
"""
|
|
298
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects"
|
|
299
|
+
response = self._request("GET", api_url)
|
|
300
|
+
|
|
301
|
+
if response.status_code != 200:
|
|
302
|
+
raise RecceCloudException(
|
|
303
|
+
reason=response.text,
|
|
304
|
+
status_code=response.status_code,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
data = response.json()
|
|
308
|
+
return data.get("projects", [])
|
|
309
|
+
|
|
310
|
+
def get_organization(self, org_id: str) -> Optional[Dict[str, Any]]:
|
|
311
|
+
"""
|
|
312
|
+
Get a specific organization by ID or slug.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
org_id: Organization ID or slug.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Organization dictionary, or None if not found.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
RecceCloudException: If the API call fails.
|
|
322
|
+
"""
|
|
323
|
+
orgs = self.list_organizations()
|
|
324
|
+
for org in orgs:
|
|
325
|
+
# Compare as strings to handle both int and str IDs
|
|
326
|
+
if str(org.get("id")) == str(org_id) or org.get("slug") == org_id or org.get("name") == org_id:
|
|
327
|
+
return org
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def get_project(self, org_id: str, project_id: str) -> Optional[Dict[str, Any]]:
|
|
331
|
+
"""
|
|
332
|
+
Get a specific project by ID or slug.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
org_id: Organization ID or slug.
|
|
336
|
+
project_id: Project ID or slug.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Project dictionary, or None if not found.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
RecceCloudException: If the API call fails.
|
|
343
|
+
"""
|
|
344
|
+
projects = self.list_projects(org_id)
|
|
345
|
+
for project in projects:
|
|
346
|
+
# Compare as strings to handle both int and str IDs
|
|
347
|
+
if (
|
|
348
|
+
str(project.get("id")) == str(project_id)
|
|
349
|
+
or project.get("slug") == project_id
|
|
350
|
+
or project.get("name") == project_id
|
|
351
|
+
):
|
|
352
|
+
return project
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def list_sessions(
|
|
356
|
+
self,
|
|
357
|
+
org_id: str,
|
|
358
|
+
project_id: str,
|
|
359
|
+
session_name: Optional[str] = None,
|
|
360
|
+
session_type: Optional[str] = None,
|
|
361
|
+
branch: Optional[str] = None,
|
|
362
|
+
limit: Optional[int] = None,
|
|
363
|
+
offset: Optional[int] = None,
|
|
364
|
+
) -> List[Dict[str, Any]]:
|
|
365
|
+
"""
|
|
366
|
+
List sessions in a project with optional filtering.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
org_id: Organization ID or slug.
|
|
370
|
+
project_id: Project ID or slug.
|
|
371
|
+
session_name: Filter by session name (exact match).
|
|
372
|
+
session_type: Filter by session type (e.g., "pr", "prod", "manual").
|
|
373
|
+
branch: Filter by branch name (exact match).
|
|
374
|
+
limit: Maximum number of results to return (1-1000).
|
|
375
|
+
offset: Number of results to skip for pagination.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of session dictionaries.
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
RecceCloudException: If the API call fails.
|
|
382
|
+
"""
|
|
383
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
|
|
384
|
+
params = {}
|
|
385
|
+
if session_name:
|
|
386
|
+
params["name"] = session_name
|
|
387
|
+
if session_type:
|
|
388
|
+
params["type"] = session_type
|
|
389
|
+
if branch:
|
|
390
|
+
params["branch"] = branch
|
|
391
|
+
if limit is not None:
|
|
392
|
+
params["limit"] = limit
|
|
393
|
+
if offset is not None:
|
|
394
|
+
params["offset"] = offset
|
|
395
|
+
|
|
396
|
+
response = self._request("GET", api_url, params=params if params else None)
|
|
397
|
+
|
|
398
|
+
if response.status_code == 404:
|
|
399
|
+
raise RecceCloudException(
|
|
400
|
+
reason="Organization or project not found",
|
|
401
|
+
status_code=response.status_code,
|
|
402
|
+
)
|
|
403
|
+
if response.status_code != 200:
|
|
404
|
+
raise RecceCloudException(
|
|
405
|
+
reason=response.text,
|
|
406
|
+
status_code=response.status_code,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
data = response.json()
|
|
410
|
+
return data.get("sessions", [])
|
|
411
|
+
|
|
412
|
+
def get_session_by_name(
|
|
413
|
+
self,
|
|
414
|
+
org_id: str,
|
|
415
|
+
project_id: str,
|
|
416
|
+
session_name: str,
|
|
417
|
+
) -> Optional[Dict[str, Any]]:
|
|
418
|
+
"""
|
|
419
|
+
Get a session by its name.
|
|
420
|
+
|
|
421
|
+
Uses the list sessions endpoint with filtering to find a session
|
|
422
|
+
by name, which is unique within a project.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
org_id: Organization ID or slug.
|
|
426
|
+
project_id: Project ID or slug.
|
|
427
|
+
session_name: The session name to look up.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Session dictionary if found, None otherwise.
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
RecceCloudException: If the API call fails.
|
|
434
|
+
"""
|
|
435
|
+
sessions = self.list_sessions(
|
|
436
|
+
org_id=org_id,
|
|
437
|
+
project_id=project_id,
|
|
438
|
+
session_name=session_name,
|
|
439
|
+
limit=1,
|
|
440
|
+
)
|
|
441
|
+
if sessions:
|
|
442
|
+
return sessions[0]
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
def create_session(
|
|
446
|
+
self,
|
|
447
|
+
org_id: str,
|
|
448
|
+
project_id: str,
|
|
449
|
+
session_name: str,
|
|
450
|
+
adapter_type: Optional[str] = None,
|
|
451
|
+
session_type: str = "manual",
|
|
452
|
+
) -> Dict[str, Any]:
|
|
453
|
+
"""
|
|
454
|
+
Create a new session with the given name.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
org_id: Organization ID or slug.
|
|
458
|
+
project_id: Project ID or slug.
|
|
459
|
+
session_name: The name for the new session.
|
|
460
|
+
adapter_type: dbt adapter type (e.g., "postgres", "snowflake").
|
|
461
|
+
session_type: Session type (default: "manual").
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Created session dictionary with id, name, and other fields.
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
RecceCloudException: If the API call fails or session creation fails.
|
|
468
|
+
"""
|
|
469
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
|
|
470
|
+
data = {
|
|
471
|
+
"name": session_name,
|
|
472
|
+
"type": session_type,
|
|
473
|
+
}
|
|
474
|
+
if adapter_type:
|
|
475
|
+
data["adapter_type"] = adapter_type
|
|
476
|
+
|
|
477
|
+
response = self._request("POST", api_url, json=data)
|
|
478
|
+
|
|
479
|
+
if response.status_code == 404:
|
|
480
|
+
raise RecceCloudException(
|
|
481
|
+
reason="Organization or project not found",
|
|
482
|
+
status_code=response.status_code,
|
|
483
|
+
)
|
|
484
|
+
if response.status_code == 409:
|
|
485
|
+
raise RecceCloudException(
|
|
486
|
+
reason=f"Session with name '{session_name}' already exists",
|
|
487
|
+
status_code=response.status_code,
|
|
488
|
+
)
|
|
489
|
+
if response.status_code == 403:
|
|
490
|
+
raise RecceCloudException(
|
|
491
|
+
reason=response.json().get("detail", "Permission denied"),
|
|
492
|
+
status_code=response.status_code,
|
|
493
|
+
)
|
|
494
|
+
if response.status_code not in [200, 201]:
|
|
495
|
+
raise RecceCloudException(
|
|
496
|
+
reason=response.text,
|
|
497
|
+
status_code=response.status_code,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
result = response.json()
|
|
501
|
+
# Handle both direct session response and wrapped response
|
|
502
|
+
if "session" in result:
|
|
503
|
+
return result["session"]
|
|
504
|
+
return result
|
|
505
|
+
|
|
227
506
|
|
|
228
507
|
class ReportClient:
|
|
229
508
|
"""Client for fetching reports from Recce Cloud API."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication module for Recce Cloud CLI.
|
|
3
|
+
|
|
4
|
+
This module provides browser-based OAuth authentication and credential management.
|
|
5
|
+
It is designed to be standalone (duplicated from Recce OSS) to support future
|
|
6
|
+
repository separation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from recce_cloud.auth.profile import (
|
|
10
|
+
get_api_token,
|
|
11
|
+
get_user_id,
|
|
12
|
+
load_profile,
|
|
13
|
+
update_api_token,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"get_api_token",
|
|
18
|
+
"get_user_id",
|
|
19
|
+
"load_profile",
|
|
20
|
+
"update_api_token",
|
|
21
|
+
]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
One-time HTTP callback server for OAuth authentication.
|
|
3
|
+
|
|
4
|
+
This server receives the encrypted token from Recce Cloud after
|
|
5
|
+
browser-based authentication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from urllib.parse import parse_qs, urlparse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CallbackResult:
|
|
16
|
+
"""Container for callback result."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.code: Optional[str] = None
|
|
20
|
+
self.error: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_callback_handler(result: CallbackResult, on_success_html: str, on_error_html: str):
|
|
24
|
+
"""
|
|
25
|
+
Create a one-time HTTP request handler.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
result: CallbackResult object to store the received code.
|
|
29
|
+
on_success_html: HTML content to return on success.
|
|
30
|
+
on_error_html: HTML content to return on error.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
HTTP request handler class.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
class OneTimeHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
37
|
+
def do_GET(self):
|
|
38
|
+
try:
|
|
39
|
+
parsed_url = urlparse(self.path)
|
|
40
|
+
query_params = parse_qs(parsed_url.query)
|
|
41
|
+
|
|
42
|
+
code = query_params.get("code", [None])[0]
|
|
43
|
+
if not code:
|
|
44
|
+
result.error = "Missing 'code' parameter in callback"
|
|
45
|
+
self._send_response(400, on_error_html)
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
result.code = code
|
|
49
|
+
self._send_response(200, on_success_html)
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
result.error = str(e)
|
|
53
|
+
self._send_response(500, on_error_html)
|
|
54
|
+
finally:
|
|
55
|
+
# Shutdown server after handling the request
|
|
56
|
+
self.server.server_close()
|
|
57
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
58
|
+
|
|
59
|
+
def _send_response(self, status_code: int, html_content: str):
|
|
60
|
+
self.send_response(status_code)
|
|
61
|
+
self.send_header("Content-Type", "text/html")
|
|
62
|
+
self.send_header("Content-Length", str(len(html_content.encode())))
|
|
63
|
+
self.end_headers()
|
|
64
|
+
self.wfile.write(html_content.encode())
|
|
65
|
+
|
|
66
|
+
def log_message(self, format, *args):
|
|
67
|
+
# Suppress default logging
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
return OneTimeHTTPRequestHandler
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def run_callback_server(
|
|
74
|
+
port: int,
|
|
75
|
+
result: CallbackResult,
|
|
76
|
+
on_success_html: str,
|
|
77
|
+
on_error_html: str,
|
|
78
|
+
timeout: int = 300,
|
|
79
|
+
) -> bool:
|
|
80
|
+
"""
|
|
81
|
+
Run a one-time HTTP server to receive the OAuth callback.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
port: Port to listen on.
|
|
85
|
+
result: CallbackResult object to store the received code.
|
|
86
|
+
on_success_html: HTML content to return on success.
|
|
87
|
+
on_error_html: HTML content to return on error.
|
|
88
|
+
timeout: Server timeout in seconds (default 5 minutes).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
True if callback was received, False if timeout or error.
|
|
92
|
+
"""
|
|
93
|
+
handler = make_callback_handler(result, on_success_html, on_error_html)
|
|
94
|
+
server = HTTPServer(("localhost", port), handler)
|
|
95
|
+
server.timeout = timeout
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# Handle a single request
|
|
99
|
+
server.handle_request()
|
|
100
|
+
return result.code is not None
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
finally:
|
|
104
|
+
server.server_close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_template(filename: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Load HTML template from the templates directory.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
filename: Name of the template file (e.g., 'success.html').
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
HTML content as string.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
FileNotFoundError: If template file is not found.
|
|
119
|
+
"""
|
|
120
|
+
templates_dir = Path(__file__).parent / "templates"
|
|
121
|
+
template_path = templates_dir / filename
|
|
122
|
+
with open(template_path, "r", encoding="utf-8") as f:
|
|
123
|
+
return f.read()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Load HTML templates from separate files
|
|
127
|
+
SUCCESS_HTML = _load_template("success.html")
|
|
128
|
+
ERROR_HTML = _load_template("error.html")
|