recce-cloud 1.32.0__py3-none-any.whl → 1.33.1__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/client.py +245 -2
- 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/cli.py +661 -33
- 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 +137 -0
- recce_cloud/services/__init__.py +1 -0
- recce_cloud/services/diagnostic_service.py +380 -0
- recce_cloud/upload.py +211 -0
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/METADATA +112 -2
- recce_cloud-1.33.1.dist-info/RECORD +37 -0
- recce_cloud-1.32.0.dist-info/RECORD +0 -24
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/WHEEL +0 -0
- {recce_cloud-1.32.0.dist-info → recce_cloud-1.33.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diagnostic service for checking Recce Cloud setup and configuration.
|
|
3
|
+
|
|
4
|
+
This service contains the business logic for health checks, separated from
|
|
5
|
+
CLI presentation concerns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CheckStatus(Enum):
|
|
19
|
+
"""Status of a diagnostic check."""
|
|
20
|
+
|
|
21
|
+
PASS = "pass"
|
|
22
|
+
FAIL = "fail"
|
|
23
|
+
SKIP = "skip" # When check cannot be performed due to missing prerequisites
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CheckResult:
|
|
28
|
+
"""Result of a single diagnostic check."""
|
|
29
|
+
|
|
30
|
+
status: CheckStatus
|
|
31
|
+
message: Optional[str] = None
|
|
32
|
+
suggestion: Optional[str] = None
|
|
33
|
+
details: dict = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def passed(self) -> bool:
|
|
37
|
+
return self.status == CheckStatus.PASS
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
"""Convert to dictionary for JSON serialization."""
|
|
41
|
+
result = {
|
|
42
|
+
"status": self.status.value,
|
|
43
|
+
"message": self.message,
|
|
44
|
+
"suggestion": self.suggestion,
|
|
45
|
+
}
|
|
46
|
+
result.update(self.details)
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DiagnosticResults:
|
|
52
|
+
"""Aggregated results of all diagnostic checks."""
|
|
53
|
+
|
|
54
|
+
login: CheckResult
|
|
55
|
+
project_binding: CheckResult
|
|
56
|
+
production_metadata: CheckResult
|
|
57
|
+
dev_session: CheckResult
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def all_passed(self) -> bool:
|
|
61
|
+
return all(
|
|
62
|
+
[
|
|
63
|
+
self.login.passed,
|
|
64
|
+
self.project_binding.passed,
|
|
65
|
+
self.production_metadata.passed,
|
|
66
|
+
self.dev_session.passed,
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def passed_count(self) -> int:
|
|
72
|
+
return sum(
|
|
73
|
+
1
|
|
74
|
+
for check in [self.login, self.project_binding, self.production_metadata, self.dev_session]
|
|
75
|
+
if check.passed
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def total_count(self) -> int:
|
|
80
|
+
return 4
|
|
81
|
+
|
|
82
|
+
def to_dict(self) -> dict:
|
|
83
|
+
"""Convert to dictionary for JSON serialization."""
|
|
84
|
+
return {
|
|
85
|
+
"login": self.login.to_dict(),
|
|
86
|
+
"project_binding": self.project_binding.to_dict(),
|
|
87
|
+
"production_metadata": self.production_metadata.to_dict(),
|
|
88
|
+
"dev_session": self.dev_session.to_dict(),
|
|
89
|
+
"all_passed": self.all_passed,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DiagnosticService:
|
|
94
|
+
"""
|
|
95
|
+
Service for performing Recce Cloud diagnostic checks.
|
|
96
|
+
|
|
97
|
+
This service checks:
|
|
98
|
+
1. Login status - Is the user authenticated?
|
|
99
|
+
2. Project binding - Is the project configured?
|
|
100
|
+
3. Production metadata - Does a production session exist?
|
|
101
|
+
4. Dev session - Does a development session exist?
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self):
|
|
105
|
+
self._token: Optional[str] = None
|
|
106
|
+
self._org: Optional[str] = None
|
|
107
|
+
self._project: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
def run_all_checks(self) -> DiagnosticResults:
|
|
110
|
+
"""
|
|
111
|
+
Run all diagnostic checks and return aggregated results.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
DiagnosticResults containing the status of all checks.
|
|
115
|
+
"""
|
|
116
|
+
login_result = self._check_login()
|
|
117
|
+
project_result = self._check_project_binding()
|
|
118
|
+
|
|
119
|
+
# Session checks depend on login and project binding
|
|
120
|
+
if login_result.passed and project_result.passed:
|
|
121
|
+
prod_result, dev_result = self._check_sessions()
|
|
122
|
+
else:
|
|
123
|
+
skip_message = "Cannot check - requires login and project binding"
|
|
124
|
+
prod_result = CheckResult(
|
|
125
|
+
status=CheckStatus.SKIP,
|
|
126
|
+
message=skip_message,
|
|
127
|
+
)
|
|
128
|
+
dev_result = CheckResult(
|
|
129
|
+
status=CheckStatus.SKIP,
|
|
130
|
+
message=skip_message,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return DiagnosticResults(
|
|
134
|
+
login=login_result,
|
|
135
|
+
project_binding=project_result,
|
|
136
|
+
production_metadata=prod_result,
|
|
137
|
+
dev_session=dev_result,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def _check_login(self) -> CheckResult:
|
|
141
|
+
"""Check if user is logged in with a valid token."""
|
|
142
|
+
from recce_cloud.auth.login import check_login_status
|
|
143
|
+
from recce_cloud.auth.profile import get_api_token
|
|
144
|
+
|
|
145
|
+
self._token = os.getenv("RECCE_API_TOKEN") or get_api_token()
|
|
146
|
+
|
|
147
|
+
if not self._token:
|
|
148
|
+
return CheckResult(
|
|
149
|
+
status=CheckStatus.FAIL,
|
|
150
|
+
message="Not logged in",
|
|
151
|
+
suggestion="Run 'recce-cloud login' to authenticate",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
is_logged_in, email = check_login_status()
|
|
155
|
+
|
|
156
|
+
if is_logged_in:
|
|
157
|
+
return CheckResult(
|
|
158
|
+
status=CheckStatus.PASS,
|
|
159
|
+
details={"email": email},
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
return CheckResult(
|
|
163
|
+
status=CheckStatus.FAIL,
|
|
164
|
+
message="Token invalid or expired",
|
|
165
|
+
suggestion="Run 'recce-cloud login' to authenticate",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _check_project_binding(self) -> CheckResult:
|
|
169
|
+
"""Check if project is bound to Recce Cloud."""
|
|
170
|
+
from recce_cloud.config.project_config import get_project_binding
|
|
171
|
+
|
|
172
|
+
binding = get_project_binding()
|
|
173
|
+
|
|
174
|
+
if binding:
|
|
175
|
+
self._org = binding.get("org")
|
|
176
|
+
self._project = binding.get("project")
|
|
177
|
+
return CheckResult(
|
|
178
|
+
status=CheckStatus.PASS,
|
|
179
|
+
details={
|
|
180
|
+
"org": self._org,
|
|
181
|
+
"project": self._project,
|
|
182
|
+
"source": "config_file",
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Check environment variables as fallback
|
|
187
|
+
env_org = os.environ.get("RECCE_ORG")
|
|
188
|
+
env_project = os.environ.get("RECCE_PROJECT")
|
|
189
|
+
|
|
190
|
+
if env_org and env_project:
|
|
191
|
+
self._org = env_org
|
|
192
|
+
self._project = env_project
|
|
193
|
+
return CheckResult(
|
|
194
|
+
status=CheckStatus.PASS,
|
|
195
|
+
details={
|
|
196
|
+
"org": self._org,
|
|
197
|
+
"project": self._project,
|
|
198
|
+
"source": "env_vars",
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return CheckResult(
|
|
203
|
+
status=CheckStatus.FAIL,
|
|
204
|
+
message="No project binding found",
|
|
205
|
+
suggestion="Run 'recce-cloud init' to bind this directory to a project",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _check_sessions(self) -> tuple[CheckResult, CheckResult]:
|
|
209
|
+
"""
|
|
210
|
+
Check for production and development sessions.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Tuple of (production_result, dev_result)
|
|
214
|
+
"""
|
|
215
|
+
from recce_cloud.api.client import RecceCloudClient
|
|
216
|
+
from recce_cloud.api.exceptions import RecceCloudException
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
client = RecceCloudClient(self._token)
|
|
220
|
+
|
|
221
|
+
# Get org and project IDs
|
|
222
|
+
org_info = client.get_organization(self._org)
|
|
223
|
+
if not org_info:
|
|
224
|
+
raise RecceCloudException(f"Organization '{self._org}' not found", 404)
|
|
225
|
+
org_id = org_info.get("id")
|
|
226
|
+
if not org_id:
|
|
227
|
+
raise RecceCloudException(f"Organization '{self._org}' response missing ID", 500)
|
|
228
|
+
|
|
229
|
+
project_info = client.get_project(org_id, self._project)
|
|
230
|
+
if not project_info:
|
|
231
|
+
raise RecceCloudException(f"Project '{self._project}' not found", 404)
|
|
232
|
+
project_id = project_info.get("id")
|
|
233
|
+
if not project_id:
|
|
234
|
+
raise RecceCloudException(f"Project '{self._project}' response missing ID", 500)
|
|
235
|
+
|
|
236
|
+
# List sessions
|
|
237
|
+
sessions = client.list_sessions(org_id, project_id)
|
|
238
|
+
|
|
239
|
+
prod_session = None
|
|
240
|
+
dev_sessions = []
|
|
241
|
+
|
|
242
|
+
for s in sessions:
|
|
243
|
+
if s.get("is_base"):
|
|
244
|
+
prod_session = s
|
|
245
|
+
elif not s.get("pr_link"): # dev = not base and no PR link
|
|
246
|
+
dev_sessions.append(s)
|
|
247
|
+
|
|
248
|
+
# Check production
|
|
249
|
+
prod_result = self._evaluate_production_session(prod_session)
|
|
250
|
+
|
|
251
|
+
# Check dev
|
|
252
|
+
dev_result = self._evaluate_dev_sessions(dev_sessions)
|
|
253
|
+
|
|
254
|
+
return prod_result, dev_result
|
|
255
|
+
|
|
256
|
+
except RecceCloudException as e:
|
|
257
|
+
error_result = CheckResult(
|
|
258
|
+
status=CheckStatus.FAIL,
|
|
259
|
+
message=f"Failed to fetch sessions: {e}",
|
|
260
|
+
)
|
|
261
|
+
return error_result, error_result
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.debug("Unexpected error during session check: %s", e, exc_info=True)
|
|
265
|
+
error_result = CheckResult(
|
|
266
|
+
status=CheckStatus.FAIL,
|
|
267
|
+
message=f"Unexpected error: {e}",
|
|
268
|
+
suggestion="Check your network connection and try again.",
|
|
269
|
+
)
|
|
270
|
+
return error_result, error_result
|
|
271
|
+
|
|
272
|
+
def _evaluate_production_session(self, prod_session: Optional[dict]) -> CheckResult:
|
|
273
|
+
"""Evaluate the production session check."""
|
|
274
|
+
if not prod_session:
|
|
275
|
+
return CheckResult(
|
|
276
|
+
status=CheckStatus.FAIL,
|
|
277
|
+
message="No production artifacts found",
|
|
278
|
+
suggestion=(
|
|
279
|
+
"To upload production metadata:\n"
|
|
280
|
+
" 1. Check out your main branch:\n"
|
|
281
|
+
" $ git checkout main\n"
|
|
282
|
+
" 2. Generate and upload production artifacts:\n"
|
|
283
|
+
" $ dbt docs generate --target prod\n"
|
|
284
|
+
" $ recce-cloud upload --type prod"
|
|
285
|
+
),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Check if the session has actual data (adapter_type is not null)
|
|
289
|
+
# An empty session created by default will have adapter_type=null
|
|
290
|
+
if not prod_session.get("adapter_type"):
|
|
291
|
+
return CheckResult(
|
|
292
|
+
status=CheckStatus.FAIL,
|
|
293
|
+
message="Production session exists but has no data",
|
|
294
|
+
suggestion=(
|
|
295
|
+
"To upload production metadata:\n"
|
|
296
|
+
" 1. Check out your main branch:\n"
|
|
297
|
+
" $ git checkout main\n"
|
|
298
|
+
" 2. Generate and upload production artifacts:\n"
|
|
299
|
+
" $ dbt docs generate --target prod\n"
|
|
300
|
+
" $ recce-cloud upload --type prod"
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
session_name = prod_session.get("name") or "(unnamed)"
|
|
305
|
+
uploaded_at = prod_session.get("updated_at") or prod_session.get("created_at")
|
|
306
|
+
|
|
307
|
+
return CheckResult(
|
|
308
|
+
status=CheckStatus.PASS,
|
|
309
|
+
details={
|
|
310
|
+
"session_name": session_name,
|
|
311
|
+
"uploaded_at": uploaded_at,
|
|
312
|
+
"relative_time": self._format_relative_time(uploaded_at),
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _evaluate_dev_sessions(self, dev_sessions: list) -> CheckResult:
|
|
317
|
+
"""Evaluate the dev session check."""
|
|
318
|
+
if not dev_sessions:
|
|
319
|
+
return CheckResult(
|
|
320
|
+
status=CheckStatus.FAIL,
|
|
321
|
+
message="No dev session found",
|
|
322
|
+
suggestion=(
|
|
323
|
+
"To create and upload a dev session:\n"
|
|
324
|
+
" 1. Check out a feature branch with your changes:\n"
|
|
325
|
+
" $ git checkout -b my-feature-branch\n"
|
|
326
|
+
" (make some changes to your dbt models)\n"
|
|
327
|
+
" 2. Generate and upload dev artifacts:\n"
|
|
328
|
+
" $ dbt docs generate\n"
|
|
329
|
+
" $ recce-cloud upload --session-name my-feature-branch"
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Sort by updated_at/created_at to get most recent
|
|
334
|
+
dev_sessions.sort(
|
|
335
|
+
key=lambda x: x.get("updated_at") or x.get("created_at") or "",
|
|
336
|
+
reverse=True,
|
|
337
|
+
)
|
|
338
|
+
latest_dev = dev_sessions[0]
|
|
339
|
+
session_name = latest_dev.get("name") or "(unnamed)"
|
|
340
|
+
uploaded_at = latest_dev.get("updated_at") or latest_dev.get("created_at")
|
|
341
|
+
|
|
342
|
+
return CheckResult(
|
|
343
|
+
status=CheckStatus.PASS,
|
|
344
|
+
details={
|
|
345
|
+
"session_name": session_name,
|
|
346
|
+
"uploaded_at": uploaded_at,
|
|
347
|
+
"relative_time": self._format_relative_time(uploaded_at),
|
|
348
|
+
},
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _format_relative_time(iso_timestamp: Optional[str]) -> Optional[str]:
|
|
353
|
+
"""Format an ISO timestamp as a human-readable relative time."""
|
|
354
|
+
if not iso_timestamp:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
# Parse ISO timestamp
|
|
359
|
+
if iso_timestamp.endswith("Z"):
|
|
360
|
+
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
|
|
361
|
+
else:
|
|
362
|
+
dt = datetime.fromisoformat(iso_timestamp)
|
|
363
|
+
|
|
364
|
+
now = datetime.now(timezone.utc)
|
|
365
|
+
diff = now - dt
|
|
366
|
+
|
|
367
|
+
seconds = diff.total_seconds()
|
|
368
|
+
if seconds < 60:
|
|
369
|
+
return "just now"
|
|
370
|
+
elif seconds < 3600:
|
|
371
|
+
mins = int(seconds / 60)
|
|
372
|
+
return f"{mins}m ago"
|
|
373
|
+
elif seconds < 86400:
|
|
374
|
+
hours = int(seconds / 3600)
|
|
375
|
+
return f"{hours}h ago"
|
|
376
|
+
else:
|
|
377
|
+
days = int(seconds / 86400)
|
|
378
|
+
return f"{days}d ago"
|
|
379
|
+
except (ValueError, TypeError):
|
|
380
|
+
return None
|
recce_cloud/upload.py
CHANGED
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
Upload helper functions for recce-cloud CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
7
8
|
|
|
9
|
+
import click
|
|
8
10
|
import requests
|
|
9
11
|
|
|
10
12
|
from recce_cloud.api.client import RecceCloudClient
|
|
11
13
|
from recce_cloud.api.exceptions import RecceCloudException
|
|
12
14
|
from recce_cloud.api.factory import create_platform_client
|
|
15
|
+
from recce_cloud.config.resolver import ConfigurationError, resolve_config
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def upload_to_existing_session(
|
|
@@ -225,3 +230,209 @@ def upload_with_platform_apis(
|
|
|
225
230
|
console.print(f"Change request: {ci_info.cr_url}")
|
|
226
231
|
|
|
227
232
|
sys.exit(0)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def upload_with_session_name(
|
|
236
|
+
console,
|
|
237
|
+
token: str,
|
|
238
|
+
session_name: str,
|
|
239
|
+
manifest_path: str,
|
|
240
|
+
catalog_path: str,
|
|
241
|
+
adapter_type: str,
|
|
242
|
+
target_path: str,
|
|
243
|
+
skip_confirmation: bool = False,
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Upload artifacts to a session identified by name.
|
|
247
|
+
|
|
248
|
+
If the session exists, uploads to it. If not, prompts to create a new session
|
|
249
|
+
(unless skip_confirmation is True, which auto-creates).
|
|
250
|
+
|
|
251
|
+
This workflow requires org/project configuration from either:
|
|
252
|
+
- Local config file (.recce/config) via 'recce-cloud init'
|
|
253
|
+
- Environment variables (RECCE_ORG, RECCE_PROJECT)
|
|
254
|
+
"""
|
|
255
|
+
# 1. Resolve org/project configuration
|
|
256
|
+
console.rule("Session Name Resolution", style="blue")
|
|
257
|
+
try:
|
|
258
|
+
config = resolve_config()
|
|
259
|
+
org = config.org
|
|
260
|
+
project = config.project
|
|
261
|
+
console.print(f"[cyan]Organization:[/cyan] {org}")
|
|
262
|
+
console.print(f"[cyan]Project:[/cyan] {project}")
|
|
263
|
+
console.print(f"[cyan]Config Source:[/cyan] {config.source}")
|
|
264
|
+
except ConfigurationError as e:
|
|
265
|
+
console.print("[red]Error:[/red] Could not resolve org/project configuration")
|
|
266
|
+
console.print(f"Reason: {e}")
|
|
267
|
+
console.print()
|
|
268
|
+
console.print("To use --session-name, you need to either:")
|
|
269
|
+
console.print(" 1. Run 'recce-cloud init' to bind this directory to a project")
|
|
270
|
+
console.print(" 2. Set RECCE_ORG and RECCE_PROJECT environment variables")
|
|
271
|
+
sys.exit(2)
|
|
272
|
+
|
|
273
|
+
# 2. Initialize API client
|
|
274
|
+
try:
|
|
275
|
+
client = RecceCloudClient(token)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
console.print("[red]Error:[/red] Failed to initialize API client")
|
|
278
|
+
console.print(f"Reason: {e}")
|
|
279
|
+
sys.exit(2)
|
|
280
|
+
|
|
281
|
+
# 3. Resolve org/project IDs (they might be slugs/names in config)
|
|
282
|
+
try:
|
|
283
|
+
org_info = client.get_organization(org)
|
|
284
|
+
if not org_info:
|
|
285
|
+
console.print(f"[red]Error:[/red] Organization '{org}' not found or you don't have access")
|
|
286
|
+
sys.exit(2)
|
|
287
|
+
org_id = org_info.get("id")
|
|
288
|
+
if not org_id:
|
|
289
|
+
console.print(f"[red]Error:[/red] Organization '{org}' response missing ID")
|
|
290
|
+
sys.exit(2)
|
|
291
|
+
|
|
292
|
+
project_info = client.get_project(org_id, project)
|
|
293
|
+
if not project_info:
|
|
294
|
+
console.print(f"[red]Error:[/red] Project '{project}' not found in organization '{org}'")
|
|
295
|
+
sys.exit(2)
|
|
296
|
+
project_id = project_info.get("id")
|
|
297
|
+
if not project_id:
|
|
298
|
+
console.print(f"[red]Error:[/red] Project '{project}' response missing ID")
|
|
299
|
+
sys.exit(2)
|
|
300
|
+
except RecceCloudException as e:
|
|
301
|
+
console.print("[red]Error:[/red] Failed to resolve organization/project")
|
|
302
|
+
console.print(f"Reason: {e.reason}")
|
|
303
|
+
sys.exit(2)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.debug("Failed to resolve organization/project: %s", e, exc_info=True)
|
|
306
|
+
console.print("[red]Error:[/red] Failed to resolve organization/project")
|
|
307
|
+
console.print(f" Reason: {e}")
|
|
308
|
+
console.print(" Check your authentication and network connection.")
|
|
309
|
+
sys.exit(2)
|
|
310
|
+
|
|
311
|
+
# 4. Look up session by name
|
|
312
|
+
console.print(f'Looking up session "{session_name}"...')
|
|
313
|
+
try:
|
|
314
|
+
existing_session = client.get_session_by_name(org_id, project_id, session_name)
|
|
315
|
+
except RecceCloudException as e:
|
|
316
|
+
console.print("[red]Error:[/red] Failed to look up session")
|
|
317
|
+
console.print(f"Reason: {e.reason}")
|
|
318
|
+
sys.exit(2)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.debug("Failed to look up session: %s", e, exc_info=True)
|
|
321
|
+
console.print("[red]Error:[/red] Failed to look up session")
|
|
322
|
+
console.print(f" Reason: {e}")
|
|
323
|
+
console.print(" Check your network connection and try again.")
|
|
324
|
+
sys.exit(2)
|
|
325
|
+
|
|
326
|
+
session_id = None
|
|
327
|
+
if existing_session:
|
|
328
|
+
# Session found, use it
|
|
329
|
+
session_id = existing_session.get("id")
|
|
330
|
+
console.print(f'[green]Found existing session:[/green] "{session_name}" (ID: {session_id})')
|
|
331
|
+
else:
|
|
332
|
+
# Session not found, prompt to create
|
|
333
|
+
console.print(f'[yellow]Session "{session_name}" not found[/yellow]')
|
|
334
|
+
|
|
335
|
+
if skip_confirmation:
|
|
336
|
+
# Auto-create with --yes flag
|
|
337
|
+
console.print("Creating new session (--yes flag specified)...")
|
|
338
|
+
else:
|
|
339
|
+
# Interactive confirmation
|
|
340
|
+
console.print()
|
|
341
|
+
if not click.confirm(f'Create new session "{session_name}"?', default=True):
|
|
342
|
+
console.print("[yellow]Upload cancelled[/yellow]")
|
|
343
|
+
sys.exit(0)
|
|
344
|
+
|
|
345
|
+
# Create the session
|
|
346
|
+
try:
|
|
347
|
+
new_session = client.create_session(
|
|
348
|
+
org_id=org_id,
|
|
349
|
+
project_id=project_id,
|
|
350
|
+
session_name=session_name,
|
|
351
|
+
adapter_type=adapter_type,
|
|
352
|
+
session_type="manual",
|
|
353
|
+
)
|
|
354
|
+
session_id = new_session.get("id")
|
|
355
|
+
console.print(f'[green]Created new session:[/green] "{session_name}" (ID: {session_id})')
|
|
356
|
+
except RecceCloudException as e:
|
|
357
|
+
console.print("[red]Error:[/red] Failed to create session")
|
|
358
|
+
console.print(f"Reason: {e.reason}")
|
|
359
|
+
sys.exit(4)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
console.print("[red]Error:[/red] Failed to create session")
|
|
362
|
+
console.print(f"Reason: {e}")
|
|
363
|
+
sys.exit(4)
|
|
364
|
+
|
|
365
|
+
# 5. Get presigned URLs and upload
|
|
366
|
+
console.rule("Uploading Artifacts", style="blue")
|
|
367
|
+
try:
|
|
368
|
+
presigned_urls = client.get_upload_urls_by_session_id(org_id, project_id, session_id)
|
|
369
|
+
except RecceCloudException as e:
|
|
370
|
+
console.print("[red]Error:[/red] Failed to get upload URLs")
|
|
371
|
+
console.print(f"Reason: {e.reason}")
|
|
372
|
+
sys.exit(4)
|
|
373
|
+
except Exception as e:
|
|
374
|
+
console.print("[red]Error:[/red] Failed to get upload URLs")
|
|
375
|
+
console.print(f"Reason: {e}")
|
|
376
|
+
sys.exit(4)
|
|
377
|
+
|
|
378
|
+
# Upload manifest.json
|
|
379
|
+
console.print(f'Uploading manifest from path "{manifest_path}"')
|
|
380
|
+
try:
|
|
381
|
+
with open(manifest_path, "rb") as f:
|
|
382
|
+
response = requests.put(presigned_urls["manifest_url"], data=f.read())
|
|
383
|
+
if response.status_code not in [200, 204]:
|
|
384
|
+
raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
|
|
385
|
+
except Exception as e:
|
|
386
|
+
console.print("[red]Error:[/red] Failed to upload manifest.json")
|
|
387
|
+
console.print(f"Reason: {e}")
|
|
388
|
+
sys.exit(4)
|
|
389
|
+
|
|
390
|
+
# Upload catalog.json
|
|
391
|
+
console.print(f'Uploading catalog from path "{catalog_path}"')
|
|
392
|
+
try:
|
|
393
|
+
with open(catalog_path, "rb") as f:
|
|
394
|
+
response = requests.put(presigned_urls["catalog_url"], data=f.read())
|
|
395
|
+
if response.status_code not in [200, 204]:
|
|
396
|
+
raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
|
|
397
|
+
except Exception as e:
|
|
398
|
+
console.print("[red]Error:[/red] Failed to upload catalog.json")
|
|
399
|
+
console.print(f"Reason: {e}")
|
|
400
|
+
sys.exit(4)
|
|
401
|
+
|
|
402
|
+
# Update session metadata (if session already existed, update adapter_type)
|
|
403
|
+
if existing_session:
|
|
404
|
+
try:
|
|
405
|
+
client.update_session(org_id, project_id, session_id, adapter_type)
|
|
406
|
+
except RecceCloudException as e:
|
|
407
|
+
console.print("[yellow]Warning:[/yellow] Failed to update session metadata")
|
|
408
|
+
console.print(f"Reason: {e.reason}")
|
|
409
|
+
# Non-fatal for existing sessions
|
|
410
|
+
except Exception as e:
|
|
411
|
+
console.print("[yellow]Warning:[/yellow] Failed to update session metadata")
|
|
412
|
+
console.print(f"Reason: {e}")
|
|
413
|
+
# Non-fatal for existing sessions
|
|
414
|
+
|
|
415
|
+
# Notify upload completion
|
|
416
|
+
console.print("Notifying upload completion...")
|
|
417
|
+
try:
|
|
418
|
+
client.upload_completed(session_id)
|
|
419
|
+
except RecceCloudException as e:
|
|
420
|
+
console.print("[yellow]Warning:[/yellow] Failed to notify upload completion")
|
|
421
|
+
console.print(f"Reason: {e.reason}")
|
|
422
|
+
# Non-fatal, continue
|
|
423
|
+
except Exception as e:
|
|
424
|
+
console.print("[yellow]Warning:[/yellow] Failed to notify upload completion")
|
|
425
|
+
console.print(f"Reason: {e}")
|
|
426
|
+
# Non-fatal, continue
|
|
427
|
+
|
|
428
|
+
# Success!
|
|
429
|
+
console.rule("Uploaded Successfully", style="green")
|
|
430
|
+
console.print("Uploaded dbt artifacts to Recce Cloud")
|
|
431
|
+
console.print()
|
|
432
|
+
console.print(f"[cyan]Session Name:[/cyan] {session_name}")
|
|
433
|
+
console.print(f"[cyan]Session ID:[/cyan] {session_id}")
|
|
434
|
+
console.print(f"[cyan]Organization:[/cyan] {org}")
|
|
435
|
+
console.print(f"[cyan]Project:[/cyan] {project}")
|
|
436
|
+
console.print(f"[cyan]Artifacts from:[/cyan] {os.path.abspath(target_path)}")
|
|
437
|
+
|
|
438
|
+
sys.exit(0)
|