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,176 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Certora Ltd.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Abstract base class for data fetchers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseDataFetcher(ABC):
|
|
14
|
+
"""Abstract base class for fetching prover data from different sources."""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def fetch_raw_output(self, job_identifier: str) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
Fetch raw output data.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Raw job data
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ProverAPIError: If fetch fails
|
|
29
|
+
"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def fetch_tree_view_data(self, job_identifier: str, path: str = "treeViewStatus.json") -> Dict[str, Any]:
|
|
34
|
+
"""
|
|
35
|
+
Fetch tree-view data.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
39
|
+
path: Optional path within the tree-view
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tree-view data
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ProverAPIError: If fetch fails
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def list_recent_jobs(
|
|
51
|
+
self,
|
|
52
|
+
created_after: str,
|
|
53
|
+
limit: int = 100,
|
|
54
|
+
all_users: bool = True,
|
|
55
|
+
) -> List[Dict[str, Any]]:
|
|
56
|
+
"""
|
|
57
|
+
List recent jobs from the data API.
|
|
58
|
+
|
|
59
|
+
The data API's /v1/domain/jobs endpoint requires createdAfter to be set
|
|
60
|
+
explicitly so this is required.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
created_after: ISO datetime string for range start (lower bound on createdAt)
|
|
64
|
+
limit: Maximum number of jobs to return (the API caps page size at 100)
|
|
65
|
+
all_users: If True, include jobs from all users; if False, only the authenticated user's
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of raw job dicts as returned by the API
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ProverAPIError: If fetch fails
|
|
72
|
+
"""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def fetch_outputs(self, job_identifier: str) -> bytes:
|
|
77
|
+
"""
|
|
78
|
+
Fetch outputs archive (tar.gz) for a job.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Raw tar.gz content as bytes
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ProverAPIError: If fetch fails
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def cancel_jobs(self, job_ids: List[str]) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Cancel multiple jobs.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
job_ids: List of job IDs to cancel
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
API response data
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ProverAPIError: If cancellation fails
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def fetch_group_jobs(
|
|
109
|
+
self, group_id: str, created_after: str, created_before: str
|
|
110
|
+
) -> List[Dict[str, Any]]:
|
|
111
|
+
"""
|
|
112
|
+
Fetch all jobs in a group.
|
|
113
|
+
|
|
114
|
+
The data API requires createdAfter/createdBefore params for the groupIds filter to work.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
group_id: Group UUID
|
|
118
|
+
created_after: ISO datetime string for range start
|
|
119
|
+
created_before: ISO datetime string for range end
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of job dicts
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ProverAPIError: If fetch fails
|
|
126
|
+
"""
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
@abstractmethod
|
|
130
|
+
def fetch_source_files_list(self, job_identifier: str) -> List[Dict[str, Any]]:
|
|
131
|
+
"""
|
|
132
|
+
Fetch the source file tree for a job.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of source file tree nodes
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ProverAPIError: If fetch fails
|
|
142
|
+
"""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def fetch_source_file_content(self, job_identifier: str, path: str) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Fetch the content of a specific source file for a job.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
152
|
+
path: Path to the source file (e.g. ".certora_sources/src/Contract.sol")
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Raw file content as a string
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ProverAPIError: If fetch fails
|
|
159
|
+
"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def fetch_alert_report(self, job_identifier: str) -> List[Dict[str, Any]]:
|
|
164
|
+
"""
|
|
165
|
+
Fetch alert report (alertReport.json) for a job.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
job_identifier: Job identifier (job ID for remote, emv path for local)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Alert report data as a list of alert objects
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ProverAPIError: If fetch fails
|
|
175
|
+
"""
|
|
176
|
+
pass
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Certora Ltd.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Persistent cache for API responses to avoid repeated expensive API calls.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APICache:
|
|
19
|
+
"""
|
|
20
|
+
Persistent file-based cache for API responses.
|
|
21
|
+
Caches breadcrumb data and other expensive API calls to disk.
|
|
22
|
+
Job data is immutable, so cached data never expires.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, cache_dir: Optional[str] = None):
|
|
26
|
+
"""
|
|
27
|
+
Initialize the API cache.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
cache_dir: Directory to store cache files. Defaults to .certora_internal/api_cache in current directory
|
|
31
|
+
"""
|
|
32
|
+
self.logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Set cache directory - use current working directory by default
|
|
35
|
+
if cache_dir:
|
|
36
|
+
self.cache_dir = Path(cache_dir)
|
|
37
|
+
else:
|
|
38
|
+
self.cache_dir = Path.cwd() / ".certora_internal" / "api_cache"
|
|
39
|
+
|
|
40
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Track cache statistics
|
|
43
|
+
self.hits = 0
|
|
44
|
+
self.misses = 0
|
|
45
|
+
|
|
46
|
+
self.logger.debug(f"API cache initialized at {self.cache_dir}")
|
|
47
|
+
|
|
48
|
+
def _get_cache_key(self, method: str, *args) -> str:
|
|
49
|
+
"""Generate a cache key from method name and arguments."""
|
|
50
|
+
# Create a unique key from method and args
|
|
51
|
+
key_parts = [method] + [str(arg) for arg in args]
|
|
52
|
+
key_str = "|".join(key_parts)
|
|
53
|
+
|
|
54
|
+
# Hash for filename safety
|
|
55
|
+
key_hash = hashlib.md5(key_str.encode()).hexdigest()
|
|
56
|
+
|
|
57
|
+
return f"{method}_{key_hash[:16]}"
|
|
58
|
+
|
|
59
|
+
def _get_cache_file(self, cache_key: str) -> Path:
|
|
60
|
+
"""Get the cache file path for a given key."""
|
|
61
|
+
return self.cache_dir / f"{cache_key}.json"
|
|
62
|
+
|
|
63
|
+
def _is_cache_valid(self, cache_file: Path) -> bool:
|
|
64
|
+
"""Check if a cache file exists and is valid. Since job data is immutable, cache never expires."""
|
|
65
|
+
return cache_file.exists()
|
|
66
|
+
|
|
67
|
+
def get(self, method: str, *args) -> Optional[Dict[str, Any]]:
|
|
68
|
+
"""
|
|
69
|
+
Get cached data for a method call.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
method: Method name (e.g., 'get_breadcrumbs')
|
|
73
|
+
*args: Arguments to the method
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Cached data if available and valid, None otherwise
|
|
77
|
+
"""
|
|
78
|
+
cache_key = self._get_cache_key(method, *args)
|
|
79
|
+
cache_file = self._get_cache_file(cache_key)
|
|
80
|
+
|
|
81
|
+
if self._is_cache_valid(cache_file):
|
|
82
|
+
try:
|
|
83
|
+
with open(cache_file, 'r') as f:
|
|
84
|
+
data = json.load(f)
|
|
85
|
+
|
|
86
|
+
self.hits += 1
|
|
87
|
+
self.logger.debug(f"Cache hit for {method} (key: {cache_key})")
|
|
88
|
+
return data.get('result')
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
self.logger.warning(f"Failed to read cache file {cache_file}: {e}")
|
|
92
|
+
# Remove corrupted cache file
|
|
93
|
+
cache_file.unlink(missing_ok=True)
|
|
94
|
+
|
|
95
|
+
self.misses += 1
|
|
96
|
+
self.logger.debug(f"Cache miss for {method} (key: {cache_key})")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def set(self, method: str, result: Any, *args) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Store data in cache.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
method: Method name
|
|
105
|
+
result: Result to cache
|
|
106
|
+
*args: Arguments to the method
|
|
107
|
+
"""
|
|
108
|
+
cache_key = self._get_cache_key(method, *args)
|
|
109
|
+
cache_file = self._get_cache_file(cache_key)
|
|
110
|
+
|
|
111
|
+
cache_data = {
|
|
112
|
+
'method': method,
|
|
113
|
+
'args': [str(arg) for arg in args],
|
|
114
|
+
'result': result,
|
|
115
|
+
'cached_at': datetime.now().isoformat()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with open(cache_file, 'w') as f:
|
|
120
|
+
json.dump(cache_data, f, indent=2, default=str)
|
|
121
|
+
|
|
122
|
+
self.logger.debug(f"Cached result for {method} (key: {cache_key})")
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
self.logger.warning(f"Failed to write cache file {cache_file}: {e}")
|
|
126
|
+
|
|
127
|
+
def clear(self) -> None:
|
|
128
|
+
"""Clear all cache entries."""
|
|
129
|
+
for cache_file in self.cache_dir.glob("*.json"):
|
|
130
|
+
cache_file.unlink()
|
|
131
|
+
|
|
132
|
+
self.logger.info(f"Cleared cache directory: {self.cache_dir}")
|
|
133
|
+
self.hits = 0
|
|
134
|
+
self.misses = 0
|
|
135
|
+
|
|
136
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
137
|
+
"""Get cache statistics."""
|
|
138
|
+
cache_files = list(self.cache_dir.glob("*.json"))
|
|
139
|
+
total_size = sum(f.stat().st_size for f in cache_files)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
'cache_dir': str(self.cache_dir),
|
|
143
|
+
'total_files': len(cache_files),
|
|
144
|
+
'total_size_bytes': total_size,
|
|
145
|
+
'total_size_mb': total_size / (1024 * 1024),
|
|
146
|
+
'hits': self.hits,
|
|
147
|
+
'misses': self.misses,
|
|
148
|
+
'hit_rate': self.hits / (self.hits + self.misses) if (self.hits + self.misses) > 0 else 0
|
|
149
|
+
}
|