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,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
+ }