certora-prover-cli 0.2.1__tar.gz → 0.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/PKG-INFO +1 -1
  2. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/_version.py +2 -2
  3. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/aws_data_fetcher.py +7 -0
  4. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/data_fetcher.py +7 -0
  5. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/local_data_fetcher.py +14 -0
  6. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/main.py +78 -6
  7. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/auth.py +40 -4
  8. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/prover_cli.py +18 -0
  9. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/.gitignore +0 -0
  10. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/LICENSE +0 -0
  11. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/README.md +0 -0
  12. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/pyproject.toml +0 -0
  13. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/__init__.py +0 -0
  14. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/__init__.py +0 -0
  15. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/base_data_fetcher.py +0 -0
  16. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/cache.py +0 -0
  17. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/tree_parser.py +0 -0
  18. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/url_utils.py +0 -0
  19. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/aws_auth.py +0 -0
  20. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/breadcrumb.py +0 -0
  21. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/exceptions.py +0 -0
  22. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/group_summary.py +0 -0
  23. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/job_report.py +0 -0
  24. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/models.py +0 -0
  25. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/parsers.py +0 -0
  26. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/prover_api.py +0 -0
  27. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/py.typed +0 -0
  28. {certora_prover_cli-0.2.1 → certora_prover_cli-0.2.3}/src/prover_output_utility/sighash.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: certora-prover-cli
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: A Python API and CLI for parsing outputs from the Certora Prover
5
5
  Project-URL: Homepage, https://github.com/Certora/ProverCLI
