codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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.
Files changed (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -49,6 +49,7 @@ def resolve_config_path(
49
49
  3. Otherwise, resolve relative to repo_root
50
50
  4. Reject '..' segments unless allow_dotdot=True
51
51
  5. Reject paths escaping repo_root (except home expansion)
52
+ - allow_dotdot allows '..' segments inside the repo, not escaping the repo
52
53
 
53
54
  Args:
54
55
  value: Path string or Path object
@@ -112,7 +113,7 @@ def resolve_config_path(
112
113
 
113
114
  resolved = (repo_root / path).resolve()
114
115
 
115
- if not allow_home and not allow_dotdot and not resolved.is_relative_to(repo_root):
116
+ if not resolved.is_relative_to(repo_root):
116
117
  raise ConfigPathError(
117
118
  "Path resolves outside repo root",
118
119
  path=value_str,
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Optional
11
+
12
+ from .locks import file_lock
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ PMA_AUDIT_LOG_FILENAME = "audit_log.jsonl"
17
+ PMA_AUDIT_LOG_LOCK_SUFFIX = ".lock"
18
+
19
+
20
+ class PmaActionType(str, Enum):
21
+ CHAT_STARTED = "chat_started"
22
+ CHAT_COMPLETED = "chat_completed"
23
+ CHAT_FAILED = "chat_failed"
24
+ CHAT_INTERRUPTED = "chat_interrupted"
25
+ FILE_UPLOADED = "file_uploaded"
26
+ FILE_DOWNLOADED = "file_downloaded"
27
+ FILE_DELETED = "file_deleted"
28
+ FILE_BULK_DELETED = "file_bulk_deleted"
29
+ DOC_UPDATED = "doc_updated"
30
+ DISPATCH_PROCESSED = "dispatch_processed"
31
+ AGENT_ACTION = "agent_action"
32
+ SESSION_NEW = "session_new"
33
+ SESSION_RESET = "session_reset"
34
+ SESSION_STOP = "session_stop"
35
+ SESSION_COMPACT = "session_compact"
36
+ UNKNOWN = "unknown"
37
+
38
+
39
+ @dataclass
40
+ class PmaAuditEntry:
41
+ action_type: PmaActionType
42
+ timestamp: str = field(
43
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
44
+ )
45
+ entry_id: str = ""
46
+ agent: Optional[str] = None
47
+ thread_id: Optional[str] = None
48
+ turn_id: Optional[str] = None
49
+ client_turn_id: Optional[str] = None
50
+ details: dict[str, Any] = field(default_factory=dict)
51
+ status: str = "ok"
52
+ error: Optional[str] = None
53
+ fingerprint: str = ""
54
+
55
+ def __post_init__(self):
56
+ if not self.entry_id:
57
+ import uuid
58
+
59
+ object.__setattr__(self, "entry_id", str(uuid.uuid4()))
60
+ if not self.fingerprint:
61
+ object.__setattr__(self, "fingerprint", self._compute_fingerprint())
62
+
63
+ def _compute_fingerprint(self) -> str:
64
+ base = {
65
+ "action_type": self.action_type.value,
66
+ "agent": self.agent,
67
+ "details": self.details,
68
+ }
69
+ raw = json.dumps(base, sort_keys=True, default=str)
70
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
71
+
72
+
73
+ def default_pma_audit_log_path(hub_root: Path) -> Path:
74
+ return hub_root / ".codex-autorunner" / "pma" / PMA_AUDIT_LOG_FILENAME
75
+
76
+
77
+ class PmaAuditLog:
78
+ def __init__(self, hub_root: Path) -> None:
79
+ self._path = default_pma_audit_log_path(hub_root)
80
+
81
+ @property
82
+ def path(self) -> Path:
83
+ return self._path
84
+
85
+ def _lock_path(self) -> Path:
86
+ return self._path.with_suffix(PMA_AUDIT_LOG_LOCK_SUFFIX)
87
+
88
+ def append(self, entry: PmaAuditEntry) -> str:
89
+ with file_lock(self._lock_path()):
90
+ self._append_unlocked(entry)
91
+ return entry.entry_id
92
+
93
+ def _append_unlocked(self, entry: PmaAuditEntry) -> None:
94
+ self._path.parent.mkdir(parents=True, exist_ok=True)
95
+ line = json.dumps(
96
+ {
97
+ "entry_id": entry.entry_id,
98
+ "action_type": entry.action_type.value,
99
+ "timestamp": entry.timestamp,
100
+ "agent": entry.agent,
101
+ "thread_id": entry.thread_id,
102
+ "turn_id": entry.turn_id,
103
+ "client_turn_id": entry.client_turn_id,
104
+ "details": entry.details,
105
+ "status": entry.status,
106
+ "error": entry.error,
107
+ "fingerprint": entry.fingerprint,
108
+ }
109
+ )
110
+ with open(self._path, "a", encoding="utf-8") as f:
111
+ f.write(line + "\n")
112
+
113
+ def list_recent(
114
+ self, *, limit: int = 100, action_type: Optional[PmaActionType] = None
115
+ ) -> list[PmaAuditEntry]:
116
+ with file_lock(self._lock_path()):
117
+ return self._list_recent_unlocked(limit=limit, action_type=action_type)
118
+
119
+ def _list_recent_unlocked(
120
+ self, *, limit: int = 100, action_type: Optional[PmaActionType] = None
121
+ ) -> list[PmaAuditEntry]:
122
+ if not self._path.exists():
123
+ return []
124
+ entries: list[PmaAuditEntry] = []
125
+ try:
126
+ with open(self._path, "r", encoding="utf-8") as f:
127
+ for line in f:
128
+ line = line.strip()
129
+ if not line:
130
+ continue
131
+ try:
132
+ data = json.loads(line)
133
+ except json.JSONDecodeError:
134
+ continue
135
+ if not isinstance(data, dict):
136
+ continue
137
+ try:
138
+ action_type_str = data.get("action_type")
139
+ event_type = (
140
+ PmaActionType(action_type_str)
141
+ if action_type_str
142
+ else PmaActionType.UNKNOWN
143
+ )
144
+ except ValueError:
145
+ event_type = PmaActionType.UNKNOWN
146
+ if action_type and event_type != action_type:
147
+ continue
148
+ entry = PmaAuditEntry(
149
+ action_type=event_type,
150
+ timestamp=data.get("timestamp", ""),
151
+ entry_id=data.get("entry_id", ""),
152
+ agent=data.get("agent"),
153
+ thread_id=data.get("thread_id"),
154
+ turn_id=data.get("turn_id"),
155
+ client_turn_id=data.get("client_turn_id"),
156
+ details=dict(data.get("details", {}) or {}),
157
+ status=data.get("status", "ok"),
158
+ error=data.get("error"),
159
+ fingerprint=data.get("fingerprint", ""),
160
+ )
161
+ entries.append(entry)
162
+ except OSError as exc:
163
+ logger.warning("Failed to read PMA audit log at %s: %s", self._path, exc)
164
+ return entries[-limit:]
165
+
166
+ def prune_old(self, *, keep_last: int = 1000) -> int:
167
+ with file_lock(self._lock_path()):
168
+ return self._prune_old_unlocked(keep_last=keep_last)
169
+
170
+ def _prune_old_unlocked(self, *, keep_last: int = 1000) -> int:
171
+ if not self._path.exists():
172
+ return 0
173
+ entries = self._list_recent_unlocked(limit=keep_last * 2)
174
+ if len(entries) <= keep_last:
175
+ return 0
176
+ to_keep = entries[-keep_last:]
177
+ self._path.parent.mkdir(parents=True, exist_ok=True)
178
+ with open(self._path, "w", encoding="utf-8") as f:
179
+ for entry in to_keep:
180
+ line = json.dumps(
181
+ {
182
+ "entry_id": entry.entry_id,
183
+ "action_type": entry.action_type.value,
184
+ "timestamp": entry.timestamp,
185
+ "agent": entry.agent,
186
+ "thread_id": entry.thread_id,
187
+ "turn_id": entry.turn_id,
188
+ "client_turn_id": entry.client_turn_id,
189
+ "details": entry.details,
190
+ "status": entry.status,
191
+ "error": entry.error,
192
+ "fingerprint": entry.fingerprint,
193
+ }
194
+ )
195
+ f.write(line + "\n")
196
+ return len(entries) - keep_last
197
+
198
+ def count_fingerprint(
199
+ self, fingerprint: str, *, within_seconds: Optional[int] = None
200
+ ) -> int:
201
+ if not within_seconds:
202
+ return sum(
203
+ 1
204
+ for e in self._list_recent_unlocked(limit=10000)
205
+ if e.fingerprint == fingerprint
206
+ )
207
+ cutoff = datetime.now(timezone.utc).timestamp() - within_seconds
208
+ count = 0
209
+ for entry in self._list_recent_unlocked(limit=10000):
210
+ try:
211
+ ts = datetime.fromisoformat(entry.timestamp.replace("Z", "+00:00"))
212
+ if ts.timestamp() >= cutoff and entry.fingerprint == fingerprint:
213
+ count += 1
214
+ except Exception:
215
+ continue
216
+ return count
217
+
218
+
219
+ __all__ = [
220
+ "PmaActionType",
221
+ "PmaAuditEntry",
222
+ "PmaAuditLog",
223
+ "default_pma_audit_log_path",
224
+ ]