projectdavid-orm 1.13.0__tar.gz → 1.14.0__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.
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/CHANGELOG.md +15 -0
- {projectdavid_orm-1.13.0/src/projectdavid_orm.egg-info → projectdavid_orm-1.14.0}/PKG-INFO +3 -1
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/pyproject.toml +3 -1
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/__init__.py +2 -0
- projectdavid_orm-1.14.0/src/projectdavid_orm/enums.py +20 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/ormInterface.py +2 -0
- projectdavid_orm-1.14.0/src/projectdavid_orm/projectdavid_orm/execution_ledger.py +340 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/models.py +7 -8
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0/src/projectdavid_orm.egg-info}/PKG-INFO +3 -1
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/SOURCES.txt +7 -1
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/requires.txt +2 -0
- projectdavid_orm-1.14.0/tests/test_execution_ledger_registration.py +42 -0
- projectdavid_orm-1.14.0/tests/test_execution_ledger_schema.py +82 -0
- projectdavid_orm-1.14.0/tests/test_runtime_dependency_boundary.py +57 -0
- projectdavid_orm-1.14.0/tests/test_scratchpad_public_exports.py +6 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/LICENSE +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/MANIFEST.in +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/README.md +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/setup.cfg +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/PKG-INFO +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/SOURCES.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/dependency_links.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/entry_points.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/requires.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/top_level.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/constants/__init__.py +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/__init__.py +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/base.py +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/utilities/__init__.py +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/dependency_links.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/top_level.txt +0 -0
- {projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/tests/test_clients.py +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [1.14.0](https://github.com/project-david-ai/projectdavid-orm/compare/v1.13.1...v1.14.0) (2026-11-13)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **orm:** add durable execution lineage and forensic audit models ([21f89d3](https://github.com/project-david-ai/projectdavid-orm/commit/21f89d385273d21e39fcc32911039fcde6457032))
|
|
7
|
+
|
|
8
|
+
## [1.13.1](https://github.com/project-david-ai/projectdavid-orm/compare/v1.13.0...v1.13.1) (2026-09-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** decouple ORM from Common runtime ([4e1ca2e](https://github.com/project-david-ai/projectdavid-orm/commit/4e1ca2e9b2a1b84d014fc0447849964bf974daf5))
|
|
14
|
+
* **exports:** expose Scratchpad through public ORM API ([205ae03](https://github.com/project-david-ai/projectdavid-orm/commit/205ae03ebe801701d9c19ce83e1b59764a8ab93b))
|
|
15
|
+
|
|
1
16
|
# [1.13.0](https://github.com/project-david-ai/projectdavid-orm/compare/v1.12.0...v1.13.0) (2026-09-29)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: projectdavid-orm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Summary: Shared SQLAlchemy ORM models for ProjectDavid services
|
|
5
5
|
Author-email: "Francis N." <engineering@projectdavid.co.uk>
|
|
6
6
|
License: PolyForm Noncommercial License 1.0.0
|
|
@@ -15,6 +15,8 @@ Requires-Dist: sqlalchemy>=2.0
|
|
|
15
15
|
Requires-Dist: pymysql
|
|
16
16
|
Requires-Dist: pydantic>=2.0
|
|
17
17
|
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: passlib==1.7.4
|
|
19
|
+
Requires-Dist: bcrypt==4.0.1
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: pytest; extra == "dev"
|
|
20
22
|
Requires-Dist: black==24.1.1; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "projectdavid-orm"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.14.0"
|
|
8
8
|
description = "Shared SQLAlchemy ORM models for ProjectDavid services"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -19,6 +19,8 @@ dependencies = [
|
|
|
19
19
|
"pymysql",
|
|
20
20
|
"pydantic>=2.0",
|
|
21
21
|
"python-dotenv",
|
|
22
|
+
"passlib==1.7.4",
|
|
23
|
+
"bcrypt==4.0.1",
|
|
22
24
|
]
|
|
23
25
|
|
|
24
26
|
[project.optional-dependencies]
|
|
@@ -15,6 +15,7 @@ from .ormInterface import (
|
|
|
15
15
|
OrmInterface,
|
|
16
16
|
Run,
|
|
17
17
|
Sandbox,
|
|
18
|
+
Scratchpad,
|
|
18
19
|
Thread,
|
|
19
20
|
TrainingJob,
|
|
20
21
|
User,
|
|
@@ -36,6 +37,7 @@ __all__ = [
|
|
|
36
37
|
"Assistant",
|
|
37
38
|
"Action",
|
|
38
39
|
"Sandbox",
|
|
40
|
+
"Scratchpad",
|
|
39
41
|
# File system
|
|
40
42
|
"File",
|
|
41
43
|
"FileStorage",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StatusEnum(str, Enum):
|
|
5
|
+
"""Canonical persistence status values used by ORM models."""
|
|
6
|
+
|
|
7
|
+
deleted = "deleted"
|
|
8
|
+
active = "active"
|
|
9
|
+
queued = "queued"
|
|
10
|
+
in_progress = "in_progress"
|
|
11
|
+
pending_action = "action_required"
|
|
12
|
+
completed = "completed"
|
|
13
|
+
failed = "failed"
|
|
14
|
+
cancelling = "cancelling"
|
|
15
|
+
cancelled = "cancelled"
|
|
16
|
+
pending = "pending"
|
|
17
|
+
processing = "processing"
|
|
18
|
+
expired = "expired"
|
|
19
|
+
retrying = "retrying"
|
|
20
|
+
offline = "offline"
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Durable execution lineage and forensic evidence metadata.
|
|
2
|
+
|
|
3
|
+
These tables survive normal ephemeral Assistant lifecycle operations.
|
|
4
|
+
|
|
5
|
+
Foreign keys to Runs and Actions use RESTRICT, not CASCADE. Controlled
|
|
6
|
+
retention and erasure services must reconcile lineage before deletion.
|
|
7
|
+
|
|
8
|
+
Import this module to register its models on the shared ORM Base.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import (
|
|
16
|
+
JSON,
|
|
17
|
+
BigInteger,
|
|
18
|
+
Boolean,
|
|
19
|
+
Column,
|
|
20
|
+
ForeignKey,
|
|
21
|
+
Index,
|
|
22
|
+
Integer,
|
|
23
|
+
String,
|
|
24
|
+
UniqueConstraint,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .base import Base
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _now() -> int:
|
|
31
|
+
return int(time.time())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now_us() -> int:
|
|
35
|
+
return time.time_ns() // 1_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RunExecutionEpoch(Base):
|
|
39
|
+
"""One execution attempt belonging to a durable logical Run."""
|
|
40
|
+
|
|
41
|
+
__tablename__ = "run_execution_epochs"
|
|
42
|
+
|
|
43
|
+
id = Column(String(64), primary_key=True)
|
|
44
|
+
|
|
45
|
+
run_id = Column(
|
|
46
|
+
String(64),
|
|
47
|
+
ForeignKey("runs.id", ondelete="RESTRICT"),
|
|
48
|
+
nullable=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
root_run_id = Column(
|
|
52
|
+
String(64),
|
|
53
|
+
ForeignKey("runs.id", ondelete="RESTRICT"),
|
|
54
|
+
nullable=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
owner_user_id = Column(String(64), nullable=False)
|
|
58
|
+
epoch_number = Column(Integer, nullable=False)
|
|
59
|
+
|
|
60
|
+
state = Column(
|
|
61
|
+
String(32),
|
|
62
|
+
nullable=False,
|
|
63
|
+
default="reserved",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
created_at = Column(BigInteger, nullable=False, default=_now)
|
|
67
|
+
started_at = Column(BigInteger, nullable=True)
|
|
68
|
+
last_seen_at = Column(BigInteger, nullable=True)
|
|
69
|
+
ended_at = Column(BigInteger, nullable=True)
|
|
70
|
+
terminal_status = Column(String(32), nullable=True)
|
|
71
|
+
|
|
72
|
+
__table_args__ = (
|
|
73
|
+
UniqueConstraint(
|
|
74
|
+
"run_id",
|
|
75
|
+
"epoch_number",
|
|
76
|
+
name="uq_exec_epoch_run_number",
|
|
77
|
+
),
|
|
78
|
+
Index(
|
|
79
|
+
"ix_exec_epoch_root_created",
|
|
80
|
+
"root_run_id",
|
|
81
|
+
"created_at",
|
|
82
|
+
),
|
|
83
|
+
Index(
|
|
84
|
+
"ix_exec_epoch_owner_root",
|
|
85
|
+
"owner_user_id",
|
|
86
|
+
"root_run_id",
|
|
87
|
+
),
|
|
88
|
+
Index(
|
|
89
|
+
"ix_exec_epoch_state_seen",
|
|
90
|
+
"state",
|
|
91
|
+
"last_seen_at",
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExecutionActor(Base):
|
|
97
|
+
"""Durable actor reservation and identity snapshot.
|
|
98
|
+
|
|
99
|
+
One actor can occupy a named slot within a Run execution epoch.
|
|
100
|
+
Database uniqueness prevents competing supervisor reservations.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
__tablename__ = "execution_actors"
|
|
104
|
+
|
|
105
|
+
id = Column(String(64), primary_key=True)
|
|
106
|
+
|
|
107
|
+
epoch_id = Column(
|
|
108
|
+
String(64),
|
|
109
|
+
ForeignKey(
|
|
110
|
+
"run_execution_epochs.id",
|
|
111
|
+
ondelete="RESTRICT",
|
|
112
|
+
),
|
|
113
|
+
nullable=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
slot = Column(String(48), nullable=False)
|
|
117
|
+
execution_role = Column(String(48), nullable=False)
|
|
118
|
+
|
|
119
|
+
# Snapshots intentionally have no FK to assistants.
|
|
120
|
+
# Assistant cleanup must not invalidate execution provenance.
|
|
121
|
+
assistant_id_snapshot = Column(String(64), nullable=True)
|
|
122
|
+
model_snapshot = Column(String(128), nullable=True)
|
|
123
|
+
|
|
124
|
+
ephemeral = Column(Boolean, nullable=False, default=False)
|
|
125
|
+
|
|
126
|
+
state = Column(
|
|
127
|
+
String(32),
|
|
128
|
+
nullable=False,
|
|
129
|
+
default="reserved",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
reserved_at = Column(BigInteger, nullable=False, default=_now)
|
|
133
|
+
activated_at = Column(BigInteger, nullable=True)
|
|
134
|
+
retired_at = Column(BigInteger, nullable=True)
|
|
135
|
+
last_seen_at = Column(BigInteger, nullable=True)
|
|
136
|
+
|
|
137
|
+
__table_args__ = (
|
|
138
|
+
UniqueConstraint(
|
|
139
|
+
"epoch_id",
|
|
140
|
+
"slot",
|
|
141
|
+
name="uq_exec_actor_epoch_slot",
|
|
142
|
+
),
|
|
143
|
+
Index(
|
|
144
|
+
"ix_exec_actor_assistant",
|
|
145
|
+
"assistant_id_snapshot",
|
|
146
|
+
),
|
|
147
|
+
Index(
|
|
148
|
+
"ix_exec_actor_state_seen",
|
|
149
|
+
"state",
|
|
150
|
+
"last_seen_at",
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RunDelegation(Base):
|
|
156
|
+
"""Links a parent Action to a delegated child execution."""
|
|
157
|
+
|
|
158
|
+
__tablename__ = "run_delegations"
|
|
159
|
+
|
|
160
|
+
id = Column(String(64), primary_key=True)
|
|
161
|
+
|
|
162
|
+
parent_epoch_id = Column(
|
|
163
|
+
String(64),
|
|
164
|
+
ForeignKey(
|
|
165
|
+
"run_execution_epochs.id",
|
|
166
|
+
ondelete="RESTRICT",
|
|
167
|
+
),
|
|
168
|
+
nullable=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
child_epoch_id = Column(
|
|
172
|
+
String(64),
|
|
173
|
+
ForeignKey(
|
|
174
|
+
"run_execution_epochs.id",
|
|
175
|
+
ondelete="RESTRICT",
|
|
176
|
+
),
|
|
177
|
+
nullable=False,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parent_action_id = Column(
|
|
181
|
+
String(64),
|
|
182
|
+
ForeignKey("actions.id", ondelete="RESTRICT"),
|
|
183
|
+
nullable=False,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
initiating_actor_id = Column(
|
|
187
|
+
String(64),
|
|
188
|
+
ForeignKey("execution_actors.id", ondelete="RESTRICT"),
|
|
189
|
+
nullable=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
created_at = Column(BigInteger, nullable=False, default=_now)
|
|
193
|
+
|
|
194
|
+
__table_args__ = (
|
|
195
|
+
UniqueConstraint(
|
|
196
|
+
"child_epoch_id",
|
|
197
|
+
name="uq_run_delegation_child_epoch",
|
|
198
|
+
),
|
|
199
|
+
Index(
|
|
200
|
+
"ix_run_delegation_parent",
|
|
201
|
+
"parent_epoch_id",
|
|
202
|
+
),
|
|
203
|
+
Index(
|
|
204
|
+
"ix_run_delegation_parent_action",
|
|
205
|
+
"parent_action_id",
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ExecutionEvent(Base):
|
|
211
|
+
"""Structured, ordered execution evidence.
|
|
212
|
+
|
|
213
|
+
Raw reasoning, credentials and unfiltered tool outputs must not be
|
|
214
|
+
stored in details. The writer service must enforce an allowlist,
|
|
215
|
+
redact secrets, and protect events against unauthorised mutation.
|
|
216
|
+
|
|
217
|
+
Evidence digests require protected external checkpoints to provide
|
|
218
|
+
meaningful tamper-evidence.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
__tablename__ = "execution_events"
|
|
222
|
+
|
|
223
|
+
id = Column(
|
|
224
|
+
BigInteger,
|
|
225
|
+
primary_key=True,
|
|
226
|
+
autoincrement=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
epoch_id = Column(
|
|
230
|
+
String(64),
|
|
231
|
+
ForeignKey(
|
|
232
|
+
"run_execution_epochs.id",
|
|
233
|
+
ondelete="RESTRICT",
|
|
234
|
+
),
|
|
235
|
+
nullable=False,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
actor_id = Column(
|
|
239
|
+
String(64),
|
|
240
|
+
ForeignKey(
|
|
241
|
+
"execution_actors.id",
|
|
242
|
+
ondelete="RESTRICT",
|
|
243
|
+
),
|
|
244
|
+
nullable=True,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
origin_run_id = Column(
|
|
248
|
+
String(64),
|
|
249
|
+
ForeignKey("runs.id", ondelete="RESTRICT"),
|
|
250
|
+
nullable=False,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
event_type = Column(String(64), nullable=False)
|
|
254
|
+
schema_version = Column(Integer, nullable=False, default=1)
|
|
255
|
+
|
|
256
|
+
evidence_class = Column(
|
|
257
|
+
String(24),
|
|
258
|
+
nullable=False,
|
|
259
|
+
default="operational",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Microsecond timestamps permit high-resolution reconstruction
|
|
263
|
+
# of interleaved agent and tool activity.
|
|
264
|
+
occurred_at_us = Column(BigInteger, nullable=False)
|
|
265
|
+
recorded_at_us = Column(
|
|
266
|
+
BigInteger,
|
|
267
|
+
nullable=False,
|
|
268
|
+
default=_now_us,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
evidence_expires_at = Column(BigInteger, nullable=True)
|
|
272
|
+
|
|
273
|
+
action_id_snapshot = Column(String(64), nullable=True)
|
|
274
|
+
tool_call_id_snapshot = Column(String(64), nullable=True)
|
|
275
|
+
|
|
276
|
+
details = Column(JSON, nullable=False, default=dict)
|
|
277
|
+
|
|
278
|
+
# Opaque reference to governed, separately protected evidence.
|
|
279
|
+
evidence_ref = Column(String(255), nullable=True)
|
|
280
|
+
evidence_sha256 = Column(String(64), nullable=True)
|
|
281
|
+
|
|
282
|
+
previous_digest = Column(String(64), nullable=True)
|
|
283
|
+
record_digest = Column(String(64), nullable=True)
|
|
284
|
+
|
|
285
|
+
__table_args__ = (
|
|
286
|
+
Index(
|
|
287
|
+
"ix_exec_event_epoch_id",
|
|
288
|
+
"epoch_id",
|
|
289
|
+
"id",
|
|
290
|
+
),
|
|
291
|
+
Index(
|
|
292
|
+
"ix_exec_event_origin_time",
|
|
293
|
+
"origin_run_id",
|
|
294
|
+
"occurred_at_us",
|
|
295
|
+
),
|
|
296
|
+
Index(
|
|
297
|
+
"ix_exec_event_type_time",
|
|
298
|
+
"event_type",
|
|
299
|
+
"occurred_at_us",
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class EvidenceRetentionHold(Base):
|
|
305
|
+
"""Authorised preservation hold for one root Run.
|
|
306
|
+
|
|
307
|
+
An open hold blocks ordinary retention purging. Releasing a hold
|
|
308
|
+
does not independently authorise deletion of otherwise protected data.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
__tablename__ = "evidence_retention_holds"
|
|
312
|
+
|
|
313
|
+
id = Column(String(64), primary_key=True)
|
|
314
|
+
|
|
315
|
+
root_run_id = Column(
|
|
316
|
+
String(64),
|
|
317
|
+
ForeignKey("runs.id", ondelete="RESTRICT"),
|
|
318
|
+
nullable=False,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
case_reference = Column(String(128), nullable=False)
|
|
322
|
+
reason_code = Column(String(48), nullable=False)
|
|
323
|
+
authorised_by = Column(String(64), nullable=False)
|
|
324
|
+
|
|
325
|
+
created_at = Column(BigInteger, nullable=False, default=_now)
|
|
326
|
+
reviewed_at = Column(BigInteger, nullable=True)
|
|
327
|
+
expires_at = Column(BigInteger, nullable=True)
|
|
328
|
+
released_at = Column(BigInteger, nullable=True)
|
|
329
|
+
|
|
330
|
+
__table_args__ = (
|
|
331
|
+
Index(
|
|
332
|
+
"ix_evidence_hold_root_released",
|
|
333
|
+
"root_run_id",
|
|
334
|
+
"released_at",
|
|
335
|
+
),
|
|
336
|
+
Index(
|
|
337
|
+
"ix_evidence_hold_case",
|
|
338
|
+
"case_reference",
|
|
339
|
+
),
|
|
340
|
+
)
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/models.py
RENAMED
|
@@ -3,9 +3,6 @@ import time
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
5
5
|
from passlib.context import CryptContext
|
|
6
|
-
from projectdavid_common import ValidationInterface
|
|
7
|
-
from projectdavid_common.schemas.enums import StatusEnum
|
|
8
|
-
from projectdavid_common.utilities.logging_service import LoggingUtility
|
|
9
6
|
from sqlalchemy import JSON, BigInteger, Boolean, Column, DateTime
|
|
10
7
|
from sqlalchemy import Enum as SAEnum
|
|
11
8
|
from sqlalchemy import (
|
|
@@ -22,11 +19,13 @@ from sqlalchemy import (
|
|
|
22
19
|
from sqlalchemy.ext.mutable import MutableDict
|
|
23
20
|
from sqlalchemy.orm import joinedload, relationship
|
|
24
21
|
|
|
25
|
-
from .
|
|
26
|
-
|
|
27
|
-
logger = LoggingUtility()
|
|
22
|
+
from projectdavid_orm.enums import StatusEnum
|
|
28
23
|
|
|
29
|
-
|
|
24
|
+
# Register execution-lineage models on the shared declarative Base.
|
|
25
|
+
# This import is intentionally placed after existing model declarations.
|
|
26
|
+
# Required for Alembic and normal ORM metadata discovery.
|
|
27
|
+
from . import execution_ledger as _execution_ledger # noqa: E402, F401
|
|
28
|
+
from .base import Base
|
|
30
29
|
|
|
31
30
|
# --- Association Tables ---
|
|
32
31
|
|
|
@@ -428,7 +427,7 @@ class Run(Base):
|
|
|
428
427
|
required_action = Column(String(256), nullable=True)
|
|
429
428
|
response_format = Column(String(64), nullable=True)
|
|
430
429
|
|
|
431
|
-
status = Column(SAEnum(
|
|
430
|
+
status = Column(SAEnum(StatusEnum), nullable=False)
|
|
432
431
|
|
|
433
432
|
thread_id = Column(String(64), nullable=False)
|
|
434
433
|
tool_choice = Column(String(64), nullable=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: projectdavid-orm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Summary: Shared SQLAlchemy ORM models for ProjectDavid services
|
|
5
5
|
Author-email: "Francis N." <engineering@projectdavid.co.uk>
|
|
6
6
|
License: PolyForm Noncommercial License 1.0.0
|
|
@@ -15,6 +15,8 @@ Requires-Dist: sqlalchemy>=2.0
|
|
|
15
15
|
Requires-Dist: pymysql
|
|
16
16
|
Requires-Dist: pydantic>=2.0
|
|
17
17
|
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: passlib==1.7.4
|
|
19
|
+
Requires-Dist: bcrypt==4.0.1
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: pytest; extra == "dev"
|
|
20
22
|
Requires-Dist: black==24.1.1; extra == "dev"
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/SOURCES.txt
RENAMED
|
@@ -10,6 +10,7 @@ src/projectdavid_common.egg-info/entry_points.txt
|
|
|
10
10
|
src/projectdavid_common.egg-info/requires.txt
|
|
11
11
|
src/projectdavid_common.egg-info/top_level.txt
|
|
12
12
|
src/projectdavid_orm/__init__.py
|
|
13
|
+
src/projectdavid_orm/enums.py
|
|
13
14
|
src/projectdavid_orm/ormInterface.py
|
|
14
15
|
src/projectdavid_orm.egg-info/PKG-INFO
|
|
15
16
|
src/projectdavid_orm.egg-info/SOURCES.txt
|
|
@@ -19,6 +20,11 @@ src/projectdavid_orm.egg-info/top_level.txt
|
|
|
19
20
|
src/projectdavid_orm/constants/__init__.py
|
|
20
21
|
src/projectdavid_orm/projectdavid_orm/__init__.py
|
|
21
22
|
src/projectdavid_orm/projectdavid_orm/base.py
|
|
23
|
+
src/projectdavid_orm/projectdavid_orm/execution_ledger.py
|
|
22
24
|
src/projectdavid_orm/projectdavid_orm/models.py
|
|
23
25
|
src/projectdavid_orm/utilities/__init__.py
|
|
24
|
-
tests/test_clients.py
|
|
26
|
+
tests/test_clients.py
|
|
27
|
+
tests/test_execution_ledger_registration.py
|
|
28
|
+
tests/test_execution_ledger_schema.py
|
|
29
|
+
tests/test_runtime_dependency_boundary.py
|
|
30
|
+
tests/test_scratchpad_public_exports.py
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Verify normal ORM imports register the execution ledger.
|
|
2
|
+
|
|
3
|
+
The test runs in a fresh Python interpreter so that earlier test imports
|
|
4
|
+
cannot accidentally register the tables and mask a packaging defect.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_fresh_models_import_registers_execution_ledger():
|
|
12
|
+
code = """
|
|
13
|
+
from projectdavid_orm.projectdavid_orm import models
|
|
14
|
+
|
|
15
|
+
expected = {
|
|
16
|
+
"run_execution_epochs",
|
|
17
|
+
"execution_actors",
|
|
18
|
+
"run_delegations",
|
|
19
|
+
"execution_events",
|
|
20
|
+
"evidence_retention_holds",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registered = set(models.User.metadata.tables)
|
|
24
|
+
missing = expected - registered
|
|
25
|
+
|
|
26
|
+
assert not missing, (
|
|
27
|
+
f"Unregistered execution ledger tables: {sorted(missing)}"
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
[sys.executable, "-c", code],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
check=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert result.returncode == 0, (
|
|
39
|
+
"Fresh ORM import failed.\n"
|
|
40
|
+
f"STDOUT:\n{result.stdout}\n"
|
|
41
|
+
f"STDERR:\n{result.stderr}"
|
|
42
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Structural tests for Project David execution-lineage models."""
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import UniqueConstraint
|
|
4
|
+
from sqlalchemy.dialects.mysql import dialect as mysql_dialect
|
|
5
|
+
from sqlalchemy.schema import CreateTable
|
|
6
|
+
|
|
7
|
+
from projectdavid_orm.projectdavid_orm import execution_ledger, models
|
|
8
|
+
|
|
9
|
+
EXPECTED_TABLES = {
|
|
10
|
+
"run_execution_epochs",
|
|
11
|
+
"execution_actors",
|
|
12
|
+
"run_delegations",
|
|
13
|
+
"execution_events",
|
|
14
|
+
"evidence_retention_holds",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_models_share_existing_metadata():
|
|
19
|
+
metadata = models.User.metadata
|
|
20
|
+
|
|
21
|
+
assert EXPECTED_TABLES.issubset(metadata.tables.keys())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_supervisor_and_epoch_uniqueness():
|
|
25
|
+
metadata = models.User.metadata
|
|
26
|
+
|
|
27
|
+
expected = {
|
|
28
|
+
"run_execution_epochs": "uq_exec_epoch_run_number",
|
|
29
|
+
"execution_actors": "uq_exec_actor_epoch_slot",
|
|
30
|
+
"run_delegations": "uq_run_delegation_child_epoch",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for table_name, constraint_name in expected.items():
|
|
34
|
+
table = metadata.tables[table_name]
|
|
35
|
+
names = {
|
|
36
|
+
constraint.name
|
|
37
|
+
for constraint in table.constraints
|
|
38
|
+
if isinstance(constraint, UniqueConstraint)
|
|
39
|
+
}
|
|
40
|
+
assert constraint_name in names
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_protected_foreign_keys_restrict_deletion():
|
|
44
|
+
metadata = models.User.metadata
|
|
45
|
+
|
|
46
|
+
for name in EXPECTED_TABLES:
|
|
47
|
+
table = metadata.tables[name]
|
|
48
|
+
|
|
49
|
+
assert table.foreign_keys
|
|
50
|
+
|
|
51
|
+
for foreign_key in table.foreign_keys:
|
|
52
|
+
assert foreign_key.ondelete == "RESTRICT"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_actor_identity_survives_assistant_deletion():
|
|
56
|
+
actor_table = models.User.metadata.tables["execution_actors"]
|
|
57
|
+
|
|
58
|
+
assistant_id = actor_table.c.assistant_id_snapshot
|
|
59
|
+
|
|
60
|
+
# This is an identity snapshot, deliberately not a foreign key.
|
|
61
|
+
assert not assistant_id.foreign_keys
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_forensic_event_resolution():
|
|
65
|
+
table = models.User.metadata.tables["execution_events"]
|
|
66
|
+
|
|
67
|
+
assert "occurred_at_us" in table.c
|
|
68
|
+
assert "recorded_at_us" in table.c
|
|
69
|
+
assert "evidence_sha256" in table.c
|
|
70
|
+
assert "evidence_expires_at" in table.c
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_mysql_ddl_compiles_for_every_new_table():
|
|
74
|
+
metadata = models.User.metadata
|
|
75
|
+
dialect = mysql_dialect()
|
|
76
|
+
|
|
77
|
+
for table_name in EXPECTED_TABLES:
|
|
78
|
+
statement = CreateTable(metadata.tables[table_name])
|
|
79
|
+
compiled = str(statement.compile(dialect=dialect))
|
|
80
|
+
|
|
81
|
+
assert "CREATE TABLE" in compiled
|
|
82
|
+
assert "RESTRICT" in compiled
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from projectdavid_orm.enums import StatusEnum
|
|
4
|
+
from projectdavid_orm.projectdavid_orm.models import ApiKey
|
|
5
|
+
|
|
6
|
+
EXPECTED_STATUS_VALUES = {
|
|
7
|
+
"deleted": "deleted",
|
|
8
|
+
"active": "active",
|
|
9
|
+
"queued": "queued",
|
|
10
|
+
"in_progress": "in_progress",
|
|
11
|
+
"pending_action": "action_required",
|
|
12
|
+
"completed": "completed",
|
|
13
|
+
"failed": "failed",
|
|
14
|
+
"cancelling": "cancelling",
|
|
15
|
+
"cancelled": "cancelled",
|
|
16
|
+
"pending": "pending",
|
|
17
|
+
"processing": "processing",
|
|
18
|
+
"expired": "expired",
|
|
19
|
+
"retrying": "retrying",
|
|
20
|
+
"offline": "offline",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_status_enum_persistence_contract_is_preserved():
|
|
25
|
+
assert {item.name: item.value for item in StatusEnum} == EXPECTED_STATUS_VALUES
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_models_have_no_runtime_dependency_on_common():
|
|
29
|
+
models_path = (
|
|
30
|
+
Path(__file__).parents[1]
|
|
31
|
+
/ "src"
|
|
32
|
+
/ "projectdavid_orm"
|
|
33
|
+
/ "projectdavid_orm"
|
|
34
|
+
/ "models.py"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
source = models_path.read_text(encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
assert "projectdavid_common" not in source
|
|
40
|
+
assert "ValidationInterface" not in source
|
|
41
|
+
assert "LoggingUtility" not in source
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_bcrypt_backend_hashes_and_verifies_api_keys():
|
|
45
|
+
plain = "project-david-runtime-dependency-probe"
|
|
46
|
+
|
|
47
|
+
hashed = ApiKey.hash_key(plain)
|
|
48
|
+
|
|
49
|
+
assert hashed != plain
|
|
50
|
+
|
|
51
|
+
api_key = ApiKey(
|
|
52
|
+
key_name="dependency-probe",
|
|
53
|
+
hashed_key=hashed,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert api_key.verify_key(plain)
|
|
57
|
+
assert not api_key.verify_key(plain + "-invalid")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/PKG-INFO
RENAMED
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/requires.txt
RENAMED
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_common.egg-info/top_level.txt
RENAMED
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/constants/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/projectdavid_orm/base.py
RENAMED
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm/utilities/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{projectdavid_orm-1.13.0 → projectdavid_orm-1.14.0}/src/projectdavid_orm.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|