6
6
  Project-URL: Repository, https://github.com/Certora/ProverCLI
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.1'
22
- __version_tuple__ = version_tuple = (0, 2, 1)
21
+ __version__ = version = '0.2.3'
22
+ __version_tuple__ = version_tuple = (0, 2, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -206,6 +206,13 @@ class AWSDataFetcher(BaseDataFetcher):
206
206
  except json.JSONDecodeError as e:
207
207
  raise ProverAPIError(f"Failed to parse statsdata.json for {job_identifier}: {e}")
208
208
 
209
+ def fetch_prover_version(self, job_identifier: str) -> Dict[str, Any]:
210
+ """Fetch a job's cvt_version.json (the prover version / commit it ran on)."""
211
+ try:
212
+ return cast(Dict[str, Any], json.loads(self.fetch_output_file(job_identifier, "cvt_version.json")))
213
+ except json.JSONDecodeError as e:
214
+ raise ProverAPIError(f"Failed to parse cvt_version.json for {job_identifier}: {e}")
215
+
209
216
  def fetch_output_file(self, job_identifier: str, rel_path: str) -> str:
210
217
  """Fetch the raw text of a Reports/-relative output file (e.g. unsat_core_map.json).
211
218
 
@@ -263,6 +263,13 @@ class DataFetcher(BaseDataFetcher):
263
263
  except json.JSONDecodeError as e:
264
264
  raise ProverAPIError(f"Failed to parse statsdata.json for {job_identifier}: {e}")
265
265
 
266
+ def fetch_prover_version(self, job_identifier: str) -> Dict[str, Any]:
267
+ """Fetch a job's cvt_version.json (the prover version / commit it ran on)."""
268
+ try:
269
+ return cast(Dict[str, Any], json.loads(self.fetch_output_file(job_identifier, "cvt_version.json")))
270
+ except json.JSONDecodeError as e:
271
+ raise ProverAPIError(f"Failed to parse cvt_version.json for {job_identifier}: {e}")
272
+
266
273
  @handle_token_expiration
267
274
  def fetch_output_file(self, job_identifier: str, rel_path: str) -> str:
268
275
  """Fetch the raw text of a Reports/-relative output file (e.g. unsat_core_map.json).
@@ -206,6 +206,20 @@ class LocalDataFetcher(BaseDataFetcher):
206
206
  except json.JSONDecodeError as e:
207
207
  raise ProverAPIError(f"Failed to parse statsdata.json: {e}")
208
208
 
209
+ def fetch_prover_version(self, job_identifier: str) -> Dict[str, Any]:
210
+ """Read cvt_version.json from a local emv-* folder's inputs/ directory."""
211
+ emv_path = os.path.abspath(job_identifier)
212
+ if not os.path.exists(emv_path):
213
+ raise JobNotFoundError(f"Local prover output path not found: {job_identifier}")
214
+ cvt_version_path = os.path.join(emv_path, "inputs", "cvt_version.json")
215
+ if not os.path.exists(cvt_version_path):
216
+ raise JobNotFoundError(f"cvt_version.json not found: {cvt_version_path}")
217
+ try:
218
+ with open(cvt_version_path, "r") as f:
219
+ return json.load(f)
220
+ except json.JSONDecodeError as e:
221
+ raise ProverAPIError(f"Failed to parse cvt_version.json: {e}")
222
+
209
223
  def fetch_outputs(self, job_identifier: str) -> bytes:
210
224
  """
211
225
  Fetch outputs archive for local job - not applicable for local files.
@@ -18,8 +18,10 @@ from pathlib import Path
18
18
  from typing import Any, Dict, List, Optional
19
19
 
20
20
  import requests
21
+ from requests.adapters import HTTPAdapter
22
+ from urllib3.util.retry import Retry
21
23
 
22
- from ..auth import ProverAuth
24
+ from ..auth import ProverAuth, organization_api_key
23
25
  from ..aws_auth import AWSAuth
24
26
  from ..breadcrumb import BreadcrumbParser
25
27
  from ..exceptions import JobNotFoundError, ProverAPIError
@@ -45,6 +47,49 @@ from .tree_parser import TreeParser
45
47
  from .url_utils import extract_job_id, extract_job_identifier
46
48
 
47
49
 
50
+ #: Seconds a request may go without progress before it is abandoned. requests measures this per
51
+ #: socket read rather than over the whole call, so a large download that keeps streaming is
52
+ #: unaffected; it fires when the peer goes quiet. The headroom over the SigV4 fetcher's 30 is for
53
+ #: the outputs archive, where the server has a lot to assemble before the first byte arrives.
54
+ _REQUEST_TIMEOUT_S = 60
55
+
56
+ #: A silent peer that resets or 5xx's mid-transfer is transient — the same request usually succeeds
57
+ #: moments later. Retry it with exponential backoff (``backoff_factor`` doubles the wait each attempt:
58
+ #: ~0.5s, 1s, 2s), capped at a few attempts so an unrecoverable endpoint still fails promptly rather
59
+ #: than looping. Only the default idempotent methods are retried (urllib3 excludes POST), so a retry
60
+ #: never re-runs a non-idempotent call.
61
+ _MAX_RETRIES = 3
62
+ _BACKOFF_FACTOR = 0.5
63
+
64
+
65
+ class _TimedSession(requests.Session):
66
+ """A session whose requests give up on a silent peer and retry a transient failure.
67
+
68
+ requests applies no timeout of its own, so an unresponsive endpoint holds its caller for as
69
+ long as the socket stays open, and a caller that handed the call to a thread cannot get that
70
+ thread back. Every ``get``/``post`` on a session goes through ``request``, so the timeout default
71
+ belongs here rather than at each call site. A caller that passes its own timeout keeps it. The
72
+ mounted adapter additionally retries transient connection/read failures and 502/503/504 with
73
+ exponential backoff.
74
+ """
75
+
76
+ def __init__(self) -> None:
77
+ super().__init__()
78
+ retry = Retry(
79
+ total=_MAX_RETRIES,
80
+ backoff_factor=_BACKOFF_FACTOR,
81
+ status_forcelist=(502, 503, 504),
82
+ raise_on_status=False, # leave status-code handling to the callers
83
+ )
84
+ adapter = HTTPAdapter(max_retries=retry)
85
+ self.mount("https://", adapter)
86
+ self.mount("http://", adapter)
87
+
88
+ def request(self, *args, **kwargs) -> requests.Response:
89
+ kwargs.setdefault("timeout", _REQUEST_TIMEOUT_S)
90
+ return super().request(*args, **kwargs)
91
+
92
+
48
93
  # Per-job locks for thread-safe fetch operations
49
94
  _fetch_locks: dict[str, threading.Lock] = {}
50
95
  _fetch_locks_lock = threading.Lock()
@@ -143,8 +188,11 @@ class ProverOutputAPI:
143
188
  # Initialize appropriate data fetcher
144
189
  if use_local:
145
190
  self.data_fetcher = LocalDataFetcher()
146
- elif os.getenv("CI") and self._is_aws_available():
147
- # In CI with AWS credentials, use AWS Lambda authentication
191
+ elif organization_api_key() is None and os.getenv("CI") and self._is_aws_available():
192
+ # In CI with AWS credentials, use AWS Lambda authentication. The
193
+ # path exists for automation that has AWS credentials and no
194
+ # Certora login, so a key the caller supplied on purpose takes
195
+ # precedence over it.
148
196
  self.logger.info("Using AWS Lambda authentication in CI")
149
197
  aws_auth = AWSAuth()
150
198
  aws_session = aws_auth.get_session()
@@ -152,15 +200,19 @@ class ProverOutputAPI:
152
200
  # No session needed for AWS auth
153
201
  self.session = None
154
202
  else:
155
- # Regular cookie-based authentication
203
+ # Key or cookie authentication; ProverAuth resolves which applies
156
204
  self.auth = ProverAuth()
157
205
  self._setup_session()
158
206
 
159
207
  def _setup_session(self, force_relogin: bool = False):
160
- """Setup session with authentication cookies."""
161
- self.session = requests.Session()
208
+ """Setup session with whichever credential ProverAuth resolves: an
209
+ organization API key on the Authorization header, or login cookies."""
210
+ self.session = _TimedSession()
162
211
  # Set up authentication
163
212
  self.session.cookies = self.auth.get_auth_cookies(force_relogin=force_relogin)
213
+ # An organization API key travels on every request of the session;
214
+ # with cookie authentication this adds nothing.
215
+ self.session.headers.update(self.auth.get_auth_headers())
164
216
  self.data_fetcher = DataFetcher(self.session, api_instance=self, api_base_url=self.api_base_url)
165
217
 
166
218
  def _is_aws_available(self) -> bool:
@@ -753,6 +805,26 @@ class ProverOutputAPI:
753
805
  except Exception as e:
754
806
  raise ProverAPIError(f"Failed to get statsdata for job {job_identifier}: {e}")
755
807
 
808
+ def get_prover_version(self, job_input: str) -> Dict[str, Any]:
809
+ """
810
+ Get a job's cvt_version.json — the prover version / commit it ran on.
811
+
812
+ Args:
813
+ job_input: Job URL, job ID, or local emv-* path
814
+
815
+ Returns:
816
+ Parsed cvt_version.json contents (e.g. a "version" field and commit hashes)
817
+
818
+ Raises:
819
+ ProverAPIError: If fetching fails
820
+ """
821
+ job_identifier = self._extract_job_identifier(job_input)
822
+
823
+ try:
824
+ return self.data_fetcher.fetch_prover_version(job_identifier)
825
+ except Exception as e:
826
+ raise ProverAPIError(f"Failed to get prover version for job {job_identifier}: {e}")
827
+
756
828
  def get_console_logs(self, job_input: str) -> str:
757
829
  """
758
830
  Get console logs for a job. This contains the full console output
@@ -83,6 +83,30 @@ except ImportError:
83
83
  SERVERS = None
84
84
 
85
85
 
86
+ # Environment variables holding an organization API key, in the order they are
87
+ # consulted. The env-scoped name comes first so a machine configured for one
88
+ # environment cannot be pointed at another by a stray generic value.
89
+ _API_KEY_ENV_VARS: Dict[LoginEnv, tuple[str, ...]] = {
90
+ "prod": ("CERTORA_API_KEY_PROD", "CERTORA_API_KEY"),
91
+ "stg": ("CERTORA_API_KEY_STG", "CERTORA_API_KEY"),
92
+ "dev": ("CERTORA_API_KEY_DEV", "CERTORA_API_KEY"),
93
+ }
94
+
95
+
96
+ def organization_api_key() -> str | None:
97
+ """The organization API key for the current login env, if one is set.
98
+
99
+ This is the key issued on the organization's API page, not ``CERTORAKEY``.
100
+ The API accepts it on the ``Authorization`` header and exchanges it for a
101
+ short-lived token itself, so a caller that has one needs no browser login.
102
+ """
103
+ for name in _API_KEY_ENV_VARS[resolve_login_env()]:
104
+ value = (os.environ.get(name) or "").strip()
105
+ if value:
106
+ return value
107
+ return None
108
+
109
+
86
110
  class ProverAuth:
87
111
  """
88
112
  Handles authentication with Certora Prover API using certora_login.
@@ -129,6 +153,11 @@ class ProverAuth:
129
153
  """
130
154
  cookies = requests.cookies.RequestsCookieJar()
131
155
 
156
+ # An organization API key authenticates on the header instead, so
157
+ # there is nothing to log in for and no cookie to carry.
158
+ if organization_api_key() is not None:
159
+ return cookies
160
+
132
161
  # Skip authentication in CI environment - return empty cookies
133
162
  if os.getenv("CI"):
134
163
  return cookies
@@ -163,10 +192,17 @@ class ProverAuth:
163
192
  """
164
193
  Get authentication headers for API requests.
165
194
 
166
- Note: Headers are typically not needed when using cookies from certora_login.
167
- This method returns an empty dict as headers are handled via cookies.
195
+ An organization API key authenticates on the ``Authorization``
196
+ header: the API classifies the credential by shape and exchanges an
197
+ opaque key for a short-lived token on the caller's behalf, so no
198
+ browser login and no cookie are involved. Without a key the cookies
199
+ from certora_login carry the identity and there is no header to add.
168
200
 
169
201
  Returns:
170
- Empty dictionary (authentication is via cookies)
202
+ The bearer header for the organization API key, or an empty
203
+ dictionary when authentication is via cookies.
171
204
  """
172
- return {}
205
+ key = organization_api_key()
206
+ if key is None:
207
+ return {}
208
+ return {"Authorization": f"Bearer {key}"}
@@ -193,6 +193,11 @@ Environment Variables:
193
193
  parser.add_argument("--calltrace", help="Get calltrace for specific output file path")
194
194
  parser.add_argument("--breadcrumbs", help="Get breadcrumb trace from DAP file")
195
195
  parser.add_argument("--statsdata", action="store_true", help="Get statsdata.json for the job")
196
+ parser.add_argument(
197
+ "--prover-version",
198
+ action="store_true",
199
+ help="Get cvt_version.json for the job (the prover version / commit it ran on)",
200
+ )
196
201
  parser.add_argument(
197
202
  "--all-checks",
198
203
  action="store_true",
@@ -458,6 +463,11 @@ Environment Variables:
458
463
  statsdata = api.get_statsdata(job_input)
459
464
  result = {"job_id": job_input, "statsdata": statsdata}
460
465
 
466
+ elif args.prover_version:
467
+ # Get cvt_version.json (prover version / commit the job ran on)
468
+ prover_version = api.get_prover_version(job_input)
469
+ result = {"job_id": job_input, "prover_version": prover_version}
470
+
461
471
  elif args.all_checks:
462
472
  # Get all checks (both successful and failed)
463
473
  all_checks = api.get_all_checks(job_input)
@@ -534,6 +544,8 @@ Environment Variables:
534
544
  )
535
545
  elif args.statsdata:
536
546
  print(f"✅ Statsdata retrieved. Results written to {args.output}")
547
+ elif args.prover_version:
548
+ print(f"✅ prover_version retrieved. Results written to {args.output}")
537
549
  elif args.all_checks:
538
550
  count = result.get("checks_count", 0)
539
551
  print(f"Found {count} checks (all tree nodes). Results written to {args.output}")
@@ -611,6 +623,12 @@ Environment Variables:
611
623
  print(f" ... and {len(result['statsdata']) - 5} more keys")
612
624
  print("Use --format json to see full data")
613
625
 
626
+ elif args.prover_version:
627
+ print(f"Job ID: {job_input}")
628
+ print("✅ Retrieved cvt_version.json")
629
+ for key, value in result["prover_version"].items():
630
+ print(f" {key}: {value}")
631
+
614
632
  elif args.all_checks:
615
633
  # Show all checks (both successful and failed)
616
634
  print(f"Job ID: {job_input}")