projectdavid-orm 1.1.5__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,5 @@
1
+ from .ormInterface import OrmInterface
2
+
3
+ __all__ = [
4
+ "OrmInterface",
5
+ ]
@@ -0,0 +1,8 @@
1
+ from .mime_types import (
2
+ ALLOWED_TEXT_ENCODINGS,
3
+ BROWSER_RENDERABLE_EXTENSIONS,
4
+ SUPPORTED_MIME_TYPES,
5
+ get_mime_type,
6
+ )
7
+ from .timeouts import DEFAULT_TIMEOUT
8
+ from .tools import PLATFORM_TOOLS, SPECIAL_CASE_TOOL_HANDLING, TOOLS_ID_MAP
@@ -0,0 +1,42 @@
1
+ # src/projectdavid_orm/ormInterface.py
2
+ from projectdavid_orm.projectdavid_orm.models import *
3
+
4
+
5
+ class OrmInterface:
6
+ """
7
+ Exposes Pydantic validation classes, retaining their original naming.
8
+
9
+ This interface allows consumers to access the various schemas like:
10
+ - ValidationInterface.FileUploadRequest
11
+ - ValidationInterface.ActionCreate
12
+ - etc.
13
+ """
14
+
15
+ # Api Key model
16
+ ApiKey = ApiKey
17
+ # User model
18
+ User = User
19
+ # GDPR Audit log
20
+ AuditLog = AuditLog
21
+ # Thread model
22
+ Thread = Thread
23
+ # Message model
24
+ Message = Message
25
+ # Run model
26
+ Run = Run
27
+ # Assistant model
28
+ Assistant = Assistant
29
+ # Action model
30
+ Action = Action
31
+ # Sandbox model
32
+ Sandbox = Sandbox
33
+ # File model
34
+ File = File
35
+ # FileStorage model
36
+ FileStorage = FileStorage
37
+ # Batfish model
38
+ BatfishSnapshot = BatfishSnapshot
39
+ # VectorStore model
40
+ VectorStore = VectorStore
41
+ # VectorStoreFile model
42
+ VectorStoreFile = VectorStoreFile
File without changes
@@ -0,0 +1,3 @@
1
+ from sqlalchemy.orm import declarative_base
2
+
3
+ Base = declarative_base()
@@ -0,0 +1,986 @@
1
+ import secrets
2
+ import time
3
+ from datetime import datetime
4
+ from enum import Enum as PyEnum
5
+
6
+ from passlib.context import CryptContext
7
+ from projectdavid_common import ValidationInterface
8
+ from projectdavid_common.projectdavid_orm.base import Base
9
+ from projectdavid_common.schemas.enums import StatusEnum
10
+ from projectdavid_common.utilities.logging_service import LoggingUtility
11
+ from sqlalchemy import (
12
+ JSON,
13
+ BigInteger,
14
+ Boolean,
15
+ Column,
16
+ DateTime,
17
+ )
18
+ from sqlalchemy import Enum as SAEnum
19
+ from sqlalchemy import (
20
+ Float,
21
+ ForeignKey,
22
+ Index,
23
+ Integer,
24
+ String,
25
+ Table,
26
+ Text,
27
+ UniqueConstraint,
28
+ )
29
+ from sqlalchemy.ext.mutable import MutableDict
30
+ from sqlalchemy.orm import joinedload, relationship
31
+
32
+ logger = LoggingUtility()
33
+
34
+ validation = ValidationInterface
35
+
36
+ # --- Association Tables ---
37
+
38
+ thread_participants = Table(
39
+ "thread_participants",
40
+ Base.metadata,
41
+ Column(
42
+ "thread_id",
43
+ String(64),
44
+ ForeignKey("threads.id", ondelete="CASCADE"),
45
+ primary_key=True,
46
+ ),
47
+ Column(
48
+ "user_id",
49
+ String(64),
50
+ ForeignKey("users.id", ondelete="CASCADE"),
51
+ primary_key=True,
52
+ ),
53
+ )
54
+
55
+ user_assistants = Table(
56
+ "user_assistants",
57
+ Base.metadata,
58
+ Column(
59
+ "user_id",
60
+ String(64),
61
+ ForeignKey("users.id", ondelete="CASCADE"),
62
+ primary_key=True,
63
+ ),
64
+ Column(
65
+ "assistant_id",
66
+ String(64),
67
+ ForeignKey("assistants.id", ondelete="CASCADE"),
68
+ primary_key=True,
69
+ ),
70
+ )
71
+
72
+
73
+ # --- Enums & Context ---
74
+
75
+
76
+ class StatusEnum(PyEnum):
77
+ deleted = "deleted"
78
+ active = "active"
79
+ queued = "queued"
80
+ in_progress = "in_progress"
81
+ pending_action = "action_required"
82
+ completed = "completed"
83
+ failed = "failed"
84
+ cancelling = "cancelling"
85
+ cancelled = "cancelled"
86
+ pending = "pending"
87
+ processing = "processing"
88
+ expired = "expired"
89
+ retrying = "retrying"
90
+
91
+
92
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
93
+
94
+
95
+ # --- Core Models ---
96
+
97
+
98
+ class ApiKey(Base):
99
+ __tablename__ = "api_keys"
100
+ id = Column(Integer, primary_key=True, index=True)
101
+ key_name = Column(String(100), nullable=True)
102
+ hashed_key = Column(String(255), unique=True, nullable=False, index=True)
103
+ prefix = Column(String(8), unique=True, nullable=False)
104
+ user_id = Column(
105
+ String(64),
106
+ ForeignKey("users.id", ondelete="CASCADE"),
107
+ nullable=False,
108
+ index=True,
109
+ )
110
+ created_at = Column(DateTime, default=datetime.utcnow)
111
+ expires_at = Column(DateTime, nullable=True)
112
+ last_used_at = Column(DateTime, nullable=True)
113
+ is_active = Column(Boolean, default=True, nullable=False)
114
+ user = relationship("User", back_populates="api_keys")
115
+ __table_args__ = (Index("idx_apikey_user_id_active", "user_id", "is_active"),)
116
+
117
+ @staticmethod
118
+ def generate_key(prefix="sk_"):
119
+ return f"{prefix}{secrets.token_urlsafe(32)}"
120
+
121
+ @staticmethod
122
+ def hash_key(key: str) -> str:
123
+ return pwd_context.hash(key)
124
+
125
+ def verify_key(self, plain_key: str) -> bool:
126
+ return pwd_context.verify(plain_key, self.hashed_key)
127
+
128
+
129
+ class User(Base):
130
+ __tablename__ = "users"
131
+ id = Column(
132
+ String(64),
133
+ primary_key=True,
134
+ index=True,
135
+ comment="Internal unique identifier for the user (e.g., user_...)",
136
+ )
137
+ created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
138
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
139
+ is_admin = Column(
140
+ Boolean,
141
+ default=False,
142
+ nullable=False,
143
+ server_default="0",
144
+ comment="Flag indicating administrative privileges",
145
+ )
146
+ email = Column(
147
+ String(255),
148
+ unique=True,
149
+ index=True,
150
+ nullable=True,
151
+ comment="Primary email address, potentially verified by OAuth provider",
152
+ )
153
+ email_verified = Column(
154
+ Boolean,
155
+ default=False,
156
+ nullable=True,
157
+ comment="Whether the email address has been verified",
158
+ )
159
+
160
+ full_name = Column(String(255), nullable=True, comment="User's full display name")
161
+ given_name = Column(String(128), nullable=True, comment="First name")
162
+ family_name = Column(String(128), nullable=True, comment="Last name")
163
+ picture_url = Column(Text, nullable=True, comment="URL to the user's profile picture")
164
+
165
+ oauth_provider = Column(
166
+ String(50),
167
+ nullable=True,
168
+ index=True,
169
+ comment="Name of the OAuth provider (e.g., 'google', 'github', 'local')",
170
+ )
171
+ provider_user_id = Column(
172
+ String(255),
173
+ nullable=True,
174
+ index=True,
175
+ comment="The unique ID assigned by the OAuth provider",
176
+ )
177
+
178
+ # Relationships
179
+ api_keys = relationship(
180
+ "ApiKey", back_populates="user", cascade="all, delete-orphan", lazy="select"
181
+ )
182
+
183
+ threads = relationship(
184
+ "Thread",
185
+ secondary=thread_participants,
186
+ back_populates="participants",
187
+ lazy="select",
188
+ )
189
+
190
+ assistants = relationship(
191
+ "Assistant", secondary=user_assistants, back_populates="users", lazy="select"
192
+ )
193
+ sandboxes = relationship(
194
+ "Sandbox", back_populates="user", cascade="all, delete-orphan", lazy="select"
195
+ )
196
+ vector_stores = relationship("VectorStore", back_populates="user", lazy="select")
197
+ files = relationship("File", back_populates="user", cascade="all, delete-orphan", lazy="select")
198
+ runs = relationship("Run", back_populates="user", lazy="select")
199
+
200
+ audit_logs = relationship("AuditLog", back_populates="user", lazy="dynamic")
201
+
202
+ __table_args__ = (
203
+ UniqueConstraint("oauth_provider", "provider_user_id", name="uq_user_oauth_provider_id"),
204
+ Index("idx_user_email", "email"),
205
+ Index("idx_user_is_admin", "is_admin"),
206
+ )
207
+
208
+
209
+ # ───────────────────────────────────────────────
210
+ # AUDIT LOGGING (GDPR & Enterprise Compliance)
211
+ # ───────────────────────────────────────────────
212
+ class AuditLog(Base):
213
+ """
214
+ Immutable record of system actions.
215
+ Supports GDPR compliance by tracking deletions and administrative access.
216
+
217
+ FK: user_id → SET NULL on user deletion.
218
+ Audit records must survive user erasure — they are the compliance trail.
219
+ """
220
+
221
+ __tablename__ = "audit_logs"
222
+
223
+ id = Column(BigInteger, primary_key=True, index=True, autoincrement=True)
224
+
225
+ # Who performed the action.
226
+ # Nullable: SET NULL when the user is erased (record survives, identity is anonymised).
227
+ user_id = Column(
228
+ String(64),
229
+ ForeignKey("users.id", ondelete="SET NULL"),
230
+ nullable=True,
231
+ index=True,
232
+ )
233
+
234
+ # What was done
235
+ action = Column(
236
+ String(32),
237
+ nullable=False,
238
+ index=True,
239
+ comment="e.g. CREATE, UPDATE, DELETE, HARD_DELETE, ERASE",
240
+ )
241
+
242
+ # What entity was affected
243
+ entity_type = Column(
244
+ String(64), nullable=False, index=True, comment="e.g. Assistant, User, Thread"
245
+ )
246
+ entity_id = Column(String(64), nullable=False, index=True)
247
+
248
+ # Context
249
+ timestamp = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
250
+ ip_address = Column(String(45), nullable=True)
251
+
252
+ # Detailed Payload (snapshot of changes)
253
+ details = Column(
254
+ JSON, nullable=True, comment="Stores before/after state or reasoning for action"
255
+ )
256
+
257
+ # Relationship
258
+ user = relationship("User", back_populates="audit_logs")
259
+
260
+
261
+ class Thread(Base):
262
+ __tablename__ = "threads"
263
+ id = Column(String(64), primary_key=True, index=True)
264
+ created_at = Column(Integer, nullable=False)
265
+ meta_data = Column(JSON, nullable=False, default={})
266
+ object = Column(String(64), nullable=False)
267
+ tool_resources = Column(JSON, nullable=False, default={})
268
+ participants = relationship("User", secondary=thread_participants, back_populates="threads")
269
+
270
+ # SET NULL on user deletion — thread record is preserved, ownership is cleared.
271
+ # Application-level erase_user() handles physical cleanup of thread content.
272
+ owner_id = Column(
273
+ String(64),
274
+ ForeignKey("users.id", ondelete="SET NULL"),
275
+ nullable=True,
276
+ index=True,
277
+ comment="Canonical creator/owner of this thread. Used for row-level access control.",
278
+ )
279
+ owner = relationship("User", foreign_keys=[owner_id], lazy="select")
280
+
281
+
282
+ class Message(Base):
283
+ __tablename__ = "messages"
284
+ id = Column(String(64), primary_key=True, index=True)
285
+ assistant_id = Column(String(64), index=True)
286
+ attachments = Column(JSON, default=[])
287
+ completed_at = Column(Integer, nullable=True)
288
+
289
+ # --- The "Brain" Output ---
290
+ content = Column(Text(length=4294967295), nullable=False)
291
+ reasoning = Column(
292
+ Text(length=4294967295),
293
+ nullable=True,
294
+ comment="Stores the internal 'thinking' or reasoning tokens from the model.",
295
+ )
296
+
297
+ # --- Native Tool Metadata (Critical for Stage 2/Level 3) ---
298
+ tool_calls = Column(
299
+ JSON,
300
+ nullable=True,
301
+ comment="Stores the native JSON array of tool calls generated by the assistant.",
302
+ )
303
+ tool_call_id = Column(
304
+ String(64),
305
+ nullable=True,
306
+ comment="For messages with role='tool', this links back to the specific tool_call_id.",
307
+ )
308
+
309
+ created_at = Column(Integer, nullable=False)
310
+ incomplete_at = Column(Integer, nullable=True)
311
+ incomplete_details = Column(JSON, nullable=True)
312
+ meta_data = Column(JSON, nullable=False, default={})
313
+ object = Column(String(64), nullable=False)
314
+ role = Column(String(32), nullable=False)
315
+ run_id = Column(String(64), nullable=True)
316
+
317
+ # Kept as simple String, no longer linked to deleted Tool table
318
+ tool_id = Column(String(64), nullable=True)
319
+
320
+ status = Column(String(32), nullable=True)
321
+
322
+ # NOTE: thread_id is intentionally stored as a plain string with no FK constraint.
323
+ # Orphaned messages (after thread deletion) are handled by:
324
+ # a) application-level erase_user() during right-to-erasure
325
+ # b) purge_orphaned_threads daemon (periodic background cleanup)
326
+ thread_id = Column(String(64), nullable=False)
327
+ sender_id = Column(String(64), nullable=True)
328
+
329
+
330
+ class Run(Base):
331
+ __tablename__ = "runs"
332
+
333
+ # CASCADE: runs are deleted when their owning user is erased.
334
+ user_id = Column(
335
+ String(64),
336
+ ForeignKey("users.id", ondelete="CASCADE"),
337
+ nullable=True,
338
+ index=True,
339
+ )
340
+ user = relationship("User", back_populates="runs")
341
+
342
+ id = Column(String(64), primary_key=True)
343
+ assistant_id = Column(String(64), nullable=False)
344
+
345
+ # timestamps as epoch seconds (ints)
346
+ cancelled_at = Column(Integer, nullable=True)
347
+ completed_at = Column(Integer, nullable=True)
348
+ created_at = Column(Integer, default=lambda: int(time.time()))
349
+ expires_at = Column(Integer, nullable=True)
350
+ failed_at = Column(Integer, nullable=True)
351
+ started_at = Column(Integer, nullable=True)
352
+
353
+ incomplete_details = Column(String(256), nullable=True)
354
+
355
+ instructions = Column(Text, nullable=True)
356
+ last_error = Column(String(256), nullable=True)
357
+ max_completion_tokens = Column(Integer, nullable=True)
358
+ max_prompt_tokens = Column(Integer, nullable=True)
359
+
360
+ meta_data = Column(MutableDict.as_mutable(JSON), nullable=True, default=dict)
361
+
362
+ model = Column(String(64), nullable=True)
363
+ object = Column(String(64), nullable=False)
364
+ parallel_tool_calls = Column(Boolean, default=False)
365
+ required_action = Column(String(256), nullable=True)
366
+ response_format = Column(String(64), nullable=True)
367
+
368
+ status = Column(SAEnum(validation.StatusEnum), nullable=False)
369
+
370
+ # NOTE: thread_id stored as plain string — no FK constraint (mirrors Message.thread_id).
371
+ thread_id = Column(String(64), nullable=False)
372
+ tool_choice = Column(String(64), nullable=True)
373
+
374
+ tools = Column(JSON, nullable=True)
375
+
376
+ # --- Agentic Behavior State (Level 3) ---
377
+ current_turn = Column(
378
+ Integer,
379
+ default=0,
380
+ server_default="0",
381
+ comment="The current iteration count of the autonomous loop.",
382
+ )
383
+ max_turns = Column(
384
+ Integer,
385
+ default=1,
386
+ server_default="1",
387
+ comment="Snapshot of Assistant.max_turns at run creation.",
388
+ )
389
+ agent_mode = Column(
390
+ String(32),
391
+ default="standard",
392
+ server_default="standard",
393
+ comment="Determines execution logic: 'standard' (Level 2) or 'autonomous' (Level 3).",
394
+ )
395
+
396
+ truncation_strategy = Column(
397
+ String(16),
398
+ nullable=True,
399
+ default="auto",
400
+ server_default="auto",
401
+ )
402
+
403
+ usage = Column(JSON, nullable=True)
404
+ temperature = Column(Integer, nullable=True)
405
+ top_p = Column(Integer, nullable=True)
406
+ tool_resources = Column(JSON, nullable=True)
407
+
408
+ actions = relationship("Action", back_populates="run")
409
+
410
+
411
+ class Assistant(Base):
412
+ __tablename__ = "assistants"
413
+
414
+ id = Column(String(64), primary_key=True, index=True)
415
+ object = Column(String(64), nullable=False)
416
+ created_at = Column(Integer, nullable=False)
417
+
418
+ name = Column(String(128), nullable=False)
419
+ description = Column(String(256))
420
+ model = Column(String(64))
421
+ instructions = Column(Text)
422
+
423
+ tool_configs = Column(JSON)
424
+ meta_data = Column(JSON)
425
+ top_p = Column(Integer)
426
+ temperature = Column(Integer)
427
+ response_format = Column(String(64))
428
+
429
+ tool_resources = Column(
430
+ JSON,
431
+ nullable=True,
432
+ comment='Resource map keyed by tool type, e.g. {"file_search": {"vector_store_ids": ["vs_123","vs_456"]}}',
433
+ )
434
+
435
+ # SET NULL on user deletion — assistant record is preserved (may be shared with
436
+ # other users via user_assistants). Application-level erase_user() decides
437
+ # whether to soft-delete exclusively-owned assistants.
438
+ owner_id = Column(
439
+ String(64),
440
+ ForeignKey("users.id", ondelete="SET NULL"),
441
+ nullable=True,
442
+ index=True,
443
+ comment=(
444
+ "Canonical owner of this assistant. "
445
+ "Primary key for row-level access filtering. "
446
+ "Separate from the many-to-many (user_assistants) which handles sharing."
447
+ ),
448
+ )
449
+
450
+ # --- Agentic Behavior Extensions (Level 3) ---
451
+ max_turns = Column(
452
+ Integer,
453
+ default=1,
454
+ server_default="1",
455
+ comment="Max number of iterative loops for Level 3 agency. 1 = Standard Level 2 (ReAct).",
456
+ )
457
+ agent_mode = Column(
458
+ Boolean,
459
+ default=False,
460
+ server_default="0",
461
+ nullable=False,
462
+ comment="False = Standard (Level 2), True = Autonomous (Level 3).",
463
+ )
464
+ web_access = Column(
465
+ Boolean,
466
+ default=False,
467
+ server_default="0",
468
+ nullable=False,
469
+ comment="Enable live web search and browsing capabilities.",
470
+ )
471
+ deep_research = Column(
472
+ Boolean,
473
+ default=False,
474
+ server_default="0",
475
+ nullable=False,
476
+ comment="Enable deep research capabilities.",
477
+ )
478
+ engineer = Column(
479
+ Boolean,
480
+ default=False,
481
+ server_default="0",
482
+ nullable=False,
483
+ comment="Enable network engineering capabilities and inventory map access.",
484
+ )
485
+ decision_telemetry = Column(
486
+ Boolean,
487
+ default=False,
488
+ server_default="0",
489
+ nullable=False,
490
+ comment="If True, captures detailed reasoning payloads and confidence scores.",
491
+ )
492
+
493
+ # --- GDPR / Lifecycle Management ---
494
+ deleted_at = Column(
495
+ Integer,
496
+ nullable=True,
497
+ default=None,
498
+ comment="Unix timestamp of soft-deletion. If present, entity is in 'Recycle Bin'.",
499
+ )
500
+
501
+ # --- Relationships ---
502
+ owner = relationship(
503
+ "User",
504
+ foreign_keys=[owner_id],
505
+ lazy="select",
506
+ )
507
+
508
+ users = relationship(
509
+ "User",
510
+ secondary="user_assistants",
511
+ back_populates="assistants",
512
+ lazy="select",
513
+ )
514
+
515
+
516
+ class Action(Base):
517
+ __tablename__ = "actions"
518
+
519
+ id = Column(String(64), primary_key=True, index=True)
520
+
521
+ # CASCADE: actions are deleted when their parent run is deleted.
522
+ run_id = Column(
523
+ String(64),
524
+ ForeignKey("runs.id", ondelete="CASCADE"),
525
+ nullable=True,
526
+ )
527
+
528
+ # --- Agentic Tracking (Level 3) ---
529
+ tool_call_id = Column(
530
+ String(64),
531
+ nullable=True,
532
+ index=True,
533
+ comment="The unique ID linking this action to a specific LLM tool request.",
534
+ )
535
+
536
+ tool_name = Column(
537
+ String(64),
538
+ nullable=True,
539
+ comment="The name of the function/tool to be executed.",
540
+ )
541
+
542
+ turn_index = Column(
543
+ Integer,
544
+ default=0,
545
+ nullable=True,
546
+ comment="The iteration of the autonomous loop.",
547
+ )
548
+
549
+ decision_payload = Column(
550
+ JSON,
551
+ nullable=True,
552
+ comment="The full structured reasoning object (reason, policy, etc) preceding the tool call.",
553
+ )
554
+
555
+ confidence_score = Column(
556
+ Float,
557
+ nullable=True,
558
+ index=True,
559
+ comment="Extracted confidence score (0.0-1.0) to allow fast querying of 'uncertain' agent actions.",
560
+ )
561
+
562
+ triggered_at = Column(DateTime, default=datetime.utcnow)
563
+ expires_at = Column(DateTime, nullable=True)
564
+ is_processed = Column(Boolean, default=False)
565
+ processed_at = Column(DateTime, nullable=True)
566
+ status = Column(String(64), nullable=True)
567
+ function_args = Column(JSON, nullable=True)
568
+ result = Column(JSON, nullable=True)
569
+
570
+ # --- Relationships ---
571
+ run = relationship("Run", back_populates="actions")
572
+
573
+ @staticmethod
574
+ def get_full_action_query(session):
575
+ return session.query(Action).options(joinedload(Action.run))
576
+
577
+
578
+ class Sandbox(Base):
579
+ __tablename__ = "sandboxes"
580
+ id = Column(String(64), primary_key=True, index=True)
581
+
582
+ # CASCADE: sandboxes are deleted when their owning user is erased.
583
+ user_id = Column(
584
+ String(64),
585
+ ForeignKey("users.id", ondelete="CASCADE"),
586
+ nullable=False,
587
+ )
588
+ name = Column(String(128), nullable=False)
589
+ created_at = Column(DateTime, default=datetime.utcnow)
590
+ status = Column(String(32), nullable=False, default="active")
591
+ config = Column(JSON, nullable=True)
592
+ user = relationship("User", back_populates="sandboxes")
593
+
594
+
595
+ class File(Base):
596
+ __tablename__ = "files"
597
+ id = Column(String(64), primary_key=True, index=True)
598
+ object = Column(String(64), nullable=False, default="file")
599
+ bytes = Column(Integer, nullable=False)
600
+ created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
601
+ expires_at = Column(DateTime, nullable=True)
602
+ filename = Column(String(256), nullable=False)
603
+ purpose = Column(String(64), nullable=False)
604
+ mime_type = Column(String(255))
605
+
606
+ # ── GDPR / Lifecycle ────────────────────────────────────────────
607
+ deleted_at = Column(
608
+ Integer,
609
+ nullable=True,
610
+ default=None,
611
+ index=True,
612
+ comment="Unix timestamp of soft-deletion. Non-null = file is in Recycle Bin.",
613
+ )
614
+
615
+ user_id = Column(
616
+ String(64),
617
+ ForeignKey("users.id", ondelete="CASCADE"),
618
+ nullable=False,
619
+ )
620
+ user = relationship("User", back_populates="files")
621
+ storage_locations = relationship(
622
+ "FileStorage", back_populates="file", cascade="all, delete-orphan"
623
+ )
624
+
625
+
626
+ class FileStorage(Base):
627
+ __tablename__ = "file_storage"
628
+ id = Column(Integer, primary_key=True, autoincrement=True)
629
+
630
+ # CASCADE: storage records are deleted when their parent file is deleted.
631
+ file_id = Column(
632
+ String(64),
633
+ ForeignKey("files.id", ondelete="CASCADE"),
634
+ nullable=False,
635
+ )
636
+ storage_system = Column(
637
+ String(64),
638
+ nullable=False,
639
+ default="samba",
640
+ comment="Storage system type (samba, s3, etc.)",
641
+ )
642
+ storage_path = Column(
643
+ String(512),
644
+ nullable=True,
645
+ comment="Path to file in storage system (relative to share root)",
646
+ )
647
+ is_primary = Column(
648
+ Boolean,
649
+ default=True,
650
+ comment="Indicates if this is the primary storage location",
651
+ )
652
+ created_at = Column(
653
+ DateTime,
654
+ nullable=False,
655
+ default=datetime.utcnow,
656
+ comment="When this storage entry was created",
657
+ )
658
+ file = relationship("File", back_populates="storage_locations")
659
+ __table_args__ = (Index("idx_file_storage_file_id", "file_id"),)
660
+
661
+
662
+ class BatfishSnapshot(Base):
663
+ __tablename__ = "batfish_snapshots"
664
+
665
+ id = Column(
666
+ String(64),
667
+ primary_key=True,
668
+ index=True,
669
+ comment="Opaque snapshot ID returned to caller e.g. snap_abc123",
670
+ )
671
+
672
+ snapshot_name = Column(
673
+ String(128),
674
+ nullable=False,
675
+ comment="Caller-supplied label e.g. 'incident_001'",
676
+ )
677
+
678
+ snapshot_key = Column(
679
+ String(256),
680
+ nullable=False,
681
+ unique=True,
682
+ index=True,
683
+ comment="Namespaced isolation key: {user_id}_{id}",
684
+ )
685
+
686
+ # CASCADE: snapshots are deleted when their owning user is erased.
687
+ user_id = Column(
688
+ String(64),
689
+ ForeignKey("users.id", ondelete="CASCADE"),
690
+ nullable=False,
691
+ index=True,
692
+ )
693
+
694
+ configs_root = Column(String(512), nullable=True)
695
+
696
+ device_count = Column(Integer, default=0, nullable=False)
697
+ devices = Column(
698
+ JSON,
699
+ default=list,
700
+ nullable=False,
701
+ comment="List of hostnames ingested into this snapshot",
702
+ )
703
+
704
+ status = Column(
705
+ SAEnum(StatusEnum),
706
+ nullable=False,
707
+ default=StatusEnum.pending,
708
+ )
709
+
710
+ error_message = Column(Text, nullable=True)
711
+
712
+ created_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
713
+ updated_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
714
+ last_ingested_at = Column(BigInteger, nullable=True)
715
+
716
+ user = relationship("User", lazy="select")
717
+
718
+ __table_args__ = (
719
+ UniqueConstraint("user_id", "snapshot_name", name="uq_batfish_user_snapshot_name"),
720
+ Index("idx_batfish_user_id", "user_id"),
721
+ Index("idx_batfish_status", "status"),
722
+ )
723
+
724
+
725
+ class VectorStore(Base):
726
+ __tablename__ = "vector_stores"
727
+ id = Column(String(64), primary_key=True, index=True)
728
+ name = Column(String(128), nullable=False, unique=False)
729
+
730
+ user_id = Column(
731
+ String(64),
732
+ ForeignKey("users.id", ondelete="CASCADE"),
733
+ nullable=False,
734
+ )
735
+ collection_name = Column(String(128), nullable=False, unique=True)
736
+ vector_size = Column(Integer, nullable=False)
737
+ distance_metric = Column(String(32), nullable=False)
738
+ created_at = Column(BigInteger, default=lambda: int(datetime.now().timestamp()))
739
+ updated_at = Column(BigInteger, onupdate=lambda: int(datetime.now().timestamp()))
740
+ status = Column(SAEnum(StatusEnum), nullable=False)
741
+ config = Column(JSON, nullable=True)
742
+ file_count = Column(Integer, default=0, nullable=False)
743
+
744
+ # ── GDPR / Lifecycle ────────────────────────────────────────────────────
745
+ deleted_at = Column(
746
+ Integer,
747
+ nullable=True,
748
+ default=None,
749
+ index=True,
750
+ comment="Unix timestamp of soft-deletion. Non-null = store is in Recycle Bin.",
751
+ )
752
+
753
+ user = relationship("User", back_populates="vector_stores", lazy="select")
754
+ files = relationship(
755
+ "VectorStoreFile",
756
+ back_populates="vector_store",
757
+ cascade="all, delete-orphan",
758
+ lazy="dynamic",
759
+ )
760
+
761
+
762
+ class VectorStoreFile(Base):
763
+ __tablename__ = "vector_store_files"
764
+ id = Column(String(64), primary_key=True, index=True)
765
+
766
+ # CASCADE: vector store files are deleted when their parent store is deleted.
767
+ vector_store_id = Column(
768
+ String(64),
769
+ ForeignKey("vector_stores.id", ondelete="CASCADE"),
770
+ nullable=False,
771
+ )
772
+ file_name = Column(String(256), nullable=False)
773
+ file_path = Column(String(1024), nullable=False)
774
+ processed_at = Column(Integer, nullable=True)
775
+ status = Column(SAEnum(StatusEnum), default=StatusEnum.queued)
776
+ error_message = Column(Text, nullable=True)
777
+ meta_data = Column(JSON, nullable=True)
778
+ vector_store = relationship("VectorStore", back_populates="files")
779
+
780
+
781
+ # ---------------------------------------------------------------------------
782
+ # Training API
783
+ #
784
+ # -------------------------------------------------------------------------------
785
+
786
+ # src/api/training/models/models.py
787
+
788
+
789
+ class Dataset(Base):
790
+ __tablename__ = "datasets"
791
+
792
+ id = Column(
793
+ String(64),
794
+ primary_key=True,
795
+ index=True,
796
+ comment="Opaque dataset ID e.g. ds_abc123",
797
+ )
798
+ user_id = Column(
799
+ String(64),
800
+ ForeignKey("users.id", ondelete="CASCADE"),
801
+ nullable=False,
802
+ index=True,
803
+ )
804
+ name = Column(String(128), nullable=False)
805
+ description = Column(Text, nullable=True)
806
+ format = Column(
807
+ String(32),
808
+ nullable=False,
809
+ comment="Training format: chatml | alpaca | sharegpt | jsonl",
810
+ )
811
+
812
+ # Reference to the uploaded file in the core API files table.
813
+ # The actual Samba path is resolved via GET /v1/files/{file_id} at training time.
814
+ file_id = Column(
815
+ String(64),
816
+ nullable=False,
817
+ index=True,
818
+ comment="Reference to the file_id in the core API files table.",
819
+ )
820
+
821
+ # Populated by the training worker after it stages the file for training.
822
+ storage_path = Column(
823
+ String(512),
824
+ nullable=True,
825
+ comment="Resolved Samba path — populated by worker at training time.",
826
+ )
827
+
828
+ train_samples = Column(Integer, nullable=True)
829
+ eval_samples = Column(Integer, nullable=True)
830
+ config = Column(JSON, nullable=True)
831
+ status = Column(
832
+ SAEnum(StatusEnum),
833
+ nullable=False,
834
+ default=StatusEnum.pending,
835
+ comment="pending → processing → active → failed",
836
+ )
837
+ created_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
838
+ updated_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
839
+ deleted_at = Column(
840
+ Integer,
841
+ nullable=True,
842
+ default=None,
843
+ index=True,
844
+ comment="Unix timestamp of soft-deletion.",
845
+ )
846
+
847
+ training_jobs = relationship("TrainingJob", back_populates="dataset", lazy="dynamic")
848
+
849
+ __table_args__ = (
850
+ Index("idx_dataset_user_id", "user_id"),
851
+ Index("idx_dataset_status", "status"),
852
+ )
853
+
854
+
855
+ class TrainingJob(Base):
856
+ __tablename__ = "training_jobs"
857
+
858
+ id = Column(String(64), primary_key=True, index=True, comment="Opaque job ID e.g. tj_abc123")
859
+ user_id = Column(
860
+ String(64),
861
+ ForeignKey("users.id", ondelete="CASCADE"),
862
+ nullable=False,
863
+ index=True,
864
+ )
865
+ dataset_id = Column(
866
+ String(64),
867
+ ForeignKey("datasets.id", ondelete="SET NULL"),
868
+ nullable=True,
869
+ index=True,
870
+ comment="Source dataset. SET NULL if dataset is deleted — job record is preserved.",
871
+ )
872
+ base_model = Column(
873
+ String(256),
874
+ nullable=False,
875
+ comment="Base model identifier e.g. Qwen/Qwen2.5-7B-Instruct",
876
+ )
877
+ framework = Column(
878
+ String(32),
879
+ nullable=False,
880
+ default="axolotl",
881
+ comment="Training framework: axolotl | unsloth",
882
+ )
883
+ config = Column(
884
+ JSON,
885
+ nullable=True,
886
+ comment="Complete training configuration passed to the training container.",
887
+ )
888
+ status = Column(
889
+ SAEnum(StatusEnum),
890
+ nullable=False,
891
+ default=StatusEnum.queued,
892
+ comment="queued → in_progress → completed | failed | cancelled",
893
+ )
894
+ created_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
895
+ started_at = Column(BigInteger, nullable=True)
896
+ completed_at = Column(BigInteger, nullable=True)
897
+ failed_at = Column(BigInteger, nullable=True)
898
+ last_error = Column(Text, nullable=True)
899
+ metrics = Column(
900
+ JSON,
901
+ nullable=True,
902
+ comment="Final training metrics: loss, eval_loss, perplexity etc.",
903
+ )
904
+ output_path = Column(
905
+ String(512),
906
+ nullable=True,
907
+ comment="Samba path to the training output checkpoint.",
908
+ )
909
+
910
+ dataset = relationship("Dataset", back_populates="training_jobs", lazy="select")
911
+ fine_tuned_model = relationship(
912
+ "FineTunedModel", back_populates="training_job", uselist=False, lazy="select"
913
+ )
914
+
915
+ __table_args__ = (
916
+ Index("idx_trainingjob_user_id", "user_id"),
917
+ Index("idx_trainingjob_status", "status"),
918
+ Index("idx_trainingjob_dataset_id", "dataset_id"),
919
+ )
920
+
921
+
922
+ class FineTunedModel(Base):
923
+ __tablename__ = "fine_tuned_models"
924
+
925
+ id = Column(
926
+ String(64),
927
+ primary_key=True,
928
+ index=True,
929
+ comment="Opaque model ID e.g. ftm_abc123",
930
+ )
931
+ user_id = Column(
932
+ String(64),
933
+ ForeignKey("users.id", ondelete="CASCADE"),
934
+ nullable=False,
935
+ index=True,
936
+ )
937
+ training_job_id = Column(
938
+ String(64),
939
+ ForeignKey("training_jobs.id", ondelete="SET NULL"),
940
+ nullable=True,
941
+ index=True,
942
+ comment="Source training job. SET NULL if job is deleted — model record is preserved.",
943
+ )
944
+ name = Column(String(128), nullable=False)
945
+ description = Column(Text, nullable=True)
946
+ base_model = Column(String(256), nullable=False, comment="Base model this was fine-tuned from.")
947
+ hf_repo = Column(
948
+ String(256),
949
+ nullable=True,
950
+ comment="HuggingFace repository ID e.g. your-org/your-model",
951
+ )
952
+ storage_path = Column(String(512), nullable=True, comment="Local Samba path to model weights.")
953
+ is_active = Column(
954
+ Boolean,
955
+ default=False,
956
+ nullable=False,
957
+ comment="True when this model is currently loaded in vLLM.",
958
+ )
959
+ vllm_model_id = Column(
960
+ String(256),
961
+ nullable=True,
962
+ comment="The VLLM_MODEL value used to serve this model.",
963
+ )
964
+ status = Column(
965
+ SAEnum(StatusEnum),
966
+ nullable=False,
967
+ default=StatusEnum.processing,
968
+ comment="processing → active → failed",
969
+ )
970
+ created_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
971
+ updated_at = Column(BigInteger, default=lambda: int(time.time()), nullable=False)
972
+ deleted_at = Column(
973
+ Integer,
974
+ nullable=True,
975
+ default=None,
976
+ index=True,
977
+ comment="Unix timestamp of soft-deletion.",
978
+ )
979
+
980
+ training_job = relationship("TrainingJob", back_populates="fine_tuned_model", lazy="select")
981
+
982
+ __table_args__ = (
983
+ Index("idx_finetunedmodel_user_id", "user_id"),
984
+ Index("idx_finetunedmodel_status", "status"),
985
+ Index("idx_finetunedmodel_is_active", "is_active"),
986
+ )
File without changes
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: projectdavid-orm
3
+ Version: 1.1.5
4
+ Summary: Shared SQLAlchemy ORM models for ProjectDavid services
5
+ Author-email: "Francis N." <francis.neequaye@projectdavid.co.uk>
6
+ License: Polyform
7
+ Project-URL: Homepage, https://github.com/frankie336/projectdavid-orm
8
+ Keywords: orm,sqlalchemy,models,projectdavid,internal
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: sqlalchemy>=2.0
13
+ Requires-Dist: pymysql
14
+ Requires-Dist: pydantic>=2.0
15
+ Requires-Dist: python-dotenv
16
+ Requires-Dist: projectdavid_common==0.50.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: black; extra == "dev"
20
+ Requires-Dist: isort; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # projectdavid-orm
24
+
25
+ Shared SQLAlchemy ORM models and database utilities for the ProjectDavid platform.
26
+
27
+ Provides a single source of truth for all database models used across ProjectDavid microservices, eliminating duplication between the core API, training service, and any future services that require direct database access.
28
+
29
+ ---
30
+
31
+ ## Overview
32
+
33
+ ProjectDavid is a self-hosted, multi-provider LLM runtime API. As the platform grows into multiple microservices (inference API, training API, sandbox, etc.), each service needs access to the same database schema. Rather than duplicating model definitions or making inter-service HTTP calls just to read data, `projectdavid-orm` exposes the full SQLAlchemy model layer as a shared, versioned package.
34
+
35
+ Any service that needs direct DB access simply installs this package and imports the models it needs.
36
+
37
+ ---
38
+ ## Installation
39
+ ```bash
40
+ pip install projectdavid-orm
@@ -0,0 +1,12 @@
1
+ projectdavid_orm/__init__.py,sha256=ez1VDbs5EGh3yWSeBBPgl0v-DweoA1DjRBd2Cv0n11M,74
2
+ projectdavid_orm/ormInterface.py,sha256=_0Wo1DN4xJuHdguhIGRH_d5ODOg56LwsrSY1eSfwFR8,1009
3
+ projectdavid_orm/constants/__init__.py,sha256=i2JnKwAKnlH8A49bQErfbZHP-PtSdJMl3TMTnup9x1M,250
4
+ projectdavid_orm/projectdavid_orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ projectdavid_orm/projectdavid_orm/base.py,sha256=kha9xmklzhuQAK8QEkNBn-mAHq8dUKbOM-3abaBpWmQ,71
6
+ projectdavid_orm/projectdavid_orm/models.py,sha256=PzswjqmERzKTKBovjYVUXqq4JlRnrb9aEpEz7z6tCzI,31077
7
+ projectdavid_orm/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ projectdavid_orm-1.1.5.dist-info/licenses/LICENSE,sha256=_8yjiEGttpS284BkfhXxfERqTRZW_tUaHiBB0GTJTMg,4563
9
+ projectdavid_orm-1.1.5.dist-info/METADATA,sha256=-nzTvhB1ibAhiaX9Y_5fzS_oWlSgGYWGLnHfPqqoIVA,1635
10
+ projectdavid_orm-1.1.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ projectdavid_orm-1.1.5.dist-info/top_level.txt,sha256=HyXao33WM-_0_6GrOa1nDUSE4NjA4kMyYFhXyXZduOQ,17
12
+ projectdavid_orm-1.1.5.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,131 @@
1
+ # PolyForm Noncommercial License 1.0.0
2
+
3
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
4
+
5
+ ## Acceptance
6
+
7
+ In order to get any license under these terms, you must agree
8
+ to them as both strict obligations and conditions to all
9
+ your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the
14
+ software to do everything you might do with the software
15
+ that would otherwise infringe the licensor's copyright
16
+ in it for any permitted purpose. However, you may
17
+ only distribute the software according to [Distribution
18
+ License](#distribution-license) and make changes or new works
19
+ based on the software according to [Changes and New Works
20
+ License](#changes-and-new-works-license).
21
+
22
+ ## Distribution License
23
+
24
+ The licensor grants you an additional copyright license
25
+ to distribute copies of the software. Your license
26
+ to distribute covers distributing the software with
27
+ changes and new works permitted by [Changes and New Works
28
+ License](#changes-and-new-works-license).
29
+
30
+ ## Notices
31
+
32
+ You must ensure that anyone who gets a copy of any part of
33
+ the software from you also gets a copy of these terms or the
34
+ URL for them above, as well as copies of any plain-text lines
35
+ beginning with `Required Notice:` that the licensor provided
36
+ with the software. For example:
37
+
38
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
39
+
40
+ ## Changes and New Works License
41
+
42
+ The licensor grants you an additional copyright license to
43
+ make changes and new works based on the software for any
44
+ permitted purpose.
45
+
46
+ ## Patent License
47
+
48
+ The licensor grants you a patent license for the software that
49
+ covers patent claims the licensor can license, or becomes able
50
+ to license, that you would infringe by using the software.
51
+
52
+ ## Noncommercial Purposes
53
+
54
+ Any noncommercial purpose is a permitted purpose.
55
+
56
+ ## Personal Uses
57
+
58
+ Personal use for research, experiment, and testing for
59
+ the benefit of public knowledge, personal study, private
60
+ entertainment, hobby projects, amateur pursuits, or religious
61
+ observance, without any anticipated commercial application,
62
+ is use for a permitted purpose.
63
+
64
+ ## Noncommercial Organizations
65
+
66
+ Use by any charitable organization, educational institution,
67
+ public research organization, public safety or health
68
+ organization, environmental protection organization,
69
+ or government institution is use for a permitted purpose
70
+ regardless of the source of funding or obligations resulting
71
+ from the funding.
72
+
73
+ ## Fair Use
74
+
75
+ You may have "fair use" rights for the software under the
76
+ law. These terms do not limit them.
77
+
78
+ ## No Other Rights
79
+
80
+ These terms do not allow you to sublicense or transfer any of
81
+ your licenses to anyone else, or prevent the licensor from
82
+ granting licenses to anyone else. These terms do not imply
83
+ any other licenses.
84
+
85
+ ## Patent Defense
86
+
87
+ If you make any written claim that the software infringes or
88
+ contributes to infringement of any patent, your patent license
89
+ for the software granted under these terms ends immediately. If
90
+ your company makes such a claim, your patent license ends
91
+ immediately for work on behalf of your company.
92
+
93
+ ## Violations
94
+
95
+ The first time you are notified in writing that you have
96
+ violated any of these terms, or done anything with the software
97
+ not covered by your licenses, your licenses can nonetheless
98
+ continue if you come into full compliance with these terms,
99
+ and take practical steps to correct past violations, within
100
+ 32 days of receiving notice. Otherwise, all your licenses
101
+ end immediately.
102
+
103
+ ## No Liability
104
+
105
+ ***As far as the law allows, the software comes as is, without
106
+ any warranty or condition, and the licensor will not be liable
107
+ to you for any damages arising out of these terms or the use
108
+ or nature of the software, under any kind of legal claim.***
109
+
110
+ ## Definitions
111
+
112
+ The **licensor** is the individual or entity offering these
113
+ terms, and the **software** is the software the licensor makes
114
+ available under these terms.
115
+
116
+ **You** refers to the individual or entity agreeing to these
117
+ terms.
118
+
119
+ **Your company** is any legal entity, sole proprietorship,
120
+ or other kind of organization that you work for, plus all
121
+ organizations that have control over, are under the control of,
122
+ or are under common control with that organization. **Control**
123
+ means ownership of substantially all the assets of an entity,
124
+ or the power to direct its management and policies by vote,
125
+ contract, or otherwise. Control can be direct or indirect.
126
+
127
+ **Your licenses** are all the licenses granted to you for the
128
+ software under these terms.
129
+
130
+ **Use** means anything you do with the software requiring one
131
+ of your licenses.
@@ -0,0 +1 @@
1
+ projectdavid_orm