certora-prover-cli 0.1.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.
- certora_prover_cli-0.1.0.dist-info/METADATA +293 -0
- certora_prover_cli-0.1.0.dist-info/RECORD +28 -0
- certora_prover_cli-0.1.0.dist-info/WHEEL +4 -0
- certora_prover_cli-0.1.0.dist-info/entry_points.txt +3 -0
- certora_prover_cli-0.1.0.dist-info/licenses/LICENSE +674 -0
- prover_output_utility/__init__.py +42 -0
- prover_output_utility/_version.py +24 -0
- prover_output_utility/api/__init__.py +14 -0
- prover_output_utility/api/aws_data_fetcher.py +485 -0
- prover_output_utility/api/base_data_fetcher.py +176 -0
- prover_output_utility/api/cache.py +149 -0
- prover_output_utility/api/data_fetcher.py +628 -0
- prover_output_utility/api/local_data_fetcher.py +302 -0
- prover_output_utility/api/main.py +1333 -0
- prover_output_utility/api/tree_parser.py +420 -0
- prover_output_utility/api/url_utils.py +125 -0
- prover_output_utility/auth.py +172 -0
- prover_output_utility/aws_auth.py +124 -0
- prover_output_utility/breadcrumb.py +358 -0
- prover_output_utility/exceptions.py +43 -0
- prover_output_utility/group_summary.py +335 -0
- prover_output_utility/job_report.py +115 -0
- prover_output_utility/models.py +689 -0
- prover_output_utility/parsers.py +212 -0
- prover_output_utility/prover_api.py +37 -0
- prover_output_utility/prover_cli.py +682 -0
- prover_output_utility/py.typed +0 -0
- prover_output_utility/sighash.py +345 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Certora Ltd.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Low-level data fetching utilities for the Prover API.
|
|
7
|
+
"""
|
|
8
|
+
import functools
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from ..auth import LoginEnv, resolve_login_env
|
|
14
|
+
from ..exceptions import AuthenticationError, JobNotFoundError, ProverAPIError
|
|
15
|
+
from .base_data_fetcher import BaseDataFetcher
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Per-environment data-api hosts. Kept in sync with the Certora login envs
|
|
21
|
+
# `resolve_login_env()` understands: AISS_ENV / GITHUB_ENVIRONMENT.
|
|
22
|
+
_API_BASE_URL_BY_ENV: Dict[LoginEnv, str] = {
|
|
23
|
+
"prod": "https://data-api.certora.com",
|
|
24
|
+
"stg": "https://data-api-stg.certora.com",
|
|
25
|
+
"dev": "https://data-api-dev.certora.com",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_api_base_url() -> str:
|
|
30
|
+
"""Return the data-api base URL for the current Certora login env.
|
|
31
|
+
|
|
32
|
+
Resolution mirrors :func:`prover_output_utility.auth.resolve_login_env` —
|
|
33
|
+
``AISS_ENV`` takes precedence; otherwise ``GITHUB_ENVIRONMENT`` maps to
|
|
34
|
+
stg/dev; otherwise prod. Callers that need a specific host should pass
|
|
35
|
+
``api_base_url=`` to the constructor explicitly.
|
|
36
|
+
|
|
37
|
+
Public so callers (e.g. job-listing tooling) can resolve the env-aware
|
|
38
|
+
data-api host without re-deriving the env→host mapping or constructing a
|
|
39
|
+
(auth-triggering) ``ProverOutputAPI``.
|
|
40
|
+
"""
|
|
41
|
+
return _API_BASE_URL_BY_ENV[resolve_login_env()]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Per-environment Prover web-UI hosts (what `--server` resolves to in
|
|
45
|
+
# certoraUtils.SupportedServers). Distinct from the data-api hosts above:
|
|
46
|
+
# prod is prover.certora.com, not data-api.certora.com.
|
|
47
|
+
_PROVER_BASE_URL_BY_ENV: Dict[LoginEnv, str] = {
|
|
48
|
+
"prod": "https://prover.certora.com",
|
|
49
|
+
"stg": "https://vaas-stg.certora.com",
|
|
50
|
+
"dev": "https://vaas-dev.certora.com",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _default_prover_base_url() -> str:
|
|
55
|
+
"""Return the Prover web-UI base URL for the current Certora login env.
|
|
56
|
+
|
|
57
|
+
Same env resolution as :func:`default_api_base_url`, but for the
|
|
58
|
+
user-facing Prover host used in shareable links (e.g. group-summary URLs).
|
|
59
|
+
"""
|
|
60
|
+
return _PROVER_BASE_URL_BY_ENV[resolve_login_env()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_token_expiration(func: Callable[..., T]) -> Callable[..., T]:
|
|
64
|
+
"""Decorator to handle token expiration and retry with re-authentication."""
|
|
65
|
+
|
|
66
|
+
@functools.wraps(func)
|
|
67
|
+
def wrapper(self, *args, **kwargs) -> T:
|
|
68
|
+
try:
|
|
69
|
+
return func(self, *args, **kwargs)
|
|
70
|
+
except AuthenticationError as e:
|
|
71
|
+
# Check if this is a token expiration error (401 response)
|
|
72
|
+
if "401" in str(e) or "Authentication failed" in str(e):
|
|
73
|
+
# Only retry once
|
|
74
|
+
if not getattr(self, "_retry_auth", False):
|
|
75
|
+
self._retry_auth = True
|
|
76
|
+
try:
|
|
77
|
+
# Get the API instance and force re-login
|
|
78
|
+
if hasattr(self, "_api_instance"):
|
|
79
|
+
self._api_instance._setup_session(force_relogin=True)
|
|
80
|
+
# Update our session reference
|
|
81
|
+
self.session = self._api_instance.session
|
|
82
|
+
# Retry the original request
|
|
83
|
+
result = func(self, *args, **kwargs)
|
|
84
|
+
self._retry_auth = False
|
|
85
|
+
return result
|
|
86
|
+
finally:
|
|
87
|
+
self._retry_auth = False
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
return wrapper
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DataFetcher(BaseDataFetcher):
|
|
94
|
+
"""Handles low-level data fetching from Certora API endpoints."""
|
|
95
|
+
|
|
96
|
+
# API URLs by environment. Kept as class attributes for back-compat with
|
|
97
|
+
# callers that referenced them directly; the canonical lookup is
|
|
98
|
+
# ``_API_BASE_URL_BY_ENV`` at module level.
|
|
99
|
+
PRODUCTION_API_BASE_URL = _API_BASE_URL_BY_ENV["prod"]
|
|
100
|
+
STAGING_API_BASE_URL = _API_BASE_URL_BY_ENV["stg"]
|
|
101
|
+
DEV_API_BASE_URL = _API_BASE_URL_BY_ENV["dev"]
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
session: requests.Session,
|
|
106
|
+
api_instance=None,
|
|
107
|
+
api_base_url: Optional[str] = None,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Initialize the data fetcher.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
session: Configured requests session with authentication
|
|
114
|
+
api_instance: Reference to the ProverOutputAPI instance for re-auth
|
|
115
|
+
api_base_url: API base URL. When None (the default), resolve from
|
|
116
|
+
the Certora login env (``AISS_ENV`` / ``GITHUB_ENVIRONMENT``,
|
|
117
|
+
via :func:`resolve_login_env`) — dev → data-api-dev,
|
|
118
|
+
stg → data-api-stg, prod → data-api. Pass an explicit
|
|
119
|
+
URL to override.
|
|
120
|
+
"""
|
|
121
|
+
self.session = session
|
|
122
|
+
# Resolve inside the body so callers picking up `AISS_ENV` at runtime get
|
|
123
|
+
# the right host; the previous `= PRODUCTION_API_BASE_URL` default was
|
|
124
|
+
# bound at class-definition time and could not be retargeted.
|
|
125
|
+
self.api_base_url = api_base_url if api_base_url is not None else default_api_base_url()
|
|
126
|
+
self._api_instance = api_instance
|
|
127
|
+
self._retry_auth = False
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def get_api_base_url_for_prover_url(cls, prover_url: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get the appropriate API base URL for a prover output URL.
|
|
133
|
+
|
|
134
|
+
Dev jobs (vaas-dev.certora.com) use data-api-dev.certora.com,
|
|
135
|
+
staging jobs (vaas-stg.certora.com) use data-api-stg.certora.com,
|
|
136
|
+
production jobs use data-api.certora.com.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
prover_url: Full prover URL (e.g., "https://vaas-stg.certora.com/output/...")
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Appropriate API base URL for API calls
|
|
143
|
+
"""
|
|
144
|
+
if "vaas-dev.certora.com" in prover_url:
|
|
145
|
+
return cls.DEV_API_BASE_URL
|
|
146
|
+
if "vaas-stg.certora.com" in prover_url:
|
|
147
|
+
return cls.STAGING_API_BASE_URL
|
|
148
|
+
return cls.PRODUCTION_API_BASE_URL
|
|
149
|
+
|
|
150
|
+
def _execute_request(
|
|
151
|
+
self,
|
|
152
|
+
method: str,
|
|
153
|
+
endpoint: str,
|
|
154
|
+
params: Optional[Dict[str, Any]] = None,
|
|
155
|
+
json_data: Optional[Dict[str, Any]] = None
|
|
156
|
+
) -> requests.Response:
|
|
157
|
+
"""
|
|
158
|
+
Execute HTTP request with method dispatch.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
method: HTTP method (GET, POST, PUT, DELETE)
|
|
162
|
+
endpoint: Full URL endpoint
|
|
163
|
+
params: Query parameters (for GET/DELETE)
|
|
164
|
+
json_data: JSON body data (for POST/PUT)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
requests.Response object
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ProverAPIError: If HTTP method is unsupported
|
|
171
|
+
"""
|
|
172
|
+
if method.upper() == "GET":
|
|
173
|
+
return self.session.get(endpoint, params=params)
|
|
174
|
+
elif method.upper() == "POST":
|
|
175
|
+
return self.session.post(endpoint, json=json_data)
|
|
176
|
+
elif method.upper() == "PUT":
|
|
177
|
+
return self.session.put(endpoint, json=json_data)
|
|
178
|
+
elif method.upper() == "DELETE":
|
|
179
|
+
return self.session.delete(endpoint, params=params)
|
|
180
|
+
else:
|
|
181
|
+
raise ProverAPIError(f"Unsupported HTTP method: {method}")
|
|
182
|
+
|
|
183
|
+
@handle_token_expiration
|
|
184
|
+
def fetch_raw_output(self, job_identifier: str) -> Dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Fetch raw output data from the prover API.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
job_identifier: Job ID to fetch
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Raw API response data
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
AuthenticationError: If authentication fails
|
|
196
|
+
JobNotFoundError: If job is not found
|
|
197
|
+
ProverAPIError: If API call fails
|
|
198
|
+
"""
|
|
199
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}"
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
response = self.session.get(endpoint)
|
|
203
|
+
|
|
204
|
+
if response.status_code == 200:
|
|
205
|
+
return cast(Dict[str, Any], response.json())
|
|
206
|
+
elif response.status_code == 401:
|
|
207
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
208
|
+
elif response.status_code == 404:
|
|
209
|
+
raise JobNotFoundError(f"Job {job_identifier} not found")
|
|
210
|
+
else:
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
return cast(Dict[str, Any], response.json())
|
|
213
|
+
|
|
214
|
+
except requests.exceptions.RequestException as e:
|
|
215
|
+
raise ProverAPIError(f"Failed to fetch job data for {job_identifier}: {e}")
|
|
216
|
+
|
|
217
|
+
@handle_token_expiration
|
|
218
|
+
def fetch_tree_view_data(
|
|
219
|
+
self, job_identifier: str, path: Optional[str] = None
|
|
220
|
+
) -> Dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Fetch tree-view data for a job.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
job_identifier: Job ID to fetch
|
|
226
|
+
path: Optional path within the tree-view
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tree-view data
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
AuthenticationError: If authentication fails
|
|
233
|
+
JobNotFoundError: If job is not found
|
|
234
|
+
ProverAPIError: If API call fails
|
|
235
|
+
"""
|
|
236
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/tree-view"
|
|
237
|
+
|
|
238
|
+
params = {}
|
|
239
|
+
if path:
|
|
240
|
+
params["path"] = path
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
response = self.session.get(endpoint, params=params)
|
|
244
|
+
|
|
245
|
+
if response.status_code == 200:
|
|
246
|
+
return cast(Dict[str, Any], response.json())
|
|
247
|
+
elif response.status_code == 401:
|
|
248
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
249
|
+
elif response.status_code == 404:
|
|
250
|
+
raise JobNotFoundError(f"Tree-view data not found for job {job_identifier}")
|
|
251
|
+
else:
|
|
252
|
+
response.raise_for_status()
|
|
253
|
+
return cast(Dict[str, Any], response.json())
|
|
254
|
+
|
|
255
|
+
except requests.exceptions.RequestException as e:
|
|
256
|
+
raise ProverAPIError(f"Failed to fetch tree-view data for {job_identifier}: {e}")
|
|
257
|
+
|
|
258
|
+
@handle_token_expiration
|
|
259
|
+
def fetch_statsdata(self, job_identifier: str) -> Dict[str, Any]:
|
|
260
|
+
"""
|
|
261
|
+
Fetch statsdata.json for a job.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
job_identifier: Job ID to fetch
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Stats data from statsdata.json
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
AuthenticationError: If authentication fails
|
|
271
|
+
JobNotFoundError: If job is not found
|
|
272
|
+
ProverAPIError: If API call fails
|
|
273
|
+
"""
|
|
274
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/f/statsdata.json"
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
response = self.session.get(endpoint)
|
|
278
|
+
|
|
279
|
+
if response.status_code == 200:
|
|
280
|
+
return cast(Dict[str, Any], response.json())
|
|
281
|
+
elif response.status_code == 401:
|
|
282
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
283
|
+
elif response.status_code == 404:
|
|
284
|
+
raise JobNotFoundError(f"Stats data not found for job {job_identifier}")
|
|
285
|
+
else:
|
|
286
|
+
response.raise_for_status()
|
|
287
|
+
return cast(Dict[str, Any], response.json())
|
|
288
|
+
|
|
289
|
+
except requests.exceptions.RequestException as e:
|
|
290
|
+
raise ProverAPIError(f"Failed to fetch stats data for {job_identifier}: {e}")
|
|
291
|
+
|
|
292
|
+
def fetch_outputs(self, job_identifier: str) -> bytes:
|
|
293
|
+
"""
|
|
294
|
+
Fetch outputs archive (tar.gz) for a job.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
job_identifier: Job ID to fetch
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Raw tar.gz content as bytes
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
AuthenticationError: If authentication fails
|
|
304
|
+
JobNotFoundError: If job is not found
|
|
305
|
+
ProverAPIError: If API call fails
|
|
306
|
+
"""
|
|
307
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/f/outputs"
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
response = self.session.get(endpoint)
|
|
311
|
+
|
|
312
|
+
if response.status_code == 200:
|
|
313
|
+
return response.content
|
|
314
|
+
elif response.status_code == 401:
|
|
315
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
316
|
+
elif response.status_code == 404:
|
|
317
|
+
raise JobNotFoundError(f"Outputs not found for job {job_identifier}")
|
|
318
|
+
else:
|
|
319
|
+
response.raise_for_status()
|
|
320
|
+
return response.content
|
|
321
|
+
|
|
322
|
+
except requests.exceptions.RequestException as e:
|
|
323
|
+
raise ProverAPIError(f"Failed to fetch outputs for {job_identifier}: {e}")
|
|
324
|
+
|
|
325
|
+
@handle_token_expiration
|
|
326
|
+
def list_recent_jobs(
|
|
327
|
+
self,
|
|
328
|
+
created_after: str,
|
|
329
|
+
limit: int = 100,
|
|
330
|
+
all_users: bool = True,
|
|
331
|
+
) -> List[Dict[str, Any]]:
|
|
332
|
+
"""List recent jobs from the data API.
|
|
333
|
+
|
|
334
|
+
See BaseDataFetcher.list_recent_jobs for parameter docs.
|
|
335
|
+
"""
|
|
336
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs"
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
response = self._execute_request("GET", endpoint, params={
|
|
340
|
+
"currentPage": 1,
|
|
341
|
+
# API caps page size at 100 per response, so don't request more
|
|
342
|
+
"pageSize": min(limit, 100),
|
|
343
|
+
"sortOrder": "desc",
|
|
344
|
+
"allUsers": "true" if all_users else "false",
|
|
345
|
+
"createdAfter": created_after,
|
|
346
|
+
"deleted": "false",
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
if response.status_code == 200:
|
|
350
|
+
return cast(List[Dict[str, Any]], response.json().get("items", []))
|
|
351
|
+
else:
|
|
352
|
+
raise ProverAPIError(
|
|
353
|
+
f"Failed to list recent jobs (status {response.status_code}): "
|
|
354
|
+
f"{response.text[:200]}"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
except requests.exceptions.RequestException as e:
|
|
358
|
+
raise ProverAPIError(f"Failed to list recent jobs: {e}")
|
|
359
|
+
|
|
360
|
+
@handle_token_expiration
|
|
361
|
+
def fetch_group_jobs(
|
|
362
|
+
self, group_id: str, created_after: str, created_before: str
|
|
363
|
+
) -> List[Dict[str, Any]]:
|
|
364
|
+
"""Fetch all jobs in a group from the data API."""
|
|
365
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs"
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
response = self._execute_request("GET", endpoint, params={
|
|
369
|
+
"groupIds": group_id,
|
|
370
|
+
"pageSize": 200,
|
|
371
|
+
"createdAfter": created_after,
|
|
372
|
+
"createdBefore": created_before,
|
|
373
|
+
"allUsers": "true",
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
if response.status_code == 200:
|
|
377
|
+
return cast(List[Dict[str, Any]], response.json().get("items", []))
|
|
378
|
+
else:
|
|
379
|
+
raise ProverAPIError(
|
|
380
|
+
f"Failed to fetch group jobs (status {response.status_code}): "
|
|
381
|
+
f"{response.text[:200]}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
except requests.exceptions.RequestException as e:
|
|
385
|
+
raise ProverAPIError(f"Failed to fetch group jobs: {e}")
|
|
386
|
+
|
|
387
|
+
@handle_token_expiration
|
|
388
|
+
def cancel_jobs(self, job_ids: List[str]) -> Dict[str, Any]:
|
|
389
|
+
"""
|
|
390
|
+
Cancel multiple jobs by their IDs.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
job_ids: List of job IDs to cancel
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
API response data
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
ProverAPIError: If API call fails
|
|
400
|
+
"""
|
|
401
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/cancel"
|
|
402
|
+
payload = {"ids": job_ids}
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
response = self.session.post(endpoint, json=payload)
|
|
406
|
+
|
|
407
|
+
if response.status_code == 200:
|
|
408
|
+
return cast(Dict[str, Any], response.json())
|
|
409
|
+
elif response.status_code == 401:
|
|
410
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
411
|
+
elif response.status_code == 404:
|
|
412
|
+
raise JobNotFoundError("Cancel endpoint not found")
|
|
413
|
+
else:
|
|
414
|
+
response.raise_for_status()
|
|
415
|
+
return cast(Dict[str, Any], response.json())
|
|
416
|
+
|
|
417
|
+
except requests.exceptions.RequestException as e:
|
|
418
|
+
raise ProverAPIError(f"Failed to cancel jobs {job_ids}: {e}")
|
|
419
|
+
|
|
420
|
+
@handle_token_expiration
|
|
421
|
+
def fetch_console_logs(self, job_identifier: str) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Fetch console logs for a job.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
job_identifier: Job ID to fetch logs for
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Console logs as a string
|
|
430
|
+
|
|
431
|
+
Raises:
|
|
432
|
+
AuthenticationError: If authentication fails
|
|
433
|
+
JobNotFoundError: If job is not found
|
|
434
|
+
ProverAPIError: If API call fails
|
|
435
|
+
"""
|
|
436
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/logs"
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
response = self.session.get(endpoint)
|
|
440
|
+
|
|
441
|
+
if response.status_code == 200:
|
|
442
|
+
return response.text
|
|
443
|
+
elif response.status_code == 401:
|
|
444
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
445
|
+
elif response.status_code == 404:
|
|
446
|
+
raise JobNotFoundError(f"Console logs not found for job {job_identifier}")
|
|
447
|
+
else:
|
|
448
|
+
response.raise_for_status()
|
|
449
|
+
return response.text
|
|
450
|
+
|
|
451
|
+
except requests.exceptions.RequestException as e:
|
|
452
|
+
raise ProverAPIError(f"Failed to fetch console logs for {job_identifier}: {e}")
|
|
453
|
+
|
|
454
|
+
@handle_token_expiration
|
|
455
|
+
def who_am_i(self) -> Dict[str, Any]:
|
|
456
|
+
"""
|
|
457
|
+
Get information about the currently authenticated user.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
User information including email and user ID
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
AuthenticationError: If authentication fails
|
|
464
|
+
ProverAPIError: If API call fails
|
|
465
|
+
"""
|
|
466
|
+
endpoint = f"{self.api_base_url}/who-am-i"
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
response = self.session.get(endpoint)
|
|
470
|
+
|
|
471
|
+
if response.status_code == 200:
|
|
472
|
+
return cast(Dict[str, Any], response.json())
|
|
473
|
+
elif response.status_code == 401:
|
|
474
|
+
raise AuthenticationError("Authentication failed - check credentials")
|
|
475
|
+
else:
|
|
476
|
+
response.raise_for_status()
|
|
477
|
+
return cast(Dict[str, Any], response.json())
|
|
478
|
+
|
|
479
|
+
except requests.exceptions.RequestException as e:
|
|
480
|
+
raise ProverAPIError(f"Failed to get user information: {e}")
|
|
481
|
+
|
|
482
|
+
@handle_token_expiration
|
|
483
|
+
def fetch_custom_endpoint(
|
|
484
|
+
self, endpoint: str, params: Optional[Dict[str, Any]] = None, method: str = "GET"
|
|
485
|
+
) -> Dict[str, Any]:
|
|
486
|
+
"""
|
|
487
|
+
Fetch data from an arbitrary user-provided endpoint.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
endpoint: The full URL endpoint to fetch from
|
|
491
|
+
params: Optional query parameters or request body
|
|
492
|
+
method: HTTP method (GET, POST, etc.)
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
API response data
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
AuthenticationError: If authentication fails
|
|
499
|
+
ProverAPIError: If API call fails
|
|
500
|
+
"""
|
|
501
|
+
try:
|
|
502
|
+
# Use helper method for HTTP method dispatch
|
|
503
|
+
if method.upper() in ("GET", "DELETE"):
|
|
504
|
+
response = self._execute_request(method, endpoint, params=params)
|
|
505
|
+
elif method.upper() in ("POST", "PUT"):
|
|
506
|
+
response = self._execute_request(method, endpoint, json_data=params)
|
|
507
|
+
else:
|
|
508
|
+
raise ProverAPIError(f"Unsupported HTTP method: {method}")
|
|
509
|
+
|
|
510
|
+
if response.status_code == 200:
|
|
511
|
+
return cast(Dict[str, Any], response.json())
|
|
512
|
+
elif response.status_code == 401:
|
|
513
|
+
raise AuthenticationError("Authentication failed - check credentials")
|
|
514
|
+
else:
|
|
515
|
+
response.raise_for_status()
|
|
516
|
+
return cast(Dict[str, Any], response.json())
|
|
517
|
+
|
|
518
|
+
except requests.exceptions.RequestException as e:
|
|
519
|
+
raise ProverAPIError(f"Failed to fetch from custom endpoint {endpoint}: {e}")
|
|
520
|
+
|
|
521
|
+
@handle_token_expiration
|
|
522
|
+
def fetch_source_files_list(self, job_identifier: str) -> List[Dict[str, Any]]:
|
|
523
|
+
"""
|
|
524
|
+
Fetch the source file tree for a job.
|
|
525
|
+
|
|
526
|
+
Calls the source-files endpoint with path="" which redirects to S3
|
|
527
|
+
and returns the full file tree as JSON.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
job_identifier: Job ID to fetch
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
List of source file tree nodes
|
|
534
|
+
|
|
535
|
+
Raises:
|
|
536
|
+
AuthenticationError: If authentication fails
|
|
537
|
+
JobNotFoundError: If job is not found
|
|
538
|
+
ProverAPIError: If API call fails
|
|
539
|
+
"""
|
|
540
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/source-files"
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
response = self.session.get(endpoint, params={"path": ""})
|
|
544
|
+
|
|
545
|
+
if response.status_code == 200:
|
|
546
|
+
return cast(List[Dict[str, Any]], response.json())
|
|
547
|
+
elif response.status_code == 401:
|
|
548
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
549
|
+
elif response.status_code == 404:
|
|
550
|
+
raise JobNotFoundError(f"Source files not found for job {job_identifier}")
|
|
551
|
+
else:
|
|
552
|
+
response.raise_for_status()
|
|
553
|
+
return cast(List[Dict[str, Any]], response.json())
|
|
554
|
+
|
|
555
|
+
except requests.exceptions.RequestException as e:
|
|
556
|
+
raise ProverAPIError(f"Failed to fetch source files for {job_identifier}: {e}")
|
|
557
|
+
|
|
558
|
+
@handle_token_expiration
|
|
559
|
+
def fetch_source_file_content(self, job_identifier: str, path: str) -> str:
|
|
560
|
+
"""
|
|
561
|
+
Fetch the content of a specific source file for a job.
|
|
562
|
+
|
|
563
|
+
Calls the source-files endpoint with the given path which redirects
|
|
564
|
+
to S3 and returns the raw file content.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
job_identifier: Job ID to fetch
|
|
568
|
+
path: Path to the source file (e.g. ".certora_sources/src/Contract.sol")
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Raw file content as a string
|
|
572
|
+
|
|
573
|
+
Raises:
|
|
574
|
+
AuthenticationError: If authentication fails
|
|
575
|
+
JobNotFoundError: If job is not found
|
|
576
|
+
ProverAPIError: If API call fails
|
|
577
|
+
"""
|
|
578
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/source-files"
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
response = self.session.get(endpoint, params={"path": path})
|
|
582
|
+
|
|
583
|
+
if response.status_code == 200:
|
|
584
|
+
return response.text
|
|
585
|
+
elif response.status_code == 401:
|
|
586
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
587
|
+
elif response.status_code == 404:
|
|
588
|
+
raise JobNotFoundError(f"Source file not found for job {job_identifier}: {path}")
|
|
589
|
+
else:
|
|
590
|
+
response.raise_for_status()
|
|
591
|
+
return response.text
|
|
592
|
+
|
|
593
|
+
except requests.exceptions.RequestException as e:
|
|
594
|
+
raise ProverAPIError(f"Failed to fetch source file for {job_identifier}: {e}")
|
|
595
|
+
|
|
596
|
+
@handle_token_expiration
|
|
597
|
+
def fetch_alert_report(self, job_identifier: str) -> List[Dict[str, Any]]:
|
|
598
|
+
"""
|
|
599
|
+
Fetch alert report (alertReport.json) for a job.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
job_identifier: Job ID to fetch
|
|
603
|
+
|
|
604
|
+
Returns:
|
|
605
|
+
Alert report data as a list of alert objects
|
|
606
|
+
|
|
607
|
+
Raises:
|
|
608
|
+
AuthenticationError: If authentication fails
|
|
609
|
+
JobNotFoundError: If job is not found
|
|
610
|
+
ProverAPIError: If API call fails
|
|
611
|
+
"""
|
|
612
|
+
endpoint = f"{self.api_base_url}/v1/domain/jobs/{job_identifier}/f/alertReport.json"
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
response = self.session.get(endpoint)
|
|
616
|
+
|
|
617
|
+
if response.status_code == 200:
|
|
618
|
+
return cast(List[Dict[str, Any]], response.json())
|
|
619
|
+
elif response.status_code == 401:
|
|
620
|
+
raise AuthenticationError("Authentication failed - check CERTORAKEY")
|
|
621
|
+
elif response.status_code == 404:
|
|
622
|
+
raise JobNotFoundError(f"Alert report not found for job {job_identifier}")
|
|
623
|
+
else:
|
|
624
|
+
response.raise_for_status()
|
|
625
|
+
return cast(List[Dict[str, Any]], response.json())
|
|
626
|
+
|
|
627
|
+
except requests.exceptions.RequestException as e:
|
|
628
|
+
raise ProverAPIError(f"Failed to fetch alert report for {job_identifier}: {e}")
|