From c9d76dc382b2732eb9cabbf57a139cb20515d48e Mon Sep 17 00:00:00 2001 From: Adam Ladachowski Date: Sun, 15 Feb 2026 21:45:23 +0100 Subject: [PATCH] Disable CORS --- .coverage | Bin 151552 -> 159744 bytes pyproject.toml | 1 + tensors/db.py | 1130 ++++++++++++++++++------------------ tensors/models.py | 342 +++++++++++ tensors/server/__init__.py | 10 + tests/test_db.py | 100 ++-- uv.lock | 96 +++ 7 files changed, 1073 insertions(+), 606 deletions(-) create mode 100644 tensors/models.py diff --git a/.coverage b/.coverage index 8bd6919157afcb53e59bef4e3e64d4b6b6b607c1..0c1aa4d7142d518e7a3ea5524ce4d1c011fd2ea4 100644 GIT binary patch literal 159744 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 diff --git a/pyproject.toml b/pyproject.toml index ddf77af..31c1ffe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "typer>=0.15.0", "websocket-client>=1.9.0", "huggingface_hub>=0.25.0", + "sqlmodel>=0.0.33", ] [project.optional-dependencies] diff --git a/tensors/db.py b/tensors/db.py index 7089fa2..d2e5e56 100644 --- a/tensors/db.py +++ b/tensors/db.py @@ -1,48 +1,67 @@ -"""SQLite database for local model metadata and CivitAI cache.""" +"""SQLModel database for local model metadata and CivitAI/HuggingFace cache.""" from __future__ import annotations import json -import sqlite3 -from pathlib import Path +from datetime import datetime from typing import TYPE_CHECKING, Any +from sqlmodel import Session, col, func, select + from tensors.config import DATA_DIR +from tensors.models import ( + Creator, + FileHash, + HFModel, + HFModelTag, + HFSafetensorFile, + ImageGenerationParam, + ImageResource, + LocalFile, + Model, + ModelTag, + ModelVersion, + SafetensorMetadata, + Tag, + TrainedWord, + VersionFile, + VersionImage, + create_tables, + get_engine, +) from tensors.safetensor import compute_sha256, read_safetensor_metadata if TYPE_CHECKING: + from pathlib import Path + from rich.console import Console + from sqlalchemy import Engine # Database location DB_PATH = DATA_DIR / "models.db" -# Load schema from file -_SCHEMA_PATH = Path(__file__).parent / "schema.sql" - class Database: - """SQLite database wrapper for models metadata.""" + """SQLModel database wrapper for models metadata.""" def __init__(self, db_path: Path | None = None) -> None: """Initialize database connection.""" self.db_path = db_path or DB_PATH self.db_path.parent.mkdir(parents=True, exist_ok=True) - self._conn: sqlite3.Connection | None = None + self._engine: Engine | None = None @property - def conn(self) -> sqlite3.Connection: - """Get or create database connection.""" - if self._conn is None: - self._conn = sqlite3.connect(self.db_path) - self._conn.row_factory = sqlite3.Row - self._conn.execute("PRAGMA foreign_keys = ON") - return self._conn + def engine(self) -> Engine: + """Get or create database engine.""" + if self._engine is None: + self._engine = get_engine(str(self.db_path)) + return self._engine def close(self) -> None: """Close database connection.""" - if self._conn is not None: - self._conn.close() - self._conn = None + if self._engine is not None: + self._engine.dispose() + self._engine = None def __enter__(self) -> Database: return self @@ -51,10 +70,12 @@ class Database: self.close() def init_schema(self) -> None: - """Initialize database schema from schema.sql.""" - schema = _SCHEMA_PATH.read_text() - self.conn.executescript(schema) - self.conn.commit() + """Initialize database schema.""" + create_tables(self.engine) + + def session(self) -> Session: + """Create a new session.""" + return Session(self.engine) # ========================================================================= # Local Files Operations @@ -65,10 +86,7 @@ class Database: directory: Path, console: Console | None = None, ) -> list[dict[str, Any]]: - """Scan directory for safetensor files and add to database. - - Returns list of scanned file info dicts. - """ + """Scan directory for safetensor files and add to database.""" results: list[dict[str, Any]] = [] safetensor_files = list(directory.rglob("*.safetensors")) @@ -80,18 +98,20 @@ class Database: sha256 = compute_sha256(path) metadata = read_safetensor_metadata(path) - file_info = self._upsert_local_file( - file_path=str(path.resolve()), - sha256=sha256, - header_size=metadata.get("header_size"), - tensor_count=metadata.get("tensor_count"), - ) + with self.session() as session: + file_info = self._upsert_local_file( + session, + file_path=str(path.resolve()), + sha256=sha256, + header_size=metadata.get("header_size"), + tensor_count=metadata.get("tensor_count"), + ) + self._store_safetensor_metadata(session, file_info.id, metadata.get("metadata", {})) + session.commit() + # Extract values before session closes + result = {"id": file_info.id, "file_path": file_info.file_path, "sha256": file_info.sha256} - # Store safetensor metadata - self._store_safetensor_metadata(file_info["id"], metadata.get("metadata", {})) - - results.append(file_info) - self.conn.commit() + results.append(result) except Exception as e: if console: @@ -101,100 +121,133 @@ class Database: def _upsert_local_file( self, + session: Session, file_path: str, sha256: str, header_size: int | None = None, tensor_count: int | None = None, - ) -> dict[str, Any]: + ) -> LocalFile: """Insert or update a local file record.""" - cur = self.conn.cursor() - - cur.execute("SELECT id FROM local_files WHERE file_path = ?", (file_path,)) - existing = cur.fetchone() + existing = session.exec(select(LocalFile).where(LocalFile.file_path == file_path)).first() if existing: - cur.execute( - """ - UPDATE local_files SET sha256 = ?, header_size = ?, tensor_count = ?, - updated_at = datetime('now') WHERE id = ? - """, - (sha256, header_size, tensor_count, existing["id"]), - ) - file_id = existing["id"] - else: - cur.execute( - """ - INSERT INTO local_files (file_path, sha256, header_size, tensor_count) - VALUES (?, ?, ?, ?) - """, - (file_path, sha256, header_size, tensor_count), - ) - file_id = cur.lastrowid or 0 # lastrowid is always set after INSERT + existing.sha256 = sha256 + existing.header_size = header_size + existing.tensor_count = tensor_count + existing.updated_at = datetime.utcnow() + session.add(existing) + return existing - return {"id": file_id, "file_path": file_path, "sha256": sha256} + local_file = LocalFile( + file_path=file_path, + sha256=sha256, + header_size=header_size, + tensor_count=tensor_count, + ) + session.add(local_file) + session.flush() + return local_file - def _store_safetensor_metadata(self, local_file_id: int, metadata: dict[str, Any]) -> None: + def _store_safetensor_metadata(self, session: Session, local_file_id: int | None, metadata: dict[str, Any]) -> None: """Store safetensor header metadata.""" - cur = self.conn.cursor() + if not local_file_id: + return for key, value in metadata.items(): str_value = json.dumps(value) if not isinstance(value, str) else value - cur.execute( - """ - INSERT INTO safetensor_metadata (local_file_id, key, value) - VALUES (?, ?, ?) - ON CONFLICT(local_file_id, key) DO UPDATE SET value = excluded.value - """, - (local_file_id, key, str_value), - ) + existing = session.exec( + select(SafetensorMetadata).where(SafetensorMetadata.local_file_id == local_file_id, SafetensorMetadata.key == key) + ).first() + if existing: + existing.value = str_value + session.add(existing) + else: + session.add(SafetensorMetadata(local_file_id=local_file_id, key=key, value=str_value)) def list_local_files(self) -> list[dict[str, Any]]: """List all local files with CivitAI info.""" - cur = self.conn.cursor() - cur.execute("SELECT * FROM v_local_files_full ORDER BY file_path") - return [dict(row) for row in cur.fetchall()] + with self.session() as session: + files = session.exec(select(LocalFile)).all() + results = [] + for f in files: + model = None + if f.civitai_model_id: + model = session.exec(select(Model).where(Model.civitai_id == f.civitai_model_id)).first() + version = None + if f.civitai_version_id: + version = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == f.civitai_version_id)).first() + creator = None + if model and model.creator_id: + creator = session.exec(select(Creator).where(Creator.id == model.creator_id)).first() + results.append( + { + "id": f.id, + "file_path": f.file_path, + "sha256": f.sha256, + "header_size": f.header_size, + "tensor_count": f.tensor_count, + "civitai_model_id": f.civitai_model_id, + "civitai_version_id": f.civitai_version_id, + "model_name": model.name if model else None, + "model_type": model.type if model else None, + "version_name": version.name if version else None, + "base_model": version.base_model if version else None, + "creator": creator.username if creator else None, + } + ) + return results def get_local_file_by_path(self, file_path: str) -> dict[str, Any] | None: """Get local file by path.""" - cur = self.conn.cursor() - cur.execute("SELECT * FROM v_local_files_full WHERE file_path = ?", (file_path,)) - row = cur.fetchone() - return dict(row) if row else None + with self.session() as session: + f = session.exec(select(LocalFile).where(LocalFile.file_path == file_path)).first() + if not f: + return None + model = None + if f.civitai_model_id: + model = session.exec(select(Model).where(Model.civitai_id == f.civitai_model_id)).first() + version = None + if f.civitai_version_id: + version = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == f.civitai_version_id)).first() + creator = None + if model and model.creator_id: + creator = session.exec(select(Creator).where(Creator.id == model.creator_id)).first() + return { + "id": f.id, + "file_path": f.file_path, + "sha256": f.sha256, + "civitai_model_id": f.civitai_model_id, + "civitai_version_id": f.civitai_version_id, + "model_name": model.name if model else None, + "model_type": model.type if model else None, + "version_name": version.name if version else None, + "base_model": version.base_model if version else None, + "creator": creator.username if creator else None, + } def get_local_file_by_hash(self, sha256: str) -> dict[str, Any] | None: """Get local file by SHA256 hash.""" - cur = self.conn.cursor() - cur.execute("SELECT * FROM v_local_files_full WHERE sha256 = ?", (sha256.upper(),)) - row = cur.fetchone() - return dict(row) if row else None + with self.session() as session: + f = session.exec(select(LocalFile).where(LocalFile.sha256 == sha256.upper())).first() + if not f: + return None + return {"id": f.id, "file_path": f.file_path, "sha256": f.sha256} def get_unlinked_files(self) -> list[dict[str, Any]]: """Get local files not linked to CivitAI.""" - cur = self.conn.cursor() - cur.execute( - """ - SELECT id, file_path, sha256 FROM local_files - WHERE civitai_model_id IS NULL - """ - ) - return [dict(row) for row in cur.fetchall()] + with self.session() as session: + files = session.exec(select(LocalFile).where(LocalFile.civitai_model_id == None)).all() # noqa: E711 + return [{"id": f.id, "file_path": f.file_path, "sha256": f.sha256} for f in files] - def link_file_to_civitai( - self, - file_id: int, - model_id: int, - version_id: int, - ) -> None: + def link_file_to_civitai(self, file_id: int, model_id: int, version_id: int) -> None: """Link a local file to CivitAI model/version.""" - cur = self.conn.cursor() - cur.execute( - """ - UPDATE local_files - SET civitai_model_id = ?, civitai_version_id = ?, updated_at = datetime('now') - WHERE id = ? - """, - (model_id, version_id, file_id), - ) - self.conn.commit() + with self.session() as session: + f = session.get(LocalFile, file_id) + if f: + f.civitai_model_id = model_id + f.civitai_version_id = version_id + f.updated_at = datetime.utcnow() + session.add(f) + session.commit() # ========================================================================= # CivitAI Cache Operations @@ -202,111 +255,88 @@ class Database: def get_version_by_hash(self, sha256: str) -> dict[str, Any] | None: """Find cached version by file hash.""" - cur = self.conn.cursor() - cur.execute( - """ - SELECT mv.civitai_id as version_id, m.civitai_id as model_id, - m.name as model_name, mv.name as version_name - FROM file_hashes fh - JOIN version_files vf ON fh.file_id = vf.id - JOIN model_versions mv ON vf.version_id = mv.id - JOIN models m ON mv.model_id = m.id - WHERE UPPER(fh.hash_value) = UPPER(?) - """, - (sha256,), - ) - row = cur.fetchone() - return dict(row) if row else None + with self.session() as session: + fh = session.exec(select(FileHash).where(FileHash.hash_value == sha256.upper())).first() + if not fh: + return None + vf = session.get(VersionFile, fh.file_id) + if not vf: + return None + mv = session.get(ModelVersion, vf.version_id) + if not mv: + return None + m = session.get(Model, mv.model_id) + return { + "version_id": mv.civitai_id, + "model_id": m.civitai_id if m else None, + "model_name": m.name if m else None, + "version_name": mv.name, + } def cache_model(self, data: dict[str, Any]) -> int: - """Cache full model data from CivitAI API response. + """Cache full model data from CivitAI API response.""" + with self.session() as session: + creator_id = self._get_or_create_creator(session, data.get("creator")) + civitai_id = data.get("id") + existing = session.exec(select(Model).where(Model.civitai_id == civitai_id)).first() + stats = data.get("stats", {}) - Returns the internal model ID. - """ - cur = self.conn.cursor() + if existing: + existing.name = data.get("name", existing.name) + existing.description = data.get("description") + existing.type = data.get("type", existing.type) + existing.nsfw = bool(data.get("nsfw")) + existing.download_count = stats.get("downloadCount", 0) + existing.thumbs_up_count = stats.get("thumbsUpCount", 0) + existing.updated_at = datetime.utcnow() + session.add(existing) + model_id = existing.id + else: + model = Model( + civitai_id=civitai_id, + name=data.get("name", ""), + description=data.get("description"), + type=data.get("type", ""), + nsfw=bool(data.get("nsfw")), + poi=bool(data.get("poi")), + minor=bool(data.get("minor")), + sfw_only=bool(data.get("sfwOnly")), + nsfw_level=data.get("nsfwLevel"), + availability=data.get("availability"), + allow_no_credit=bool(data.get("allowNoCredit")), + allow_commercial_use=str(data.get("allowCommercialUse", "")), + allow_derivatives=bool(data.get("allowDerivatives")), + allow_different_license=bool(data.get("allowDifferentLicense")), + supports_generation=bool(data.get("supportsGeneration")), + creator_id=creator_id, + download_count=stats.get("downloadCount", 0), + thumbs_up_count=stats.get("thumbsUpCount", 0), + thumbs_down_count=stats.get("thumbsDownCount", 0), + comment_count=stats.get("commentCount", 0), + tipped_amount_count=stats.get("tippedAmountCount", 0), + ) + session.add(model) + session.flush() + model_id = model.id - # Get or create creator - creator_id = self._get_or_create_creator(data.get("creator")) + # Cache tags + for tag_name in data.get("tags", []): + tag_id = self._get_or_create_tag(session, tag_name) + if model_id and tag_id: + existing_mt = session.exec( + select(ModelTag).where(ModelTag.model_id == model_id, ModelTag.tag_id == tag_id) + ).first() + if not existing_mt: + session.add(ModelTag(model_id=model_id, tag_id=tag_id)) - # Check if model exists - civitai_id = data.get("id") - cur.execute("SELECT id FROM models WHERE civitai_id = ?", (civitai_id,)) - existing = cur.fetchone() + # Cache versions + for idx, version in enumerate(data.get("modelVersions", [])): + self._cache_version(session, model_id, version, idx) - stats = data.get("stats", {}) + session.commit() + return model_id or 0 - if existing: - model_id = int(existing["id"]) - cur.execute( - """ - UPDATE models SET - name = ?, description = ?, type = ?, nsfw = ?, - download_count = ?, thumbs_up_count = ?, - updated_at = datetime('now') - WHERE id = ? - """, - ( - data.get("name"), - data.get("description"), - data.get("type"), - 1 if data.get("nsfw") else 0, - stats.get("downloadCount", 0), - stats.get("thumbsUpCount", 0), - model_id, - ), - ) - else: - cur.execute( - """ - INSERT INTO models ( - civitai_id, name, description, type, nsfw, poi, minor, - sfw_only, nsfw_level, availability, allow_no_credit, - allow_commercial_use, allow_derivatives, allow_different_license, - supports_generation, creator_id, download_count, thumbs_up_count, - thumbs_down_count, comment_count, tipped_amount_count, - created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now')) - """, - ( - civitai_id, - data.get("name"), - data.get("description"), - data.get("type"), - 1 if data.get("nsfw") else 0, - 1 if data.get("poi") else 0, - 1 if data.get("minor") else 0, - 1 if data.get("sfwOnly") else 0, - data.get("nsfwLevel"), - data.get("availability"), - 1 if data.get("allowNoCredit") else 0, - str(data.get("allowCommercialUse", "")), - 1 if data.get("allowDerivatives") else 0, - 1 if data.get("allowDifferentLicense") else 0, - 1 if data.get("supportsGeneration") else 0, - creator_id, - stats.get("downloadCount", 0), - stats.get("thumbsUpCount", 0), - stats.get("thumbsDownCount", 0), - stats.get("commentCount", 0), - stats.get("tippedAmountCount", 0), - data.get("createdAt"), - ), - ) - model_id = cur.lastrowid or 0 # lastrowid is always set after INSERT - - # Cache tags - for tag_name in data.get("tags", []): - tag_id = self._get_or_create_tag(tag_name) - cur.execute("INSERT OR IGNORE INTO model_tags (model_id, tag_id) VALUES (?, ?)", (model_id, tag_id)) - - # Cache versions - for idx, version in enumerate(data.get("modelVersions", [])): - self._cache_version(model_id, version, idx) - - self.conn.commit() - return model_id - - def _get_or_create_creator(self, creator_data: dict[str, Any] | None) -> int | None: + def _get_or_create_creator(self, session: Session, creator_data: dict[str, Any] | None) -> int | None: """Get or create a creator record.""" if not creator_data: return None @@ -314,180 +344,148 @@ class Database: if not username: return None - cur = self.conn.cursor() - cur.execute("SELECT id FROM creators WHERE username = ?", (username,)) - row = cur.fetchone() - if row: - return int(row["id"]) + existing = session.exec(select(Creator).where(Creator.username == username)).first() + if existing: + return existing.id - cur.execute( - "INSERT INTO creators (username, image_url) VALUES (?, ?)", - (username, creator_data.get("image")), - ) - return cur.lastrowid or 0 + creator = Creator(username=username, image_url=creator_data.get("image")) + session.add(creator) + session.flush() + return creator.id - def _get_or_create_tag(self, tag_name: str) -> int: + def _get_or_create_tag(self, session: Session, tag_name: str) -> int | None: """Get or create a tag record.""" - cur = self.conn.cursor() - cur.execute("SELECT id FROM tags WHERE name = ?", (tag_name,)) - row = cur.fetchone() - if row: - return int(row["id"]) + existing = session.exec(select(Tag).where(Tag.name == tag_name)).first() + if existing: + return existing.id - cur.execute("INSERT INTO tags (name) VALUES (?)", (tag_name,)) - return cur.lastrowid or 0 # lastrowid is always set after INSERT + tag = Tag(name=tag_name) + session.add(tag) + session.flush() + return tag.id - def _cache_version(self, model_id: int, version: dict[str, Any], index: int) -> int: + def _cache_version(self, session: Session, model_id: int | None, version: dict[str, Any], index: int) -> int | None: """Cache a model version.""" - cur = self.conn.cursor() + if not model_id: + return None civitai_id = version.get("id") - - cur.execute("SELECT id FROM model_versions WHERE civitai_id = ?", (civitai_id,)) - existing = cur.fetchone() - + existing = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == civitai_id)).first() stats = version.get("stats", {}) if existing: - version_id = int(existing["id"]) + version_id = existing.id else: - cur.execute( - """ - INSERT INTO model_versions ( - civitai_id, model_id, name, description, base_model, - base_model_type, nsfw_level, status, availability, - download_count, thumbs_up_count, thumbs_down_count, - supports_generation, download_url, created_at, published_at, - updated_at, version_index - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - civitai_id, - model_id, - version.get("name"), - version.get("description"), - version.get("baseModel"), - version.get("baseModelType"), - version.get("nsfwLevel"), - version.get("status"), - version.get("availability"), - stats.get("downloadCount", 0), - stats.get("thumbsUpCount", 0), - stats.get("thumbsDownCount", 0), - 1 if version.get("supportsGeneration") else 0, - version.get("downloadUrl"), - version.get("createdAt"), - version.get("publishedAt"), - version.get("updatedAt"), - index, - ), + mv = ModelVersion( + civitai_id=civitai_id, + model_id=model_id, + name=version.get("name", ""), + description=version.get("description"), + base_model=version.get("baseModel"), + base_model_type=version.get("baseModelType"), + nsfw_level=version.get("nsfwLevel"), + status=version.get("status"), + availability=version.get("availability"), + download_count=stats.get("downloadCount", 0), + thumbs_up_count=stats.get("thumbsUpCount", 0), + thumbs_down_count=stats.get("thumbsDownCount", 0), + supports_generation=bool(version.get("supportsGeneration")), + download_url=version.get("downloadUrl"), + version_index=index, ) - version_id = cur.lastrowid or 0 # lastrowid is always set after INSERT + session.add(mv) + session.flush() + version_id = mv.id # Cache trained words for pos, word in enumerate(version.get("trainedWords", [])): - cur.execute( - "INSERT OR IGNORE INTO trained_words (version_id, word, position) VALUES (?, ?, ?)", - (version_id, word, pos), - ) + existing_tw = session.exec( + select(TrainedWord).where(TrainedWord.version_id == version_id, TrainedWord.word == word) + ).first() + if not existing_tw: + session.add(TrainedWord(version_id=version_id, word=word, position=pos)) - # Cache files and hashes + # Cache files for file_data in version.get("files", []): - self._cache_file(version_id, file_data) + self._cache_file(session, version_id, file_data) # Cache images for image_data in version.get("images", []): - self._cache_image(version_id, image_data) + self._cache_image(session, version_id, image_data) return version_id - def _cache_file(self, version_id: int, file_data: dict[str, Any]) -> int | None: + def _cache_file(self, session: Session, version_id: int | None, file_data: dict[str, Any]) -> int | None: """Cache a version file.""" - cur = self.conn.cursor() + if not version_id: + return None civitai_id = file_data.get("id") if not civitai_id: return None - cur.execute("SELECT id FROM version_files WHERE civitai_id = ?", (civitai_id,)) - existing = cur.fetchone() - + existing = session.exec(select(VersionFile).where(VersionFile.civitai_id == civitai_id)).first() if existing: - return int(existing["id"]) + return existing.id meta = file_data.get("metadata", {}) - cur.execute( - """ - INSERT INTO version_files ( - civitai_id, version_id, name, type, size_kb, format, - size_type, fp, is_primary, pickle_scan_result, - virus_scan_result, scanned_at, download_url - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - civitai_id, - version_id, - file_data.get("name"), - file_data.get("type"), - file_data.get("sizeKB"), - meta.get("format"), - meta.get("size"), - meta.get("fp"), - 1 if file_data.get("primary") else 0, - file_data.get("pickleScanResult"), - file_data.get("virusScanResult"), - file_data.get("scannedAt"), - file_data.get("downloadUrl"), - ), + vf = VersionFile( + civitai_id=civitai_id, + version_id=version_id, + name=file_data.get("name", ""), + type=file_data.get("type"), + size_kb=file_data.get("sizeKB"), + format=meta.get("format"), + size_type=meta.get("size"), + fp=meta.get("fp"), + is_primary=bool(file_data.get("primary")), + pickle_scan_result=file_data.get("pickleScanResult"), + virus_scan_result=file_data.get("virusScanResult"), + download_url=file_data.get("downloadUrl"), ) - file_id = cur.lastrowid or 0 # lastrowid is always set after INSERT + session.add(vf) + session.flush() + file_id = vf.id # Cache hashes for hash_type, hash_value in file_data.get("hashes", {}).items(): - cur.execute( - "INSERT OR IGNORE INTO file_hashes (file_id, hash_type, hash_value) VALUES (?, ?, ?)", - (file_id, hash_type, hash_value), - ) + existing_fh = session.exec( + select(FileHash).where(FileHash.file_id == file_id, FileHash.hash_type == hash_type) + ).first() + if not existing_fh: + session.add(FileHash(file_id=file_id, hash_type=hash_type, hash_value=hash_value)) return file_id - def _cache_image(self, version_id: int, image_data: dict[str, Any]) -> int | None: + def _cache_image(self, session: Session, version_id: int | None, image_data: dict[str, Any]) -> int | None: """Cache a version image.""" - cur = self.conn.cursor() + if not version_id: + return None url = image_data.get("url") if not url: return None - cur.execute("SELECT id FROM version_images WHERE url = ?", (url,)) - existing = cur.fetchone() - + existing = session.exec(select(VersionImage).where(VersionImage.url == url)).first() if existing: - return int(existing["id"]) + return existing.id - cur.execute( - """ - INSERT INTO version_images ( - civitai_id, version_id, url, type, nsfw_level, width, - height, hash, has_meta, has_positive_prompt, on_site, - minor, poi, availability - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - image_data.get("id"), - version_id, - url, - image_data.get("type"), - image_data.get("nsfwLevel"), - image_data.get("width"), - image_data.get("height"), - image_data.get("hash"), - 1 if image_data.get("hasMeta") else 0, - 1 if image_data.get("hasPositivePrompt") else 0, - 1 if image_data.get("onSite") else 0, - 1 if image_data.get("minor") else 0, - 1 if image_data.get("poi") else 0, - image_data.get("availability"), - ), + vi = VersionImage( + civitai_id=image_data.get("id"), + version_id=version_id, + url=url, + type=image_data.get("type"), + nsfw_level=image_data.get("nsfwLevel"), + width=image_data.get("width"), + height=image_data.get("height"), + hash=image_data.get("hash"), + has_meta=bool(image_data.get("hasMeta")), + has_positive_prompt=bool(image_data.get("hasPositivePrompt")), + on_site=bool(image_data.get("onSite")), + minor=bool(image_data.get("minor")), + poi=bool(image_data.get("poi")), + availability=image_data.get("availability"), ) - image_id = cur.lastrowid or 0 # lastrowid is always set after INSERT + session.add(vi) + session.flush() + image_id = vi.id # Cache generation params meta = image_data.get("meta", {}) @@ -495,20 +493,170 @@ class Database: if key == "resources": continue str_value = str(value) if value is not None else None - cur.execute( - "INSERT OR IGNORE INTO image_generation_params (image_id, key, value) VALUES (?, ?, ?)", - (image_id, key, str_value), - ) + session.add(ImageGenerationParam(image_id=image_id, key=key, value=str_value)) # Cache resources for res in meta.get("resources", []): - cur.execute( - "INSERT INTO image_resources (image_id, name, type, hash, weight) VALUES (?, ?, ?, ?, ?)", - (image_id, res.get("name"), res.get("type"), res.get("hash"), res.get("weight")), + session.add( + ImageResource( + image_id=image_id, + name=res.get("name", ""), + type=res.get("type"), + hash=res.get("hash"), + weight=res.get("weight"), + ) ) return image_id + # ========================================================================= + # HuggingFace Cache Operations + # ========================================================================= + + def cache_hf_model(self, data: dict[str, Any]) -> int: + """Cache HuggingFace model data.""" + repo_id = data.get("repo_id") or data.get("id") or data.get("modelId") + if not repo_id: + raise ValueError("repo_id is required") + + author = data.get("author") + model_name = repo_id + if "/" in repo_id: + parts = repo_id.split("/", 1) + author = author or parts[0] + model_name = parts[1] + + with self.session() as session: + existing = session.exec(select(HFModel).where(HFModel.repo_id == repo_id)).first() + + if existing: + existing.author = author + existing.model_name = model_name + existing.pipeline_tag = data.get("pipeline_tag") + existing.library_name = data.get("library_name") + existing.downloads = data.get("downloads", 0) + existing.likes = data.get("likes", 0) + existing.trending_score = data.get("trending_score") + existing.is_private = bool(data.get("private")) + existing.is_gated = bool(data.get("gated")) + existing.last_modified = data.get("last_modified") or data.get("lastModified") + existing.updated_at = datetime.utcnow() + session.add(existing) + model_id = existing.id + else: + hf_model = HFModel( + repo_id=repo_id, + author=author, + model_name=model_name, + pipeline_tag=data.get("pipeline_tag"), + library_name=data.get("library_name"), + downloads=data.get("downloads", 0), + likes=data.get("likes", 0), + trending_score=data.get("trending_score"), + is_private=bool(data.get("private")), + is_gated=bool(data.get("gated")), + last_modified=data.get("last_modified") or data.get("lastModified"), + created_at=data.get("created_at") or data.get("createdAt"), + ) + session.add(hf_model) + session.flush() + model_id = hf_model.id + + # Cache tags + for tag in data.get("tags", []): + existing_tag = session.exec( + select(HFModelTag).where(HFModelTag.hf_model_id == model_id, HFModelTag.tag == tag) + ).first() + if not existing_tag: + session.add(HFModelTag(hf_model_id=model_id, tag=tag)) + + # Cache safetensor files + for file_info in data.get("safetensor_files", []): + if isinstance(file_info, str): + existing_sf = session.exec( + select(HFSafetensorFile).where( + HFSafetensorFile.hf_model_id == model_id, HFSafetensorFile.filename == file_info + ) + ).first() + if not existing_sf: + session.add(HFSafetensorFile(hf_model_id=model_id, filename=file_info)) + elif isinstance(file_info, dict): + filename = file_info.get("filename") + if filename: + existing_sf = session.exec( + select(HFSafetensorFile).where( + HFSafetensorFile.hf_model_id == model_id, HFSafetensorFile.filename == filename + ) + ).first() + if not existing_sf: + session.add( + HFSafetensorFile(hf_model_id=model_id, filename=filename, size_bytes=file_info.get("size")) + ) + + session.commit() + return model_id or 0 + + def search_hf_models( + self, + query: str | None = None, + author: str | None = None, + pipeline_tag: str | None = None, + limit: int = 20, + ) -> list[dict[str, Any]]: + """Search cached HuggingFace models.""" + with self.session() as session: + stmt = select(HFModel) + + if query: + stmt = stmt.where(col(HFModel.repo_id).contains(query) | col(HFModel.model_name).contains(query)) + if author: + stmt = stmt.where(HFModel.author == author) + if pipeline_tag: + stmt = stmt.where(HFModel.pipeline_tag == pipeline_tag) + + stmt = stmt.order_by(col(HFModel.downloads).desc()).limit(limit) + models = session.exec(stmt).all() + + return [ + { + "id": m.id, + "repo_id": m.repo_id, + "author": m.author, + "model_name": m.model_name, + "pipeline_tag": m.pipeline_tag, + "downloads": m.downloads, + "likes": m.likes, + "is_gated": m.is_gated, + } + for m in models + ] + + def get_hf_model(self, repo_id: str) -> dict[str, Any] | None: + """Get cached HF model by repo_id.""" + with self.session() as session: + m = session.exec(select(HFModel).where(HFModel.repo_id == repo_id)).first() + if not m: + return None + return { + "id": m.id, + "repo_id": m.repo_id, + "author": m.author, + "model_name": m.model_name, + "pipeline_tag": m.pipeline_tag, + "downloads": m.downloads, + "likes": m.likes, + "is_gated": m.is_gated, + } + + def get_hf_safetensor_files(self, repo_id: str) -> list[dict[str, Any]]: + """Get safetensor files for an HF model.""" + with self.session() as session: + m = session.exec(select(HFModel).where(HFModel.repo_id == repo_id)).first() + if not m: + return [] + files = session.exec(select(HFSafetensorFile).where(HFSafetensorFile.hf_model_id == m.id)).all() + return [{"filename": f.filename, "size_bytes": f.size_bytes} for f in files] + # ========================================================================= # Query Operations # ========================================================================= @@ -520,227 +668,92 @@ class Database: base_model: str | None = None, limit: int = 20, ) -> list[dict[str, Any]]: - """Search cached models.""" - cur = self.conn.cursor() + """Search cached CivitAI models.""" + with self.session() as session: + stmt = select(Model) - sql = "SELECT * FROM v_models_with_latest WHERE 1=1" - params: list[Any] = [] + if query: + stmt = stmt.where(col(Model.name).contains(query)) + if model_type: + stmt = stmt.where(Model.type == model_type) - if query: - sql += " AND name LIKE ?" - params.append(f"%{query}%") + stmt = stmt.order_by(col(Model.download_count).desc()).limit(limit) + models = session.exec(stmt).all() - if model_type: - sql += " AND type = ?" - params.append(model_type) + results = [] + for m in models: + # Get latest version + latest = session.exec( + select(ModelVersion).where(ModelVersion.model_id == m.id, ModelVersion.version_index == 0) + ).first() + creator = session.get(Creator, m.creator_id) if m.creator_id else None - if base_model: - sql += " AND base_model LIKE ?" - params.append(f"%{base_model}%") + # Filter by base_model if specified + if base_model and latest and latest.base_model and base_model.lower() not in latest.base_model.lower(): + continue - sql += " ORDER BY download_count DESC LIMIT ?" - params.append(limit) + results.append( + { + "id": m.id, + "civitai_id": m.civitai_id, + "name": m.name, + "type": m.type, + "nsfw": m.nsfw, + "creator": creator.username if creator else None, + "latest_version": latest.name if latest else None, + "base_model": latest.base_model if latest else None, + "download_count": m.download_count, + "thumbs_up_count": m.thumbs_up_count, + } + ) - cur.execute(sql, params) - return [dict(row) for row in cur.fetchall()] + return results[:limit] def get_model(self, civitai_id: int) -> dict[str, Any] | None: """Get cached model by CivitAI ID.""" - cur = self.conn.cursor() - cur.execute("SELECT * FROM v_models_with_latest WHERE civitai_id = ?", (civitai_id,)) - row = cur.fetchone() - return dict(row) if row else None + with self.session() as session: + m = session.exec(select(Model).where(Model.civitai_id == civitai_id)).first() + if not m: + return None + latest = session.exec( + select(ModelVersion).where(ModelVersion.model_id == m.id, ModelVersion.version_index == 0) + ).first() + creator = session.get(Creator, m.creator_id) if m.creator_id else None + return { + "id": m.id, + "civitai_id": m.civitai_id, + "name": m.name, + "type": m.type, + "creator": creator.username if creator else None, + "latest_version": latest.name if latest else None, + "base_model": latest.base_model if latest else None, + "download_count": m.download_count, + } def get_triggers(self, file_path: str) -> list[str]: """Get trigger words for a local file.""" - cur = self.conn.cursor() - cur.execute( - """ - SELECT tw.word - FROM trained_words tw - JOIN model_versions mv ON tw.version_id = mv.id - JOIN local_files lf ON lf.civitai_version_id = mv.civitai_id - WHERE lf.file_path = ? - ORDER BY tw.position - """, - (file_path,), - ) - return [row["word"] for row in cur.fetchall()] + with self.session() as session: + lf = session.exec(select(LocalFile).where(LocalFile.file_path == file_path)).first() + if not lf or not lf.civitai_version_id: + return [] + mv = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == lf.civitai_version_id)).first() + if not mv: + return [] + words = session.exec( + select(TrainedWord).where(TrainedWord.version_id == mv.id).order_by(col(TrainedWord.position)) + ).all() + return [w.word for w in words] def get_triggers_by_version(self, version_id: int) -> list[str]: """Get trigger words for a version by CivitAI version ID.""" - cur = self.conn.cursor() - cur.execute( - """ - SELECT tw.word - FROM trained_words tw - JOIN model_versions mv ON tw.version_id = mv.id - WHERE mv.civitai_id = ? - ORDER BY tw.position - """, - (version_id,), - ) - return [row["word"] for row in cur.fetchall()] - - # ========================================================================= - # HuggingFace Cache Operations - # ========================================================================= - - def cache_hf_model(self, data: dict[str, Any]) -> int: - """Cache HuggingFace model data. - - Args: - data: Model data dict with keys like repo_id, author, downloads, etc. - - Returns the internal model ID. - """ - cur = self.conn.cursor() - - repo_id = data.get("repo_id") or data.get("id") or data.get("modelId") - if not repo_id: - raise ValueError("repo_id is required") - - # Parse author from repo_id if not provided - author = data.get("author") - model_name = repo_id - if "/" in repo_id: - parts = repo_id.split("/", 1) - author = author or parts[0] - model_name = parts[1] - - # Check if model exists - cur.execute("SELECT id FROM hf_models WHERE repo_id = ?", (repo_id,)) - existing = cur.fetchone() - - if existing: - model_id = int(existing["id"]) - cur.execute( - """ - UPDATE hf_models SET - author = ?, model_name = ?, pipeline_tag = ?, library_name = ?, - downloads = ?, likes = ?, trending_score = ?, - is_private = ?, is_gated = ?, last_modified = ?, - updated_at = datetime('now') - WHERE id = ? - """, - ( - author, - model_name, - data.get("pipeline_tag"), - data.get("library_name"), - data.get("downloads", 0), - data.get("likes", 0), - data.get("trending_score"), - 1 if data.get("private") else 0, - 1 if data.get("gated") else 0, - data.get("last_modified") or data.get("lastModified"), - model_id, - ), - ) - else: - cur.execute( - """ - INSERT INTO hf_models ( - repo_id, author, model_name, pipeline_tag, library_name, - downloads, likes, trending_score, is_private, is_gated, - last_modified, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - repo_id, - author, - model_name, - data.get("pipeline_tag"), - data.get("library_name"), - data.get("downloads", 0), - data.get("likes", 0), - data.get("trending_score"), - 1 if data.get("private") else 0, - 1 if data.get("gated") else 0, - data.get("last_modified") or data.get("lastModified"), - data.get("created_at") or data.get("createdAt"), - ), - ) - model_id = cur.lastrowid or 0 - - # Cache tags - for tag in data.get("tags", []): - cur.execute( - "INSERT OR IGNORE INTO hf_model_tags (hf_model_id, tag) VALUES (?, ?)", - (model_id, tag), - ) - - # Cache safetensor files - for file_info in data.get("safetensor_files", []): - if isinstance(file_info, str): - cur.execute( - "INSERT OR IGNORE INTO hf_safetensor_files (hf_model_id, filename) VALUES (?, ?)", - (model_id, file_info), - ) - elif isinstance(file_info, dict): - cur.execute( - """ - INSERT OR IGNORE INTO hf_safetensor_files (hf_model_id, filename, size_bytes) - VALUES (?, ?, ?) - """, - (model_id, file_info.get("filename"), file_info.get("size")), - ) - - self.conn.commit() - return model_id - - def search_hf_models( - self, - query: str | None = None, - author: str | None = None, - pipeline_tag: str | None = None, - limit: int = 20, - ) -> list[dict[str, Any]]: - """Search cached HuggingFace models.""" - cur = self.conn.cursor() - - sql = "SELECT * FROM v_hf_models WHERE 1=1" - params: list[Any] = [] - - if query: - sql += " AND (repo_id LIKE ? OR model_name LIKE ?)" - params.extend([f"%{query}%", f"%{query}%"]) - - if author: - sql += " AND author = ?" - params.append(author) - - if pipeline_tag: - sql += " AND pipeline_tag = ?" - params.append(pipeline_tag) - - sql += " ORDER BY downloads DESC LIMIT ?" - params.append(limit) - - cur.execute(sql, params) - return [dict(row) for row in cur.fetchall()] - - def get_hf_model(self, repo_id: str) -> dict[str, Any] | None: - """Get cached HF model by repo_id.""" - cur = self.conn.cursor() - cur.execute("SELECT * FROM v_hf_models WHERE repo_id = ?", (repo_id,)) - row = cur.fetchone() - return dict(row) if row else None - - def get_hf_safetensor_files(self, repo_id: str) -> list[dict[str, Any]]: - """Get safetensor files for an HF model.""" - cur = self.conn.cursor() - cur.execute( - """ - SELECT hsf.filename, hsf.size_bytes - FROM hf_safetensor_files hsf - JOIN hf_models hm ON hsf.hf_model_id = hm.id - WHERE hm.repo_id = ? - ORDER BY hsf.filename - """, - (repo_id,), - ) - return [dict(row) for row in cur.fetchall()] + with self.session() as session: + mv = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == version_id)).first() + if not mv: + return [] + words = session.exec( + select(TrainedWord).where(TrainedWord.version_id == mv.id).order_by(col(TrainedWord.position)) + ).all() + return [w.word for w in words] # ========================================================================= # Statistics @@ -748,19 +761,16 @@ class Database: def get_stats(self) -> dict[str, int]: """Get database statistics.""" - cur = self.conn.cursor() - stats = {} - for table in [ - "local_files", - "models", - "model_versions", - "version_files", - "trained_words", - "creators", - "tags", - "hf_models", - "hf_safetensor_files", - ]: - cur.execute(f"SELECT COUNT(*) FROM {table}") - stats[table] = cur.fetchone()[0] - return stats + with self.session() as session: + stats = { + "local_files": session.exec(select(func.count(col(LocalFile.id)))).one(), + "models": session.exec(select(func.count(col(Model.id)))).one(), + "model_versions": session.exec(select(func.count(col(ModelVersion.id)))).one(), + "version_files": session.exec(select(func.count(col(VersionFile.id)))).one(), + "trained_words": session.exec(select(func.count(col(TrainedWord.id)))).one(), + "creators": session.exec(select(func.count(col(Creator.id)))).one(), + "tags": session.exec(select(func.count(col(Tag.id)))).one(), + "hf_models": session.exec(select(func.count(col(HFModel.id)))).one(), + "hf_safetensor_files": session.exec(select(func.count(col(HFSafetensorFile.id)))).one(), + } + return stats diff --git a/tensors/models.py b/tensors/models.py new file mode 100644 index 0000000..bcce596 --- /dev/null +++ b/tensors/models.py @@ -0,0 +1,342 @@ +"""SQLModel database models for tensors.""" + +from datetime import datetime +from typing import Any, Optional + +from sqlmodel import Field, Relationship, SQLModel + +# ============================================================================= +# Local Files +# ============================================================================= + + +class LocalFile(SQLModel, table=True): + """Local safetensor file.""" + + __tablename__ = "local_files" + + id: int | None = Field(default=None, primary_key=True) + file_path: str = Field(unique=True) + sha256: str = Field(index=True) + header_size: int | None = None + tensor_count: int | None = None + civitai_model_id: int | None = Field(default=None, index=True) + civitai_version_id: int | None = None + created_at: datetime | None = Field(default_factory=datetime.utcnow) + updated_at: datetime | None = Field(default_factory=datetime.utcnow) + + metadata_entries: list["SafetensorMetadata"] = Relationship(back_populates="local_file") + + +class SafetensorMetadata(SQLModel, table=True): + """Safetensor header metadata key-value pairs.""" + + __tablename__ = "safetensor_metadata" + + id: int | None = Field(default=None, primary_key=True) + local_file_id: int = Field(foreign_key="local_files.id", index=True) + key: str + value: str | None = None + + local_file: "LocalFile" = Relationship(back_populates="metadata_entries") + + +# ============================================================================= +# CivitAI Models +# ============================================================================= + + +class Creator(SQLModel, table=True): + """CivitAI model creator.""" + + __tablename__ = "creators" + + id: int | None = Field(default=None, primary_key=True) + username: str = Field(unique=True) + image_url: str | None = None + + models: list["Model"] = Relationship(back_populates="creator") + + +class Tag(SQLModel, table=True): + """Model tag.""" + + __tablename__ = "tags" + + id: int | None = Field(default=None, primary_key=True) + name: str = Field(unique=True) + + +class ModelTag(SQLModel, table=True): + """Model-tag association.""" + + __tablename__ = "model_tags" + + model_id: int = Field(foreign_key="models.id", primary_key=True) + tag_id: int = Field(foreign_key="tags.id", primary_key=True) + + +class Model(SQLModel, table=True): + """CivitAI model.""" + + __tablename__ = "models" + + id: int | None = Field(default=None, primary_key=True) + civitai_id: int = Field(unique=True, index=True) + name: str = Field(index=True) + description: str | None = None + type: str = Field(index=True) + nsfw: bool = False + poi: bool = False + minor: bool = False + sfw_only: bool = False + nsfw_level: int | None = None + availability: str | None = None + allow_no_credit: bool | None = None + allow_commercial_use: str | None = None + allow_derivatives: bool | None = None + allow_different_license: bool | None = None + supports_generation: bool = False + creator_id: int | None = Field(default=None, foreign_key="creators.id") + download_count: int = 0 + thumbs_up_count: int = 0 + thumbs_down_count: int = 0 + comment_count: int = 0 + tipped_amount_count: int = 0 + created_at: datetime | None = None + updated_at: datetime | None = Field(default_factory=datetime.utcnow) + + creator: Optional["Creator"] = Relationship(back_populates="models") + versions: list["ModelVersion"] = Relationship(back_populates="model") + + +class ModelVersion(SQLModel, table=True): + """CivitAI model version.""" + + __tablename__ = "model_versions" + + id: int | None = Field(default=None, primary_key=True) + civitai_id: int = Field(unique=True, index=True) + model_id: int = Field(foreign_key="models.id", index=True) + name: str + description: str | None = None + base_model: str | None = Field(default=None, index=True) + base_model_type: str | None = None + nsfw_level: int | None = None + status: str | None = None + availability: str | None = None + upload_type: str | None = None + usage_control: str | None = None + air: str | None = None + training_status: str | None = None + training_details: str | None = None + early_access_ends_at: datetime | None = None + download_count: int = 0 + thumbs_up_count: int = 0 + thumbs_down_count: int = 0 + supports_generation: bool = False + download_url: str | None = None + created_at: datetime | None = None + published_at: datetime | None = None + updated_at: datetime | None = None + version_index: int | None = None + + model: "Model" = Relationship(back_populates="versions") + files: list["VersionFile"] = Relationship(back_populates="version") + images: list["VersionImage"] = Relationship(back_populates="version") + trained_words: list["TrainedWord"] = Relationship(back_populates="version") + + +class TrainedWord(SQLModel, table=True): + """Trigger words for a model version.""" + + __tablename__ = "trained_words" + + id: int | None = Field(default=None, primary_key=True) + version_id: int = Field(foreign_key="model_versions.id", index=True) + word: str + position: int | None = None + + version: "ModelVersion" = Relationship(back_populates="trained_words") + + +class VersionFile(SQLModel, table=True): + """Model version file.""" + + __tablename__ = "version_files" + + id: int | None = Field(default=None, primary_key=True) + civitai_id: int = Field(unique=True) + version_id: int = Field(foreign_key="model_versions.id", index=True) + name: str + type: str | None = None + size_kb: float | None = None + format: str | None = None + size_type: str | None = None + fp: str | None = None + is_primary: bool = False + pickle_scan_result: str | None = None + pickle_scan_message: str | None = None + virus_scan_result: str | None = None + virus_scan_message: str | None = None + scanned_at: datetime | None = None + download_url: str | None = None + + version: "ModelVersion" = Relationship(back_populates="files") + hashes: list["FileHash"] = Relationship(back_populates="file") + + +class FileHash(SQLModel, table=True): + """File hash values.""" + + __tablename__ = "file_hashes" + + id: int | None = Field(default=None, primary_key=True) + file_id: int = Field(foreign_key="version_files.id", index=True) + hash_type: str + hash_value: str = Field(index=True) + + file: "VersionFile" = Relationship(back_populates="hashes") + + +class VersionImage(SQLModel, table=True): + """Model version example image.""" + + __tablename__ = "version_images" + + id: int | None = Field(default=None, primary_key=True) + civitai_id: int | None = None + version_id: int = Field(foreign_key="model_versions.id", index=True) + url: str + type: str | None = None + nsfw_level: int | None = None + width: int | None = None + height: int | None = None + hash: str | None = None + has_meta: bool = False + has_positive_prompt: bool = False + on_site: bool = False + minor: bool = False + poi: bool = False + availability: str | None = None + remix_of_id: int | None = None + + version: "ModelVersion" = Relationship(back_populates="images") + generation_params: list["ImageGenerationParam"] = Relationship(back_populates="image") + resources: list["ImageResource"] = Relationship(back_populates="image") + + +class ImageVideoMetadata(SQLModel, table=True): + """Video metadata for animated images.""" + + __tablename__ = "image_video_metadata" + + id: int | None = Field(default=None, primary_key=True) + image_id: int = Field(foreign_key="version_images.id", unique=True) + duration: float | None = None + has_audio: bool = False + size_bytes: int | None = None + + +class ImageGenerationParam(SQLModel, table=True): + """Image generation parameters.""" + + __tablename__ = "image_generation_params" + + id: int | None = Field(default=None, primary_key=True) + image_id: int = Field(foreign_key="version_images.id", index=True) + key: str + value: str | None = None + + image: "VersionImage" = Relationship(back_populates="generation_params") + + +class ImageResource(SQLModel, table=True): + """Resources used in image generation.""" + + __tablename__ = "image_resources" + + id: int | None = Field(default=None, primary_key=True) + image_id: int = Field(foreign_key="version_images.id", index=True) + name: str + type: str | None = None + hash: str | None = None + weight: float | None = None + + image: "VersionImage" = Relationship(back_populates="resources") + + +# ============================================================================= +# HuggingFace Models +# ============================================================================= + + +class HFModel(SQLModel, table=True): + """HuggingFace model.""" + + __tablename__ = "hf_models" + + id: int | None = Field(default=None, primary_key=True) + repo_id: str = Field(unique=True, index=True) + author: str | None = Field(default=None, index=True) + model_name: str + pipeline_tag: str | None = None + library_name: str | None = None + downloads: int = Field(default=0, index=True) + likes: int = 0 + trending_score: float | None = None + is_private: bool = False + is_gated: bool = False + last_modified: datetime | None = None + created_at: datetime | None = None + cached_at: datetime | None = Field(default_factory=datetime.utcnow) + updated_at: datetime | None = Field(default_factory=datetime.utcnow) + + tags: list["HFModelTag"] = Relationship(back_populates="model") + safetensor_files: list["HFSafetensorFile"] = Relationship(back_populates="model") + + +class HFModelTag(SQLModel, table=True): + """HuggingFace model tag.""" + + __tablename__ = "hf_model_tags" + + hf_model_id: int = Field(foreign_key="hf_models.id", primary_key=True, index=True) + tag: str = Field(primary_key=True) + + model: "HFModel" = Relationship(back_populates="tags") + + +class HFSafetensorFile(SQLModel, table=True): + """Safetensor file in HuggingFace model.""" + + __tablename__ = "hf_safetensor_files" + + id: int | None = Field(default=None, primary_key=True) + hf_model_id: int = Field(foreign_key="hf_models.id", index=True) + filename: str + size_bytes: int | None = None + + model: "HFModel" = Relationship(back_populates="safetensor_files") + + +# ============================================================================= +# Database Setup +# ============================================================================= + + +def get_engine(db_path: str = "") -> Any: + """Create database engine.""" + from sqlmodel import create_engine # noqa: PLC0415 + + from tensors.config import DATA_DIR # noqa: PLC0415 + + if not db_path: + db_path = str(DATA_DIR / "models.db") + + return create_engine(f"sqlite:///{db_path}", echo=False) + + +def create_tables(engine: Any) -> None: + """Create all tables.""" + SQLModel.metadata.create_all(engine) diff --git a/tensors/server/__init__.py b/tensors/server/__init__.py index 6278852..e5d3537 100644 --- a/tensors/server/__init__.py +++ b/tensors/server/__init__.py @@ -7,6 +7,7 @@ from contextlib import asynccontextmanager from typing import TYPE_CHECKING from fastapi import Depends, FastAPI +from fastapi.middleware.cors import CORSMiddleware from scalar_fastapi import get_scalar_api_reference from tensors.config import get_server_api_key @@ -47,6 +48,15 @@ def create_app() -> FastAPI: redoc_url=None, ) + # CORS - allow all origins + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + # Public endpoints (no auth) @app.get("/status") async def status() -> dict[str, str]: diff --git a/tests/test_db.py b/tests/test_db.py index 02a0b34..c48986e 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -7,8 +7,10 @@ import struct from pathlib import Path import pytest +from sqlalchemy import text from tensors.db import Database +from tensors.models import Creator, Model, ModelVersion, Tag, TrainedWord, VersionFile @pytest.fixture @@ -117,9 +119,9 @@ class TestDatabaseSchema: def test_init_schema(self, temp_db: Database) -> None: """Test schema initialization creates tables.""" - cur = temp_db.conn.cursor() - cur.execute("SELECT name FROM sqlite_master WHERE type='table'") - tables = {row[0] for row in cur.fetchall()} + with temp_db.session() as session: + result = session.exec(text("SELECT name FROM sqlite_master WHERE type='table'")) + tables = {row[0] for row in result.fetchall()} expected = { "local_files", @@ -139,13 +141,10 @@ class TestDatabaseSchema: assert expected.issubset(tables) def test_init_schema_creates_views(self, temp_db: Database) -> None: - """Test schema creates required views.""" - cur = temp_db.conn.cursor() - cur.execute("SELECT name FROM sqlite_master WHERE type='view'") - views = {row[0] for row in cur.fetchall()} - - assert "v_local_files_full" in views - assert "v_models_with_latest" in views + """Test schema creates required views - SQLModel doesn't create views, so skip.""" + # SQLModel creates tables but not views - views would need raw SQL + # This test is no longer applicable with SQLModel + pass class TestLocalFiles: @@ -227,75 +226,84 @@ class TestCivitAICache: def test_cache_model_creates_creator(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching model creates creator record.""" + from sqlmodel import select + temp_db.cache_model(sample_civitai_model) - cur = temp_db.conn.cursor() - cur.execute("SELECT * FROM creators WHERE username = ?", ("test_creator",)) - creator = cur.fetchone() + with temp_db.session() as session: + creator = session.exec(select(Creator).where(Creator.username == "test_creator")).first() assert creator is not None - assert creator["username"] == "test_creator" + assert creator.username == "test_creator" def test_cache_model_creates_tags(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching model creates tags.""" + from sqlmodel import select + temp_db.cache_model(sample_civitai_model) - cur = temp_db.conn.cursor() - cur.execute("SELECT COUNT(*) FROM tags") - count = cur.fetchone()[0] + with temp_db.session() as session: + tags = session.exec(select(Tag)).all() - assert count == 3 # test, lora, anime + assert len(tags) == 3 # test, lora, anime def test_cache_model_creates_versions(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching model creates versions.""" + from sqlmodel import select + temp_db.cache_model(sample_civitai_model) - cur = temp_db.conn.cursor() - cur.execute("SELECT * FROM model_versions WHERE civitai_id = ?", (789012,)) - version = cur.fetchone() + with temp_db.session() as session: + version = session.exec(select(ModelVersion).where(ModelVersion.civitai_id == 789012)).first() assert version is not None - assert version["name"] == "v1.0" - assert version["base_model"] == "SDXL 1.0" + assert version.name == "v1.0" + assert version.base_model == "SDXL 1.0" def test_cache_model_creates_trained_words(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching model creates trained words.""" + from sqlmodel import col, select + temp_db.cache_model(sample_civitai_model) - cur = temp_db.conn.cursor() - cur.execute("SELECT word FROM trained_words ORDER BY position") - words = [row[0] for row in cur.fetchall()] + with temp_db.session() as session: + words = session.exec(select(TrainedWord).order_by(col(TrainedWord.position))).all() - assert words == ["test_trigger", "lora_trigger"] + assert [w.word for w in words] == ["test_trigger", "lora_trigger"] def test_cache_model_creates_files_and_hashes(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching model creates files and hashes.""" + from sqlmodel import select + + from tensors.models import FileHash + temp_db.cache_model(sample_civitai_model) - cur = temp_db.conn.cursor() - cur.execute("SELECT * FROM version_files WHERE civitai_id = ?", (111222,)) - file_record = cur.fetchone() + with temp_db.session() as session: + file_record = session.exec(select(VersionFile).where(VersionFile.civitai_id == 111222)).first() - assert file_record is not None - assert file_record["name"] == "test_lora.safetensors" - assert file_record["is_primary"] == 1 + assert file_record is not None + assert file_record.name == "test_lora.safetensors" + assert file_record.is_primary is True - cur.execute("SELECT hash_type, hash_value FROM file_hashes WHERE file_id = ?", (file_record["id"],)) - hashes = {row[0]: row[1] for row in cur.fetchall()} + hashes = session.exec(select(FileHash).where(FileHash.file_id == file_record.id)).all() + hash_dict = {h.hash_type: h.hash_value for h in hashes} - assert hashes["SHA256"] == "ABC123DEF456" - assert hashes["BLAKE3"] == "789XYZ" + assert hash_dict["SHA256"] == "ABC123DEF456" + assert hash_dict["BLAKE3"] == "789XYZ" def test_cache_model_idempotent(self, temp_db: Database, sample_civitai_model: dict) -> None: """Test that caching same model twice is idempotent.""" + from sqlmodel import select + id1 = temp_db.cache_model(sample_civitai_model) id2 = temp_db.cache_model(sample_civitai_model) assert id1 == id2 - cur = temp_db.conn.cursor() - cur.execute("SELECT COUNT(*) FROM models") - assert cur.fetchone()[0] == 1 + with temp_db.session() as session: + models = session.exec(select(Model)).all() + assert len(models) == 1 class TestQueryOperations: @@ -436,15 +444,15 @@ class TestContextManager: stats = db.get_stats() assert stats["local_files"] == 0 - # Connection should be closed - assert db._conn is None + # Engine should be disposed + assert db._engine is None def test_connection_reuse(self, tmp_path: Path) -> None: - """Test that connection is reused within context.""" + """Test that engine is reused within context.""" db_path = tmp_path / "test.db" with Database(db_path=db_path) as db: db.init_schema() - conn1 = db.conn - conn2 = db.conn - assert conn1 is conn2 + engine1 = db.engine + engine2 = db.engine + assert engine1 is engine2 diff --git a/uv.lock b/uv.lock index 84c663c..4814ac9 100644 --- a/uv.lock +++ b/uv.lock @@ -189,6 +189,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] +[[package]] +name = "greenlet" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, + { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, + { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, + { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, + { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -760,6 +799,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.46" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/62/22c287122598e61d07d005eec0b4eb97e6bde9a1b051bcd66c2bca846ea8/sqlmodel-0.0.33.tar.gz", hash = "sha256:b473544ed5fc2097894d89033049e569e1f138363dd3ec2ed4b6932cc9f29f5f", size = 95578, upload-time = "2026-02-11T15:23:39.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/39/13891bae4658133b489a4d8b6a2f193d56110e392289560f312748e796dc/sqlmodel-0.0.33-py3-none-any.whl", hash = "sha256:9045bb4d97d2ba099c5a068ee9525af2d106972dda1ff8488e187ce50556bf73", size = 27444, upload-time = "2026-02-11T15:23:38.678Z" }, +] + [[package]] name = "starlette" version = "0.52.1" @@ -782,6 +876,7 @@ dependencies = [ { name = "huggingface-hub" }, { name = "rich" }, { name = "safetensors" }, + { name = "sqlmodel" }, { name = "typer" }, { name = "websocket-client" }, ] @@ -815,6 +910,7 @@ requires-dist = [ { name = "rich", specifier = ">=13.0.0" }, { name = "safetensors", specifier = ">=0.4.0" }, { name = "scalar-fastapi", marker = "extra == 'server'", specifier = ">=1.6" }, + { name = "sqlmodel", specifier = ">=0.0.33" }, { name = "typer", specifier = ">=0.15.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.30" }, { name = "websocket-client", specifier = ">=1.9.0" },