recce-cloud 1.32.0__py3-none-any.whl → 1.34.0__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.
- recce_cloud/VERSION +1 -1
- recce_cloud/api/base.py +9 -9
- recce_cloud/api/client.py +501 -6
- recce_cloud/api/github.py +19 -19
- recce_cloud/api/gitlab.py +19 -19
- recce_cloud/auth/__init__.py +21 -0
- recce_cloud/auth/callback_server.py +128 -0
- recce_cloud/auth/login.py +281 -0
- recce_cloud/auth/profile.py +131 -0
- recce_cloud/auth/templates/error.html +58 -0
- recce_cloud/auth/templates/success.html +58 -0
- recce_cloud/ci_providers/base.py +8 -8
- recce_cloud/ci_providers/detector.py +17 -17
- recce_cloud/ci_providers/github_actions.py +8 -8
- recce_cloud/ci_providers/gitlab_ci.py +8 -8
- recce_cloud/cli.py +856 -70
- recce_cloud/commands/__init__.py +1 -0
- recce_cloud/commands/diagnostics.py +174 -0
- recce_cloud/config/__init__.py +19 -0
- recce_cloud/config/project_config.py +187 -0
- recce_cloud/config/resolver.py +165 -0
- recce_cloud/delete.py +6 -6
- recce_cloud/download.py +5 -5
- recce_cloud/review.py +541 -0
- recce_cloud/services/__init__.py +1 -0
- recce_cloud/services/diagnostic_service.py +380 -0
- recce_cloud/upload.py +214 -3
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.34.0.dist-info}/METADATA +117 -7
- recce_cloud-1.34.0.dist-info/RECORD +38 -0
- recce_cloud-1.32.0.dist-info/RECORD +0 -24
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.34.0.dist-info}/WHEEL +0 -0
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.34.0.dist-info}/entry_points.txt +0 -0
recce_cloud/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.34.0
|
recce_cloud/api/base.py
CHANGED
|
@@ -74,7 +74,7 @@ class BaseRecceCloudClient(ABC):
|
|
|
74
74
|
self,
|
|
75
75
|
branch: str,
|
|
76
76
|
adapter_type: str,
|
|
77
|
-
|
|
77
|
+
pr_number: Optional[int] = None,
|
|
78
78
|
commit_sha: Optional[str] = None,
|
|
79
79
|
session_type: Optional[str] = None,
|
|
80
80
|
) -> Dict:
|
|
@@ -84,9 +84,9 @@ class BaseRecceCloudClient(ABC):
|
|
|
84
84
|
Args:
|
|
85
85
|
branch: Branch name
|
|
86
86
|
adapter_type: DBT adapter type (e.g., 'postgres', 'snowflake', 'bigquery')
|
|
87
|
-
|
|
87
|
+
pr_number: Pull/Merge request number (PR/MR number) for PR sessions
|
|
88
88
|
commit_sha: Commit SHA (GitLab requires this)
|
|
89
|
-
session_type: Session type ("
|
|
89
|
+
session_type: Session type ("pr", "prod", "dev") - determines if pr_number is used
|
|
90
90
|
|
|
91
91
|
Returns:
|
|
92
92
|
Dictionary containing:
|
|
@@ -113,15 +113,15 @@ class BaseRecceCloudClient(ABC):
|
|
|
113
113
|
@abstractmethod
|
|
114
114
|
def get_session_download_urls(
|
|
115
115
|
self,
|
|
116
|
-
|
|
116
|
+
pr_number: Optional[int] = None,
|
|
117
117
|
session_type: Optional[str] = None,
|
|
118
118
|
) -> Dict:
|
|
119
119
|
"""
|
|
120
120
|
Get download URLs for artifacts from a session.
|
|
121
121
|
|
|
122
122
|
Args:
|
|
123
|
-
|
|
124
|
-
session_type: Session type ("
|
|
123
|
+
pr_number: Pull/Merge request number (PR/MR number) for PR sessions
|
|
124
|
+
session_type: Session type ("pr", "prod", "dev")
|
|
125
125
|
|
|
126
126
|
Returns:
|
|
127
127
|
Dictionary containing:
|
|
@@ -134,15 +134,15 @@ class BaseRecceCloudClient(ABC):
|
|
|
134
134
|
@abstractmethod
|
|
135
135
|
def delete_session(
|
|
136
136
|
self,
|
|
137
|
-
|
|
137
|
+
pr_number: Optional[int] = None,
|
|
138
138
|
session_type: Optional[str] = None,
|
|
139
139
|
) -> Dict:
|
|
140
140
|
"""
|
|
141
141
|
Delete a session.
|
|
142
142
|
|
|
143
143
|
Args:
|
|
144
|
-
|
|
145
|
-
session_type: Session type ("
|
|
144
|
+
pr_number: Pull/Merge request number (PR/MR number) for PR sessions
|
|
145
|
+
session_type: Session type ("pr", "prod") - "prod" deletes base session
|
|
146
146
|
|
|
147
147
|
Returns:
|
|
148
148
|
Dictionary containing:
|
recce_cloud/api/client.py
CHANGED
|
@@ -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
|
|
|
@@ -42,6 +42,24 @@ class RecceCloudClient:
|
|
|
42
42
|
}
|
|
43
43
|
return requests.request(method, url, headers=headers, **kwargs)
|
|
44
44
|
|
|
45
|
+
def _safe_get_error_detail(self, response, default: str) -> str:
|
|
46
|
+
"""Safely extract error detail from response JSON.
|
|
47
|
+
|
|
48
|
+
Some error responses may not have a valid JSON body (e.g., HTML error pages
|
|
49
|
+
from proxies), so we need to handle JSONDecodeError gracefully.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
response: The HTTP response object.
|
|
53
|
+
default: Default message to return if JSON parsing fails.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The error detail string from the response, or the default message.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
return response.json().get("detail", default)
|
|
60
|
+
except (json.JSONDecodeError, ValueError):
|
|
61
|
+
return default
|
|
62
|
+
|
|
45
63
|
def _replace_localhost_with_docker_internal(self, url: str) -> str:
|
|
46
64
|
"""Convert localhost URLs to docker internal URLs if running in Docker."""
|
|
47
65
|
if url is None:
|
|
@@ -75,7 +93,7 @@ class RecceCloudClient:
|
|
|
75
93
|
api_url = f"{self.base_url_v2}/sessions/{session_id}"
|
|
76
94
|
response = self._request("GET", api_url)
|
|
77
95
|
if response.status_code == 403:
|
|
78
|
-
return {"status": "error", "message":
|
|
96
|
+
return {"status": "error", "message": self._safe_get_error_detail(response, "Permission denied")}
|
|
79
97
|
if response.status_code != 200:
|
|
80
98
|
raise RecceCloudException(
|
|
81
99
|
reason=response.text,
|
|
@@ -181,7 +199,7 @@ class RecceCloudClient:
|
|
|
181
199
|
data = {"adapter_type": adapter_type}
|
|
182
200
|
response = self._request("PATCH", api_url, json=data)
|
|
183
201
|
if response.status_code == 403:
|
|
184
|
-
return {"status": "error", "message":
|
|
202
|
+
return {"status": "error", "message": self._safe_get_error_detail(response, "Permission denied")}
|
|
185
203
|
if response.status_code != 200:
|
|
186
204
|
raise RecceCloudException(
|
|
187
205
|
reason=response.text,
|
|
@@ -211,7 +229,7 @@ class RecceCloudClient:
|
|
|
211
229
|
return True
|
|
212
230
|
if response.status_code == 403:
|
|
213
231
|
raise RecceCloudException(
|
|
214
|
-
reason=
|
|
232
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
215
233
|
status_code=response.status_code,
|
|
216
234
|
)
|
|
217
235
|
if response.status_code == 404:
|
|
@@ -247,7 +265,7 @@ class RecceCloudClient:
|
|
|
247
265
|
return response.json()
|
|
248
266
|
if response.status_code == 403:
|
|
249
267
|
raise RecceCloudException(
|
|
250
|
-
reason=
|
|
268
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
251
269
|
status_code=response.status_code,
|
|
252
270
|
)
|
|
253
271
|
if response.status_code == 404:
|
|
@@ -260,6 +278,483 @@ class RecceCloudClient:
|
|
|
260
278
|
status_code=response.status_code,
|
|
261
279
|
)
|
|
262
280
|
|
|
281
|
+
def list_organizations(self) -> List[Dict[str, Any]]:
|
|
282
|
+
"""
|
|
283
|
+
List all organizations the user has access to.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of organization dictionaries with id, name, slug fields.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
RecceCloudException: If the API call fails.
|
|
290
|
+
"""
|
|
291
|
+
api_url = f"{self.base_url_v2}/organizations"
|
|
292
|
+
response = self._request("GET", api_url)
|
|
293
|
+
|
|
294
|
+
if response.status_code != 200:
|
|
295
|
+
raise RecceCloudException(
|
|
296
|
+
reason=response.text,
|
|
297
|
+
status_code=response.status_code,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
data = response.json()
|
|
301
|
+
return data.get("organizations", [])
|
|
302
|
+
|
|
303
|
+
def list_projects(self, org_id: str) -> List[Dict[str, Any]]:
|
|
304
|
+
"""
|
|
305
|
+
List all projects in an organization.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
org_id: Organization ID or slug.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of project dictionaries with id, name, slug fields.
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
RecceCloudException: If the API call fails.
|
|
315
|
+
"""
|
|
316
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects"
|
|
317
|
+
response = self._request("GET", api_url)
|
|
318
|
+
|
|
319
|
+
if response.status_code != 200:
|
|
320
|
+
raise RecceCloudException(
|
|
321
|
+
reason=response.text,
|
|
322
|
+
status_code=response.status_code,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
data = response.json()
|
|
326
|
+
return data.get("projects", [])
|
|
327
|
+
|
|
328
|
+
def get_organization(self, org_id: str) -> Optional[Dict[str, Any]]:
|
|
329
|
+
"""
|
|
330
|
+
Get a specific organization by ID or slug.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
org_id: Organization ID or slug.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Organization dictionary, or None if not found.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
RecceCloudException: If the API call fails.
|
|
340
|
+
"""
|
|
341
|
+
orgs = self.list_organizations()
|
|
342
|
+
for org in orgs:
|
|
343
|
+
# Compare as strings to handle both int and str IDs
|
|
344
|
+
if str(org.get("id")) == str(org_id) or org.get("slug") == org_id or org.get("name") == org_id:
|
|
345
|
+
return org
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
def get_project(self, org_id: str, project_id: str) -> Optional[Dict[str, Any]]:
|
|
349
|
+
"""
|
|
350
|
+
Get a specific project by ID or slug.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
org_id: Organization ID or slug.
|
|
354
|
+
project_id: Project ID or slug.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Project dictionary, or None if not found.
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
RecceCloudException: If the API call fails.
|
|
361
|
+
"""
|
|
362
|
+
projects = self.list_projects(org_id)
|
|
363
|
+
for project in projects:
|
|
364
|
+
# Compare as strings to handle both int and str IDs
|
|
365
|
+
if (
|
|
366
|
+
str(project.get("id")) == str(project_id)
|
|
367
|
+
or project.get("slug") == project_id
|
|
368
|
+
or project.get("name") == project_id
|
|
369
|
+
):
|
|
370
|
+
return project
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
def list_sessions(
|
|
374
|
+
self,
|
|
375
|
+
org_id: str,
|
|
376
|
+
project_id: str,
|
|
377
|
+
session_name: Optional[str] = None,
|
|
378
|
+
session_type: Optional[str] = None,
|
|
379
|
+
branch: Optional[str] = None,
|
|
380
|
+
limit: Optional[int] = None,
|
|
381
|
+
offset: Optional[int] = None,
|
|
382
|
+
) -> List[Dict[str, Any]]:
|
|
383
|
+
"""
|
|
384
|
+
List sessions in a project with optional filtering.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
org_id: Organization ID or slug.
|
|
388
|
+
project_id: Project ID or slug.
|
|
389
|
+
session_name: Filter by session name (exact match).
|
|
390
|
+
session_type: Filter by session type (e.g., "pr", "prod", "manual").
|
|
391
|
+
branch: Filter by branch name (exact match).
|
|
392
|
+
limit: Maximum number of results to return (1-1000).
|
|
393
|
+
offset: Number of results to skip for pagination.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
List of session dictionaries.
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
RecceCloudException: If the API call fails.
|
|
400
|
+
"""
|
|
401
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
|
|
402
|
+
params = {}
|
|
403
|
+
if session_name:
|
|
404
|
+
params["name"] = session_name
|
|
405
|
+
if session_type:
|
|
406
|
+
params["type"] = session_type
|
|
407
|
+
if branch:
|
|
408
|
+
params["branch"] = branch
|
|
409
|
+
if limit is not None:
|
|
410
|
+
params["limit"] = limit
|
|
411
|
+
if offset is not None:
|
|
412
|
+
params["offset"] = offset
|
|
413
|
+
|
|
414
|
+
response = self._request("GET", api_url, params=params if params else None)
|
|
415
|
+
|
|
416
|
+
if response.status_code == 404:
|
|
417
|
+
raise RecceCloudException(
|
|
418
|
+
reason="Organization or project not found",
|
|
419
|
+
status_code=response.status_code,
|
|
420
|
+
)
|
|
421
|
+
if response.status_code != 200:
|
|
422
|
+
raise RecceCloudException(
|
|
423
|
+
reason=response.text,
|
|
424
|
+
status_code=response.status_code,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
data = response.json()
|
|
428
|
+
return data.get("sessions", [])
|
|
429
|
+
|
|
430
|
+
def get_session_by_name(
|
|
431
|
+
self,
|
|
432
|
+
org_id: str,
|
|
433
|
+
project_id: str,
|
|
434
|
+
session_name: str,
|
|
435
|
+
) -> Optional[Dict[str, Any]]:
|
|
436
|
+
"""
|
|
437
|
+
Get a session by its name.
|
|
438
|
+
|
|
439
|
+
Uses the list sessions endpoint with filtering to find a session
|
|
440
|
+
by name, which is unique within a project.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
org_id: Organization ID or slug.
|
|
444
|
+
project_id: Project ID or slug.
|
|
445
|
+
session_name: The session name to look up.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
Session dictionary if found, None otherwise.
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
RecceCloudException: If the API call fails.
|
|
452
|
+
"""
|
|
453
|
+
sessions = self.list_sessions(
|
|
454
|
+
org_id=org_id,
|
|
455
|
+
project_id=project_id,
|
|
456
|
+
session_name=session_name,
|
|
457
|
+
limit=1,
|
|
458
|
+
)
|
|
459
|
+
if sessions:
|
|
460
|
+
return sessions[0]
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
def create_session(
|
|
464
|
+
self,
|
|
465
|
+
org_id: str,
|
|
466
|
+
project_id: str,
|
|
467
|
+
session_name: str,
|
|
468
|
+
adapter_type: Optional[str] = None,
|
|
469
|
+
session_type: str = "manual",
|
|
470
|
+
) -> Dict[str, Any]:
|
|
471
|
+
"""
|
|
472
|
+
Create a new session with the given name.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
org_id: Organization ID or slug.
|
|
476
|
+
project_id: Project ID or slug.
|
|
477
|
+
session_name: The name for the new session.
|
|
478
|
+
adapter_type: dbt adapter type (e.g., "postgres", "snowflake").
|
|
479
|
+
session_type: Session type (default: "manual").
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Created session dictionary with id, name, and other fields.
|
|
483
|
+
|
|
484
|
+
Raises:
|
|
485
|
+
RecceCloudException: If the API call fails or session creation fails.
|
|
486
|
+
"""
|
|
487
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
|
|
488
|
+
data = {
|
|
489
|
+
"name": session_name,
|
|
490
|
+
"type": session_type,
|
|
491
|
+
}
|
|
492
|
+
if adapter_type:
|
|
493
|
+
data["adapter_type"] = adapter_type
|
|
494
|
+
|
|
495
|
+
response = self._request("POST", api_url, json=data)
|
|
496
|
+
|
|
497
|
+
if response.status_code == 404:
|
|
498
|
+
raise RecceCloudException(
|
|
499
|
+
reason="Organization or project not found",
|
|
500
|
+
status_code=response.status_code,
|
|
501
|
+
)
|
|
502
|
+
if response.status_code == 409:
|
|
503
|
+
raise RecceCloudException(
|
|
504
|
+
reason=f"Session with name '{session_name}' already exists",
|
|
505
|
+
status_code=response.status_code,
|
|
506
|
+
)
|
|
507
|
+
if response.status_code == 403:
|
|
508
|
+
raise RecceCloudException(
|
|
509
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
510
|
+
status_code=response.status_code,
|
|
511
|
+
)
|
|
512
|
+
if response.status_code not in [200, 201]:
|
|
513
|
+
raise RecceCloudException(
|
|
514
|
+
reason=response.text,
|
|
515
|
+
status_code=response.status_code,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
result = response.json()
|
|
519
|
+
# Handle both direct session response and wrapped response
|
|
520
|
+
if "session" in result:
|
|
521
|
+
return result["session"]
|
|
522
|
+
return result
|
|
523
|
+
|
|
524
|
+
def check_prerequisites(
|
|
525
|
+
self,
|
|
526
|
+
org_id: str,
|
|
527
|
+
project_id: str,
|
|
528
|
+
session_id: str,
|
|
529
|
+
) -> Dict[str, Any]:
|
|
530
|
+
"""
|
|
531
|
+
Check prerequisites for data review generation.
|
|
532
|
+
|
|
533
|
+
This calls the backend API to verify:
|
|
534
|
+
1. Session exists and belongs to the project
|
|
535
|
+
2. Session has artifacts uploaded (adapter_type is set)
|
|
536
|
+
3. Base session exists for the project
|
|
537
|
+
4. Base session has artifacts uploaded
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
org_id: Organization ID or slug.
|
|
541
|
+
project_id: Project ID or slug.
|
|
542
|
+
session_id: Session ID to check.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
dict with:
|
|
546
|
+
- success: bool
|
|
547
|
+
- session_id: str
|
|
548
|
+
- session_name: str
|
|
549
|
+
- adapter_type: str or None
|
|
550
|
+
- has_base_session: bool
|
|
551
|
+
- base_session_has_artifacts: bool
|
|
552
|
+
- is_ready: bool
|
|
553
|
+
- reason: str or None (explains why not ready)
|
|
554
|
+
|
|
555
|
+
Raises:
|
|
556
|
+
RecceCloudException: If the API call fails.
|
|
557
|
+
"""
|
|
558
|
+
api_url = (
|
|
559
|
+
f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/check-prerequisites"
|
|
560
|
+
)
|
|
561
|
+
response = self._request("GET", api_url)
|
|
562
|
+
|
|
563
|
+
if response.status_code == 404:
|
|
564
|
+
error_detail = "Session or project not found"
|
|
565
|
+
try:
|
|
566
|
+
error_detail = response.json().get("detail", error_detail)
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
raise RecceCloudException(
|
|
570
|
+
reason=error_detail,
|
|
571
|
+
status_code=response.status_code,
|
|
572
|
+
)
|
|
573
|
+
if response.status_code == 400:
|
|
574
|
+
raise RecceCloudException(
|
|
575
|
+
reason=self._safe_get_error_detail(response, "Bad request"),
|
|
576
|
+
status_code=response.status_code,
|
|
577
|
+
)
|
|
578
|
+
if response.status_code == 403:
|
|
579
|
+
raise RecceCloudException(
|
|
580
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
581
|
+
status_code=response.status_code,
|
|
582
|
+
)
|
|
583
|
+
if response.status_code != 200:
|
|
584
|
+
raise RecceCloudException(
|
|
585
|
+
reason=response.text,
|
|
586
|
+
status_code=response.status_code,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return response.json()
|
|
590
|
+
|
|
591
|
+
def generate_data_review(
|
|
592
|
+
self,
|
|
593
|
+
org_id: str,
|
|
594
|
+
project_id: str,
|
|
595
|
+
session_id: str,
|
|
596
|
+
regenerate: bool = False,
|
|
597
|
+
) -> Dict[str, Any]:
|
|
598
|
+
"""
|
|
599
|
+
Trigger data review generation for a session.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
org_id: Organization ID or slug.
|
|
603
|
+
project_id: Project ID or slug.
|
|
604
|
+
session_id: Session ID to generate review for.
|
|
605
|
+
regenerate: If True, regenerate even if a review already exists.
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
dict with task_id if a new task was created, or empty if review already exists.
|
|
609
|
+
|
|
610
|
+
Raises:
|
|
611
|
+
RecceCloudException: If the API call fails.
|
|
612
|
+
"""
|
|
613
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/recce_summary"
|
|
614
|
+
data = {"regenerate": regenerate}
|
|
615
|
+
|
|
616
|
+
response = self._request("POST", api_url, json=data)
|
|
617
|
+
|
|
618
|
+
if response.status_code == 404:
|
|
619
|
+
raise RecceCloudException(
|
|
620
|
+
reason="Session not found",
|
|
621
|
+
status_code=response.status_code,
|
|
622
|
+
)
|
|
623
|
+
if response.status_code == 403:
|
|
624
|
+
raise RecceCloudException(
|
|
625
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
626
|
+
status_code=response.status_code,
|
|
627
|
+
)
|
|
628
|
+
if response.status_code == 400:
|
|
629
|
+
raise RecceCloudException(
|
|
630
|
+
reason=self._safe_get_error_detail(response, "Bad request"),
|
|
631
|
+
status_code=response.status_code,
|
|
632
|
+
)
|
|
633
|
+
if response.status_code not in [200, 201, 202]:
|
|
634
|
+
raise RecceCloudException(
|
|
635
|
+
reason=response.text,
|
|
636
|
+
status_code=response.status_code,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
return response.json()
|
|
640
|
+
|
|
641
|
+
def get_data_review(
|
|
642
|
+
self,
|
|
643
|
+
org_id: str,
|
|
644
|
+
project_id: str,
|
|
645
|
+
session_id: str,
|
|
646
|
+
) -> Optional[Dict[str, Any]]:
|
|
647
|
+
"""
|
|
648
|
+
Get the existing data review for a session.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
org_id: Organization ID or slug.
|
|
652
|
+
project_id: Project ID or slug.
|
|
653
|
+
session_id: Session ID to get review for.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
dict with session_id, session_name, summary (content), trace_id if found.
|
|
657
|
+
None if no review exists.
|
|
658
|
+
|
|
659
|
+
Raises:
|
|
660
|
+
RecceCloudException: If the API call fails.
|
|
661
|
+
"""
|
|
662
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/recce_summary"
|
|
663
|
+
response = self._request("GET", api_url)
|
|
664
|
+
|
|
665
|
+
if response.status_code == 404:
|
|
666
|
+
# No review exists for this session
|
|
667
|
+
return None
|
|
668
|
+
if response.status_code == 403:
|
|
669
|
+
raise RecceCloudException(
|
|
670
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
671
|
+
status_code=response.status_code,
|
|
672
|
+
)
|
|
673
|
+
if response.status_code != 200:
|
|
674
|
+
raise RecceCloudException(
|
|
675
|
+
reason=response.text,
|
|
676
|
+
status_code=response.status_code,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
return response.json()
|
|
680
|
+
|
|
681
|
+
def get_running_task(
|
|
682
|
+
self,
|
|
683
|
+
org_id: str,
|
|
684
|
+
project_id: str,
|
|
685
|
+
session_id: str,
|
|
686
|
+
) -> Optional[Dict[str, Any]]:
|
|
687
|
+
"""
|
|
688
|
+
Get the currently running task for a session.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
org_id: Organization ID or slug.
|
|
692
|
+
project_id: Project ID or slug.
|
|
693
|
+
session_id: Session ID to check.
|
|
694
|
+
|
|
695
|
+
Returns:
|
|
696
|
+
dict with task_id and status if a task is running, None otherwise.
|
|
697
|
+
|
|
698
|
+
Raises:
|
|
699
|
+
RecceCloudException: If the API call fails.
|
|
700
|
+
"""
|
|
701
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/running_task"
|
|
702
|
+
response = self._request("GET", api_url)
|
|
703
|
+
|
|
704
|
+
if response.status_code == 404:
|
|
705
|
+
return None
|
|
706
|
+
if response.status_code == 403:
|
|
707
|
+
raise RecceCloudException(
|
|
708
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
709
|
+
status_code=response.status_code,
|
|
710
|
+
)
|
|
711
|
+
if response.status_code != 200:
|
|
712
|
+
raise RecceCloudException(
|
|
713
|
+
reason=response.text,
|
|
714
|
+
status_code=response.status_code,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
data = response.json()
|
|
718
|
+
# Return None if no task is running
|
|
719
|
+
if data.get("task_id") is None:
|
|
720
|
+
return None
|
|
721
|
+
return data
|
|
722
|
+
|
|
723
|
+
def get_task_status(self, org_id: str, task_id: str) -> Dict[str, Any]:
|
|
724
|
+
"""
|
|
725
|
+
Get the status of a task by ID.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
org_id: Organization ID or slug.
|
|
729
|
+
task_id: Task ID to check.
|
|
730
|
+
|
|
731
|
+
Returns:
|
|
732
|
+
dict with id, command, status, created_at, started_at, finished_at, metadata.
|
|
733
|
+
|
|
734
|
+
Raises:
|
|
735
|
+
RecceCloudException: If the API call fails or task not found.
|
|
736
|
+
"""
|
|
737
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/tasks/{task_id}/status"
|
|
738
|
+
response = self._request("GET", api_url)
|
|
739
|
+
|
|
740
|
+
if response.status_code == 404:
|
|
741
|
+
raise RecceCloudException(
|
|
742
|
+
reason="Task not found",
|
|
743
|
+
status_code=response.status_code,
|
|
744
|
+
)
|
|
745
|
+
if response.status_code == 403:
|
|
746
|
+
raise RecceCloudException(
|
|
747
|
+
reason=self._safe_get_error_detail(response, "Permission denied"),
|
|
748
|
+
status_code=response.status_code,
|
|
749
|
+
)
|
|
750
|
+
if response.status_code != 200:
|
|
751
|
+
raise RecceCloudException(
|
|
752
|
+
reason=response.text,
|
|
753
|
+
status_code=response.status_code,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
return response.json()
|
|
757
|
+
|
|
263
758
|
|
|
264
759
|
class ReportClient:
|
|
265
760
|
"""Client for fetching reports from Recce Cloud API."""
|