certora-prover-cli 0.2.2__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.2 → certora_prover_cli-0.2.3}/PKG-INFO +1 -1
  2. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/_version.py +2 -2
  3. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/aws_data_fetcher.py +7 -0
  4. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/data_fetcher.py +7 -0
  5. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/local_data_fetcher.py +14 -0
  6. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/main.py +32 -5
  7. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/auth.py +40 -4
  8. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/prover_cli.py +18 -0
  9. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/.gitignore +0 -0
  10. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/LICENSE +0 -0
  11. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/README.md +0 -0
  12. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/pyproject.toml +0 -0
  13. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/__init__.py +0 -0
  14. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/__init__.py +0 -0
  15. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/base_data_fetcher.py +0 -0
  16. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/cache.py +0 -0
  17. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/tree_parser.py +0 -0
  18. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/api/url_utils.py +0 -0
  19. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/aws_auth.py +0 -0
  20. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/breadcrumb.py +0 -0
  21. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/exceptions.py +0 -0
  22. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/group_summary.py +0 -0
  23. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/job_report.py +0 -0
  24. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/models.py +0 -0
  25. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/parsers.py +0 -0
  26. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/prover_api.py +0 -0
  27. {certora_prover_cli-0.2.2 → certora_prover_cli-0.2.3}/src/prover_output_utility/py.typed +0 -0
  28. {certora_prover_cli-0.2.2 → 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.2
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.2'
22
- __version_tuple__ = version_tuple = (0, 2, 2)
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.
@@ -21,7 +21,7 @@ import requests
21
21
  from requests.adapters import HTTPAdapter
22
22
  from urllib3.util.retry import Retry
23
23
 
24
- from ..auth import ProverAuth
24
+ from ..auth import ProverAuth, organization_api_key
25
25
  from ..aws_auth import AWSAuth
26
26
  from ..breadcrumb import BreadcrumbParser
27
27
  from ..exceptions import JobNotFoundError, ProverAPIError
@@ -188,8 +188,11 @@ class ProverOutputAPI:
188
188
  # Initialize appropriate data fetcher
189
189
  if use_local:
190
190
  self.data_fetcher = LocalDataFetcher()
191
- elif os.getenv("CI") and self._is_aws_available():
192
- # 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.
193
196
  self.logger.info("Using AWS Lambda authentication in CI")
194
197
  aws_auth = AWSAuth()
195
198
  aws_session = aws_auth.get_session()
@@ -197,15 +200,19 @@ class ProverOutputAPI:
197
200
  # No session needed for AWS auth
198
201
  self.session = None
199
202
  else:
200
- # Regular cookie-based authentication
203
+ # Key or cookie authentication; ProverAuth resolves which applies
201
204
  self.auth = ProverAuth()
202
205
  self._setup_session()
203
206
 
204
207
  def _setup_session(self, force_relogin: bool = False):
205
- """Setup session with authentication cookies."""
208
+ """Setup session with whichever credential ProverAuth resolves: an
209
+ organization API key on the Authorization header, or login cookies."""
206
210
  self.session = _TimedSession()
207
211
  # Set up authentication
208
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())
209
216
  self.data_fetcher = DataFetcher(self.session, api_instance=self, api_base_url=self.api_base_url)
210
217
 
211
218
  def _is_aws_available(self) -> bool:
@@ -798,6 +805,26 @@ class ProverOutputAPI:
798
805
  except Exception as e:
799
806
  raise ProverAPIError(f"Failed to get statsdata for job {job_identifier}: {e}")
800
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
+
801
828
  def get_console_logs(self, job_input: str) -> str:
802
829
  """
803
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}")