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.
@@ -0,0 +1,42 @@
1
+ # SPDX-FileCopyrightText: 2026 Certora Ltd.
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ """
6
+ ProverCLI - Python API for parsing Certora Prover outputs.
7
+
8
+ This module provides an easy-to-use API for fetching and parsing outputs from the Certora Prover.
9
+ Supports both job URLs and job IDs as input.
10
+ """
11
+
12
+ from .api.data_fetcher import default_api_base_url
13
+ from .auth import cloud_server_for_env, prover_frontend_url, resolve_login_env
14
+ from .prover_api import ProverOutputAPI
15
+ from .models import (
16
+ AlertType,
17
+ AssertType,
18
+ CheckResult,
19
+ JobStatus,
20
+ NodeStatus,
21
+ NodeType,
22
+ ParsedAlert,
23
+ )
24
+ from .job_report import JobAnalyzer, JobReport
25
+
26
+ __version__ = "1.0.0"
27
+ __all__ = [
28
+ "ProverOutputAPI",
29
+ "AlertType",
30
+ "AssertType",
31
+ "CheckResult",
32
+ "JobStatus",
33
+ "NodeStatus",
34
+ "NodeType",
35
+ "ParsedAlert",
36
+ "JobAnalyzer",
37
+ "JobReport",
38
+ "resolve_login_env",
39
+ "cloud_server_for_env",
40
+ "prover_frontend_url",
41
+ "default_api_base_url",
42
+ ]
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2026 Certora Ltd.
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ """
6
+ Prover Output API package.
7
+
8
+ This package contains the refactored API modules for interacting with
9
+ the Certora Prover output data.
10
+ """
11
+
12
+ from .main import ProverOutputAPI
13
+
14
+ __all__ = ["ProverOutputAPI"]
@@ -0,0 +1,485 @@
1
+ # SPDX-FileCopyrightText: 2026 Certora Ltd.
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+
5
+ """
6
+ AWS Lambda-based data fetcher for CI environments.
7
+ Uses AWS SigV4 authentication to access Certora API through Lambda.
8
+ """
9
+
10
+ import os
11
+ from typing import Any, Dict, List, Optional, cast
12
+
13
+ import boto3
14
+ import requests
15
+ from botocore.auth import SigV4Auth
16
+ from botocore.awsrequest import AWSRequest
17
+
18
+ from ..exceptions import AuthenticationError, JobNotFoundError, ProverAPIError
19
+ from .base_data_fetcher import BaseDataFetcher
20
+
21
+
22
+ class AWSDataFetcher(BaseDataFetcher):
23
+ """Handles data fetching from Certora API via AWS Lambda with SigV4 authentication."""
24
+
25
+ #: Environment variable holding the Lambda function URL (no default is baked in).
26
+ LAMBDA_URL_ENV_VAR = "CERTORA_LAMBDA_URL"
27
+
28
+ def __init__(
29
+ self, aws_session: Optional[boto3.Session] = None, base_url: Optional[str] = None
30
+ ):
31
+ """
32
+ Initialize the AWS data fetcher.
33
+
34
+ Args:
35
+ aws_session: Optional boto3 session. If not provided, creates default session.
36
+ base_url: Lambda function URL. If not provided, read from the
37
+ ``CERTORA_LAMBDA_URL`` environment variable.
38
+
39
+ Raises:
40
+ ProverAPIError: If no base URL is provided or configured.
41
+ """
42
+ self.session = aws_session or boto3.Session()
43
+ resolved = base_url or os.environ.get(self.LAMBDA_URL_ENV_VAR)
44
+ if not resolved:
45
+ raise ProverAPIError(
46
+ f"AWS Lambda endpoint not configured. Set the {self.LAMBDA_URL_ENV_VAR} "
47
+ "environment variable to the Lambda function URL, or pass base_url=."
48
+ )
49
+ self.base_url = resolved.rstrip("/")
50
+ self.region = "us-west-2"
51
+ self.service = "lambda"
52
+
53
+ def _make_signed_request(
54
+ self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None
55
+ ) -> requests.Response:
56
+ """
57
+ Make a signed request to the AWS Lambda endpoint with redirect handling.
58
+
59
+ Args:
60
+ method: HTTP method (GET, POST, etc.)
61
+ endpoint: Full URL endpoint
62
+ data: Optional JSON data for POST requests
63
+ params: Optional query parameters
64
+
65
+ Returns:
66
+ requests Response object
67
+
68
+ Raises:
69
+ ProverAPIError: If request fails
70
+ """
71
+ # Create AWS request for signing
72
+ aws_request = AWSRequest(method=method, url=endpoint, data=data, params=params)
73
+
74
+ # Sign the request with SigV4
75
+ credentials = self.session.get_credentials()
76
+ if not credentials:
77
+ raise AuthenticationError("No AWS credentials available. Check AWS configuration.")
78
+
79
+ SigV4Auth(credentials, self.service, self.region).add_auth(aws_request)
80
+
81
+ # Make the initial request with signed headers, but don't follow redirects
82
+ try:
83
+ response = requests.request(
84
+ method=method,
85
+ url=endpoint,
86
+ headers=dict(aws_request.headers),
87
+ json=data,
88
+ params=params,
89
+ timeout=30,
90
+ allow_redirects=False, # Handle redirects manually
91
+ )
92
+
93
+ # Handle redirects manually
94
+ if response.status_code in (301, 302, 303, 307, 308):
95
+ redirect_url = response.headers.get("Location")
96
+ if redirect_url:
97
+ # Check if redirecting to *.certora.com domain
98
+ from urllib.parse import urlparse
99
+
100
+ parsed_url = urlparse(redirect_url)
101
+
102
+ if parsed_url.hostname and not parsed_url.hostname.endswith(".certora.com"):
103
+ # For non certora.com domains, follow without AWS headers
104
+ response = requests.request(
105
+ method="GET" if response.status_code == 303 else method,
106
+ url=redirect_url,
107
+ json=data,
108
+ params=params,
109
+ timeout=30,
110
+ allow_redirects=True, # Allow further redirects
111
+ )
112
+ else:
113
+ # For certora.com domains, make request with AWS headers
114
+ response = requests.request(
115
+ method="GET" if response.status_code == 303 else method,
116
+ url=redirect_url,
117
+ headers=dict(aws_request.headers),
118
+ json=data,
119
+ params=params,
120
+ timeout=30,
121
+ allow_redirects=True,
122
+ )
123
+
124
+ return response
125
+ except requests.exceptions.RequestException as e:
126
+ raise ProverAPIError(f"Request failed: {e}")
127
+
128
+ def fetch_raw_output(self, job_identifier: str) -> Dict[str, Any]:
129
+ """
130
+ Fetch raw output data from the prover API via Lambda.
131
+
132
+ Args:
133
+ job_identifier: Job ID to fetch
134
+
135
+ Returns:
136
+ Raw API response data
137
+
138
+ Raises:
139
+ AuthenticationError: If authentication fails
140
+ JobNotFoundError: If job is not found
141
+ ProverAPIError: If API call fails
142
+ """
143
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}"
144
+
145
+ try:
146
+ response = self._make_signed_request("GET", endpoint)
147
+
148
+ if response.status_code == 200:
149
+ return cast(Dict[str, Any], response.json())
150
+ elif response.status_code == 401:
151
+ raise AuthenticationError("Authentication failed - check AWS credentials")
152
+ elif response.status_code == 404:
153
+ raise JobNotFoundError(f"Job {job_identifier} not found")
154
+ else:
155
+ response.raise_for_status()
156
+ return cast(Dict[str, Any], response.json())
157
+
158
+ except requests.exceptions.RequestException as e:
159
+ raise ProverAPIError(f"Failed to fetch job data for {job_identifier}: {e}")
160
+
161
+ def fetch_tree_view_data(
162
+ self, job_identifier: str, path: Optional[str] = None
163
+ ) -> Dict[str, Any]:
164
+ """
165
+ Fetch tree-view data for a job via Lambda.
166
+
167
+ Args:
168
+ job_identifier: Job ID to fetch
169
+ path: Optional path within the tree-view
170
+
171
+ Returns:
172
+ Tree-view data
173
+
174
+ Raises:
175
+ AuthenticationError: If authentication fails
176
+ JobNotFoundError: If job is not found
177
+ ProverAPIError: If API call fails
178
+ """
179
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}/tree-view"
180
+
181
+ params = {}
182
+ if path:
183
+ params["path"] = path
184
+
185
+ try:
186
+ response = self._make_signed_request("GET", endpoint, params=params)
187
+
188
+ if response.status_code == 200:
189
+ return cast(Dict[str, Any], response.json())
190
+ elif response.status_code == 401:
191
+ raise AuthenticationError("Authentication failed - check AWS credentials")
192
+ elif response.status_code == 404:
193
+ raise JobNotFoundError(f"Tree-view data not found for job {job_identifier}")
194
+ else:
195
+ response.raise_for_status()
196
+ return cast(Dict[str, Any], response.json())
197
+
198
+ except requests.exceptions.RequestException as e:
199
+ raise ProverAPIError(f"Failed to fetch tree-view data for {job_identifier}: {e}")
200
+
201
+ def fetch_statsdata(self, job_identifier: str) -> Dict[str, Any]:
202
+ """
203
+ Fetch statsdata.json for a job via Lambda.
204
+
205
+ Args:
206
+ job_identifier: Job ID to fetch
207
+
208
+ Returns:
209
+ Stats data from statsdata.json
210
+
211
+ Raises:
212
+ AuthenticationError: If authentication fails
213
+ JobNotFoundError: If job is not found
214
+ ProverAPIError: If API call fails
215
+ """
216
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}/f/statsdata.json"
217
+
218
+ try:
219
+ response = self._make_signed_request("GET", endpoint)
220
+
221
+ if response.status_code == 200:
222
+ return cast(Dict[str, Any], response.json())
223
+ elif response.status_code == 401:
224
+ raise AuthenticationError("Authentication failed - check AWS credentials")
225
+ elif response.status_code == 404:
226
+ raise JobNotFoundError(f"Stats data not found for job {job_identifier}")
227
+ else:
228
+ response.raise_for_status()
229
+ return cast(Dict[str, Any], response.json())
230
+
231
+ except requests.exceptions.RequestException as e:
232
+ raise ProverAPIError(f"Failed to fetch stats data for {job_identifier}: {e}")
233
+
234
+ def fetch_outputs(self, job_identifier: str) -> bytes:
235
+ """
236
+ Fetch outputs archive (tar.gz) for a job via Lambda.
237
+
238
+ Args:
239
+ job_identifier: Job ID to fetch
240
+
241
+ Returns:
242
+ Raw tar.gz content as bytes
243
+
244
+ Raises:
245
+ AuthenticationError: If authentication fails
246
+ JobNotFoundError: If job is not found
247
+ ProverAPIError: If API call fails
248
+ """
249
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}/f/outputs"
250
+
251
+ try:
252
+ response = self._make_signed_request("GET", endpoint)
253
+
254
+ if response.status_code == 200:
255
+ return response.content
256
+ elif response.status_code == 401:
257
+ raise AuthenticationError("Authentication failed - check AWS credentials")
258
+ elif response.status_code == 404:
259
+ raise JobNotFoundError(f"Outputs not found for job {job_identifier}")
260
+ else:
261
+ response.raise_for_status()
262
+ return response.content
263
+
264
+ except requests.exceptions.RequestException as e:
265
+ raise ProverAPIError(f"Failed to fetch outputs for {job_identifier}: {e}")
266
+
267
+ def list_recent_jobs(
268
+ self,
269
+ created_after: str,
270
+ limit: int = 100,
271
+ all_users: bool = True,
272
+ ) -> List[Dict[str, Any]]:
273
+ """List recent jobs via Lambda.
274
+
275
+ See BaseDataFetcher.list_recent_jobs for parameter docs.
276
+ """
277
+ endpoint = f"{self.base_url}/v1/domain/jobs"
278
+
279
+ try:
280
+ response = self._make_signed_request("GET", endpoint, params={
281
+ "currentPage": 1,
282
+ "pageSize": min(limit, 100),
283
+ "sortOrder": "desc",
284
+ "allUsers": "true" if all_users else "false",
285
+ "createdAfter": created_after,
286
+ "deleted": "false",
287
+ })
288
+
289
+ if response.status_code == 200:
290
+ return cast(List[Dict[str, Any]], response.json().get("items", []))
291
+ else:
292
+ raise ProverAPIError(
293
+ f"Failed to list recent jobs (status {response.status_code}): "
294
+ f"{response.text[:200]}"
295
+ )
296
+
297
+ except requests.exceptions.RequestException as e:
298
+ raise ProverAPIError(f"Failed to list recent jobs: {e}")
299
+
300
+ def fetch_group_jobs(
301
+ self, group_id: str, created_after: str, created_before: str
302
+ ) -> List[Dict[str, Any]]:
303
+ """Fetch all jobs in a group via Lambda."""
304
+ endpoint = f"{self.base_url}/v1/domain/jobs"
305
+
306
+ try:
307
+ response = self._make_signed_request("GET", endpoint, params={
308
+ "groupIds": group_id,
309
+ "pageSize": 200,
310
+ "createdAfter": created_after,
311
+ "createdBefore": created_before,
312
+ "allUsers": "true",
313
+ })
314
+
315
+ if response.status_code == 200:
316
+ return cast(List[Dict[str, Any]], response.json().get("items", []))
317
+ else:
318
+ raise ProverAPIError(
319
+ f"Failed to fetch group jobs (status {response.status_code}): "
320
+ f"{response.text[:200]}"
321
+ )
322
+
323
+ except requests.exceptions.RequestException as e:
324
+ raise ProverAPIError(f"Failed to fetch group jobs: {e}")
325
+
326
+ def cancel_jobs(self, job_ids: List[str]) -> Dict[str, Any]:
327
+ """
328
+ Cancel multiple jobs by their IDs via Lambda.
329
+
330
+ Args:
331
+ job_ids: List of job IDs to cancel
332
+
333
+ Returns:
334
+ API response data
335
+
336
+ Raises:
337
+ ProverAPIError: If API call fails
338
+ """
339
+ endpoint = f"{self.base_url}/v1/domain/jobs/cancel"
340
+ payload = {"ids": job_ids}
341
+
342
+ try:
343
+ response = self._make_signed_request("POST", endpoint, data=payload)
344
+
345
+ if response.status_code == 200:
346
+ return cast(Dict[str, Any], response.json())
347
+ elif response.status_code == 401:
348
+ raise AuthenticationError("Authentication failed - check AWS credentials")
349
+ elif response.status_code == 404:
350
+ raise JobNotFoundError("Cancel endpoint not found")
351
+ else:
352
+ response.raise_for_status()
353
+ return cast(Dict[str, Any], response.json())
354
+
355
+ except requests.exceptions.RequestException as e:
356
+ raise ProverAPIError(f"Failed to cancel jobs {job_ids}: {e}")
357
+
358
+ def who_am_i(self) -> Dict[str, Any]:
359
+ """
360
+ Get information about the currently authenticated user via Lambda.
361
+
362
+ Returns:
363
+ User information including email and user ID
364
+
365
+ Raises:
366
+ AuthenticationError: If authentication fails
367
+ ProverAPIError: If API call fails
368
+ """
369
+ endpoint = f"{self.base_url}/who-am-i"
370
+
371
+ try:
372
+ response = self._make_signed_request("GET", endpoint)
373
+
374
+ if response.status_code == 200:
375
+ return cast(Dict[str, Any], response.json())
376
+ elif response.status_code == 401:
377
+ raise AuthenticationError("Authentication failed - check AWS credentials")
378
+ else:
379
+ response.raise_for_status()
380
+ return cast(Dict[str, Any], response.json())
381
+
382
+ except requests.exceptions.RequestException as e:
383
+ if "401" in str(e):
384
+ raise AuthenticationError("Authentication failed - check AWS credentials")
385
+ raise ProverAPIError(f"Failed to get user information: {e}")
386
+
387
+ def fetch_source_files_list(self, job_identifier: str) -> List[Dict[str, Any]]:
388
+ """
389
+ Fetch the source file tree for a job via Lambda.
390
+
391
+ Args:
392
+ job_identifier: Job ID to fetch
393
+
394
+ Returns:
395
+ List of source file tree nodes
396
+
397
+ Raises:
398
+ AuthenticationError: If authentication fails
399
+ JobNotFoundError: If job is not found
400
+ ProverAPIError: If API call fails
401
+ """
402
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}/source-files"
403
+
404
+ try:
405
+ response = self._make_signed_request("GET", endpoint, params={"path": ""})
406
+
407
+ if response.status_code == 200:
408
+ return cast(List[Dict[str, Any]], response.json())
409
+ elif response.status_code == 401:
410
+ raise AuthenticationError("Authentication failed - check AWS credentials")
411
+ elif response.status_code == 404:
412
+ raise JobNotFoundError(f"Source files not found for job {job_identifier}")
413
+ else:
414
+ response.raise_for_status()
415
+ return cast(List[Dict[str, Any]], response.json())
416
+
417
+ except requests.exceptions.RequestException as e:
418
+ raise ProverAPIError(f"Failed to fetch source files for {job_identifier}: {e}")
419
+
420
+ def fetch_source_file_content(self, job_identifier: str, path: str) -> str:
421
+ """
422
+ Fetch the content of a specific source file for a job via Lambda.
423
+
424
+ Args:
425
+ job_identifier: Job ID to fetch
426
+ path: Path to the source file (e.g. ".certora_sources/src/Contract.sol")
427
+
428
+ Returns:
429
+ Raw file content 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.base_url}/v1/domain/jobs/{job_identifier}/source-files"
437
+
438
+ try:
439
+ response = self._make_signed_request("GET", endpoint, params={"path": path})
440
+
441
+ if response.status_code == 200:
442
+ return response.text
443
+ elif response.status_code == 401:
444
+ raise AuthenticationError("Authentication failed - check AWS credentials")
445
+ elif response.status_code == 404:
446
+ raise JobNotFoundError(f"Source file not found for job {job_identifier}: {path}")
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 source file for {job_identifier}: {e}")
453
+
454
+ def fetch_alert_report(self, job_identifier: str) -> List[Dict[str, Any]]:
455
+ """
456
+ Fetch alert report (alertReport.json) for a job via Lambda.
457
+
458
+ Args:
459
+ job_identifier: Job ID to fetch
460
+
461
+ Returns:
462
+ Alert report data as a list of alert objects
463
+
464
+ Raises:
465
+ AuthenticationError: If authentication fails
466
+ JobNotFoundError: If job is not found
467
+ ProverAPIError: If API call fails
468
+ """
469
+ endpoint = f"{self.base_url}/v1/domain/jobs/{job_identifier}/f/alertReport.json"
470
+
471
+ try:
472
+ response = self._make_signed_request("GET", endpoint)
473
+
474
+ if response.status_code == 200:
475
+ return cast(List[Dict[str, Any]], response.json())
476
+ elif response.status_code == 401:
477
+ raise AuthenticationError("Authentication failed - check AWS credentials")
478
+ elif response.status_code == 404:
479
+ raise JobNotFoundError(f"Alert report not found for job {job_identifier}")
480
+ else:
481
+ response.raise_for_status()
482
+ return cast(List[Dict[str, Any]], response.json())
483
+
484
+ except requests.exceptions.RequestException as e:
485
+ raise ProverAPIError(f"Failed to fetch alert report for {job_identifier}: {e}")