Compare commits

..

3 Commits

Author SHA1 Message Date
Jann Stute 27bd1622b7 refactor: remove engine; breaks migrations until individual refactor 2026-08-28 15:04:15 +02:00
Jann Stute e2b54ef3ff refactor: sqlalchemy-independence for _set_version 2026-08-28 15:04:15 +02:00
Jann Stute 70f1a3d975 refactor: sqlalchemy-independence for _get_version 2026-08-28 15:03:54 +02:00
10 changed files with 137 additions and 182 deletions
Generated
+6 -6
View File
@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1787559586,
"narHash": "sha256-onL0VLf9vPllmT0H/OlURIU5r5t5WIEl7t4tVNKT0Nw=",
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9d0d87172c374f89da73c1cfe6d81ae62feac1f1",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
@@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1787736819,
"narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=",
"lastModified": 1785571196,
"narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3",
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
"type": "github"
},
"original": {
+3 -4
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
description = "TagStudio";
@@ -49,10 +49,9 @@
inherit pillow-jxl-plugin;
};
openexr = python3Packages.callPackage ./nix/package/openexr.nix { inherit (pkgs) openexr; };
pillow-jxl-plugin = python3Packages.callPackage ./nix/package/pillow-jxl-plugin.nix {
inherit (pkgs) cmake;
inherit openexr pyexiv2;
inherit pyexiv2;
};
pyexiv2 = python3Packages.callPackage ./nix/package/pyexiv2.nix { inherit (pkgs) exiv2; };
in
@@ -60,7 +59,7 @@
inherit tagstudio;
tagstudio-jxl = tagstudio.override { withJXLSupport = true; };
inherit openexr pillow-jxl-plugin pyexiv2;
inherit pillow-jxl-plugin pyexiv2;
}
)
);
+38 -42
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
ffmpeg-headless,
@@ -40,36 +40,12 @@ python3Packages.buildPythonApplication {
qt6.qtmultimedia
];
build-system = with python3Packages; [ hatchling ];
dependencies =
with python3Packages;
[
chardet_5
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
py7zr
pydantic
pydub
pyside6
rarfile
rawpy
requests
semver
send2trash
sqlalchemy
srctools
structlog
toml
ujson
wcmatch
]
++ lib.optional (pythonAtLeast "3.13") audioop-lts
++ lib.optional withJXLSupport pillow-jxl-plugin;
nativeCheckInputs = with python3Packages; [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
# TODO: Install more icon resolutions when available.
preInstall = ''
@@ -114,19 +90,39 @@ python3Packages.buildPythonApplication {
"structlog"
"typing-extensions"
];
# HACK: All tests fail with Python: Aborted for unknown reasons.
doCheck = false;
nativeCheckInputs = with python3Packages; [
pytest-qt
pytest-xdist
pytestCheckHook
syrupy
];
pythonImportsCheck = [ "tagstudio" ];
build-system = with python3Packages; [ hatchling ];
dependencies =
with python3Packages;
[
chardet_5
ffmpeg-python
humanfriendly
mutagen
numpy
opencv-python
pillow
pillow-heif
py7zr
pydantic
pydub
pyside6
rarfile
rawpy
requests
semver
send2trash
sqlalchemy
srctools
structlog
toml
ujson
wcmatch
]
++ lib.optional (pythonAtLeast "3.13") audioop-lts
++ lib.optional withJXLSupport pillow-jxl-plugin;
# These tests require modifications to a library, which does not work
# in a read-only environment.
disabledTests = [
-38
View File
@@ -1,38 +0,0 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
{
lib,
numpy,
openexr,
pybind11,
pytest,
python,
pythonImportsCheckHook,
toPythonModule,
}:
toPythonModule (
openexr.overrideAttrs (o: {
buildInputs = o.buildInputs or [ ] ++ [ pybind11 ];
cmakeFlags = o.cmakeFlags or [ ] ++ [ (lib.cmakeBool "OPENEXR_BUILD_PYTHON" true) ];
# `python.sitePackages` replacement can be removed once Python install path is inherited from sysconfig.
# Currently on main, but not part of a release.
# See: https://github.com/AcademySoftwareFoundation/openexr/commit/30345db72944b38926f13b5114b9a01b4b553890
postPatch = o.postPatch or "" + /* bash */ ''
substituteInPlace src/wrappers/python/CMakeLists.txt \
--replace-warn python/OpenEXR ${python.sitePackages} \
--replace-fail 'PYTHONPATH=''${CMAKE_CURRENT_BINARY_DIR}' 'PYTHONPATH=''${CMAKE_CURRENT_BINARY_DIR}:'"$PYTHONPATH"
'';
nativeCheckInputs = o.nativeCheckInputs or [ ] ++ [
numpy
pythonImportsCheckHook
];
checkInputs = o.checkInputs or [ ] ++ [ pytest ];
pythonImportsCheck = o.pythonImportsCheck or [ ] ++ [ "OpenEXR" ];
})
)
+13 -15
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
buildPythonPackage,
@@ -7,7 +7,6 @@
fetchPypi,
lib,
numpy,
openexr,
packaging,
pillow,
pyexiv2,
@@ -17,18 +16,18 @@
buildPythonPackage rec {
pname = "pillow-jxl-plugin";
version = "1.3.8";
version = "1.3.4";
pyproject = true;
src = fetchPypi {
pname = "pillow_jxl_plugin";
inherit version;
hash = "sha256-RDD9d1eJl0IHnFSKfSU31tY88PHTIxgAlbwPbwPZ1Po=";
hash = "sha256-jqWJ/FWep8XfzLQq9NgUj121CPX01FGDKLq1ox/LJo4=";
};
cargoDeps = rustPlatform.fetchCargoVendor {
inherit src;
hash = "sha256-IiVTlKtKkfZnRXme7QFA5MS8PPiL8+riOYOEoNaHHXc=";
hash = "sha256-7j+sCn+P6q6tsm2MJ/cM7hF2KEjILJNA6SDb35tecPg=";
};
nativeBuildInputs = [
@@ -37,22 +36,12 @@ buildPythonPackage rec {
rustPlatform.maturinBuildHook
];
dependencies = [
packaging
pillow
];
dontUseCmakeConfigure = true;
nativeCheckInputs = [
numpy
openexr
pyexiv2
pytestCheckHook
];
pythonImportsCheck = [ "pillow_jxl" ];
# Working directory takes precedence in the Python path. Remove
# `pillow_jxl` to prevent it from being loaded during pytest, rather than the
# built module, as it includes a `pillow_jxl.pillow_jxl.so` that is imported.
@@ -62,6 +51,15 @@ buildPythonPackage rec {
rm -r pillow_jxl
'';
dontUseCmakeConfigure = true;
pythonImportsCheck = [ "pillow_jxl" ];
dependencies = [
packaging
pillow
];
meta = {
description = "Pillow plugin for JPEG-XL, using Rust for bindings";
homepage = "https://github.com/Isotr0py/pillow-jpegxl-plugin";
+8 -16
View File
@@ -1,42 +1,34 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
autoPatchelfHook,
buildPythonPackage,
exiv2,
fetchFromGitHub,
lib,
pybind11,
python,
setuptools,
}:
buildPythonPackage rec {
pname = "pyexiv2";
version = "2.16.0";
version = "2.15.3";
pyproject = true;
src = fetchFromGitHub {
owner = "LeoHsiao1";
repo = "pyexiv2";
tag = "v${version}";
hash = "sha256-FH5nbbh0vaErJzBl6L2HPh0SQXkQ558abTBml7nSLU8=";
hash = "sha256-83bFMaoXncvhRJNcCgkkC7B29wR5pjuLO/EdkQdqxxo=";
};
buildInputs = [ exiv2.dev ];
build-system = [ setuptools ];
dependencies = [ pybind11 ];
postBuild = ''
lib_dir=$out/${python.sitePackages}/pyexiv2/lib
mkdir -p "$lib_dir"
cp -rT ${exiv2.lib}/lib "$lib_dir"
'';
nativeBuildInputs = [ autoPatchelfHook ];
buildInputs = [ exiv2.lib ];
pythonImportsCheck = [ "pyexiv2" ];
build-system = [ setuptools ];
meta = {
description = "Read and write image metadata, including EXIF, IPTC, XMP, ICC Profile";
homepage = "https://github.com/LeoHsiao1/pyexiv2";
+3 -3
View File
@@ -1,5 +1,5 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
# SPDX-License-Identifier: GPL-3.0-only
{
lib,
@@ -21,7 +21,7 @@ let
stdenv.cc.cc
zstd
]
++ lib.optionals (!stdenv.hostPlatform.isDarwin) [
++ lib.optionals (!stdenv.isDarwin) [
dbus
libGL
libdrm
@@ -35,7 +35,7 @@ let
]
);
libraryPath = "${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin "DY"}LD_LIBRARY_PATH";
libraryPath = "${lib.optionalString pkgs.stdenv.isDarwin "DY"}LD_LIBRARY_PATH";
python3Wrapped = pkgs.symlinkJoin {
inherit (python3)
@@ -504,10 +504,9 @@ class Library:
) -> LibraryStatus:
logger.info("[Library] Opening SQLite Library", library_dir=library_dir)
self.engine = self.__get_engine(library_dir, in_memory, sql_filename)
# migrate if necessary
try:
migrations = DBMigrations(library_dir, self.engine)
migrations = DBMigrations(library_dir, sql_filename)
# save backup if patches will be applied
if migrations.required:
@@ -517,7 +516,8 @@ class Library:
except MigrationError as e:
return LibraryStatus(success=False, message=e.args[0])
# everything is fine, set the library path
# open up-to-date library
self.engine = self.__get_engine(library_dir, in_memory, sql_filename)
self.library_dir = library_dir
return LibraryStatus(success=True, library_path=library_dir)
@@ -2,14 +2,14 @@
# SPDX-License-Identifier: MIT
import sqlite3
from collections.abc import Callable
from pathlib import Path
from typing import override
import sqlalchemy
import structlog
import ujson
from sqlalchemy import Engine, and_, delete, select, text, update
from sqlalchemy import and_, delete, select, text, update
from sqlalchemy.orm import Session
from tagstudio.core.constants import IGNORE_NAME, TAG_ARCHIVED, TS_FOLDER_NAME
@@ -23,6 +23,7 @@ from tagstudio.core.library.alchemy.constants import (
from tagstudio.core.library.alchemy.fields import LEGACY_FIELD_MAP, DatetimeField, TextField
from tagstudio.core.library.alchemy.joins import TagParent
from tagstudio.core.library.alchemy.models import Entry, Tag, TagColorGroup, Version
from tagstudio.core.library.alchemy.utils import list_tables
from tagstudio.core.library.ignore import migrate_ext_list
from tagstudio.core.utils.types import unwrap
from tagstudio.i18n.translations import Translations
@@ -46,9 +47,11 @@ class DBMigration:
class DBMigrations:
def __init__(self, library_dir: Path, engine: Engine) -> None:
def __init__(self, library_dir: Path, sql_filename: str) -> None:
self.library_dir = library_dir
self.engine = engine
self._connection = sqlite3.connect(
str(library_dir / TS_FOLDER_NAME / sql_filename), autocommit=False
)
# Don't check DB version when creating new library
self.loaded_db_version = self._get_version(DB_VERSION_CURRENT_KEY)
@@ -102,70 +105,63 @@ class DBMigrations:
MigrationTo300, # changes: deletes folders
MigrationTo400, # changes: add category_exclusions
]
with Session(self.engine) as session:
for migration in migrations:
if self.loaded_db_version < migration.version and (
migration.initial_version is None
or self.initial_db_version < migration.initial_version
):
logger.info(f"[Library][Migration][{migration.version}] Starting DB Migration")
# any error causes transaction to rollback
migration.run(
session,
self.library_dir,
lambda msg, v=migration.version: f"[Library][Migration][{v}] {msg}",
for migration in migrations:
if self.loaded_db_version < migration.version and (
migration.initial_version is None
or self.initial_db_version < migration.initial_version
):
logger.info(f"[Library][Migration][{migration.version}] Starting DB Migration")
# any error causes transaction to rollback
migration.run(
None, # TODO: remove session param once all Migrations have been updated
self.library_dir,
lambda msg, v=migration.version: f"[Library][Migration][{v}] {msg}",
)
self.loaded_db_version = migration.version
try:
self._set_version(DB_VERSION_CURRENT_KEY, migration.version)
logger.info(f"[Library][Migration][{migration.version}] Completed DB Migration")
except Exception as e:
logger.info(
f"[Library][Migration][{migration.version}] "
"Couldn't update version, continuing without commit",
error=e,
)
self.loaded_db_version = migration.version
try:
self._set_version(session, DB_VERSION_CURRENT_KEY, migration.version)
logger.info(
f"[Library][Migration][{migration.version}] Completed DB Migration"
)
except Exception as e:
logger.info(
f"[Library][Migration][{migration.version}] "
"Couldn't update version, continuing without commit",
error=e,
)
session.flush()
else:
session.commit()
else:
self._connection.commit()
assert self.loaded_db_version >= DB_VERSION, (
"Ran all migrations, but the DB is still not on the newest version"
)
def _get_version(self, key: str) -> int:
with Session(self.engine) as session:
inspector = sqlalchemy.inspect(self.engine)
try:
# "Version" table added in DB_VERSION 101
if inspector and inspector.has_table("versions"):
version = session.scalar(select(Version).where(Version.key == key))
assert version
return version.value
# "Preferences" table deprecated in TagStudio 9.5.4
else:
return int(
unwrap(
session.scalar(
text("SELECT value FROM preferences WHERE key == 'DB_VERSION'")
)
)
)
except Exception:
return 0
"""Get a version value from the DB.
def _set_version(self, session: Session, key: str, value: int) -> None:
Args:
key(str): The name of the version type to retrieve.
"""
# "Version" table added in DB_VERSION 101
if "versions" in list_tables(self._connection):
query = ("SELECT value FROM versions WHERE key == ?", [key])
# "Preferences" table deprecated in TagStudio 9.5.4
else:
query = ("SELECT value FROM preferences WHERE key == 'DB_VERSION'", [])
return int(unwrap(self._connection.execute(*query).fetchone())[0])
def _set_version(self, key: str, value: int) -> None:
"""Set a version value to the DB.
Args:
session(Session): The SQLAlchemy DB Session to use.
key(str): The key for the name of the version type to set.
key(str): The the name of the version type to set.
value(int): The version value to set.
"""
# Insert if key has no value yet, otherwise update the value
session.merge(Version(key=key, value=value))
self._connection.execute(
"INSERT INTO versions (key, value) VALUES (?, ?)"
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
[key, value],
)
class MigrationTo7(DBMigration):
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: MIT
from sqlite3 import Connection
def list_tables(con: Connection) -> list[str]:
return [
row[0]
for row in con.execute("SELECT name FROM sqlite_master WHERE type == 'table'").fetchall()
]