From 69dce93af5eaabe1e88ec4a8ca741bc86758fdbc Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Sat, 11 Jul 2026 23:41:39 +0200 Subject: [PATCH 1/5] refactor: remove dead folders table --- .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/library.py | 68 ++++++++++--------- src/tagstudio/core/library/alchemy/models.py | 14 ---- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index 845de2a87..b2d42ebca 100644 --- a/src/tagstudio/core/library/alchemy/constants.py +++ b/src/tagstudio/core/library/alchemy/constants.py @@ -9,7 +9,7 @@ DB_VERSION_CURRENT_KEY: str = "CURRENT" DB_VERSION_INITIAL_KEY: str = "INITIAL" -DB_VERSION: int = 202 +DB_VERSION: int = 203 TAG_CHILDREN_QUERY = text(""" WITH RECURSIVE ChildTags AS ( diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index ea9f20595..93feb4877 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -18,7 +18,6 @@ from os import makedirs from pathlib import Path from typing import TYPE_CHECKING -from uuid import uuid4 import sqlalchemy import structlog @@ -95,7 +94,6 @@ from tagstudio.core.library.alchemy.joins import TagEntry, TagParent from tagstudio.core.library.alchemy.models import ( Entry, - Folder, Namespace, Tag, TagAlias, @@ -234,7 +232,6 @@ class Library: library_dir: Path | None = None engine: Engine | None = None - folder: Folder | None = None included_files: set[Path] = set() def __init__(self) -> None: @@ -259,7 +256,6 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary): """Migrate JSON library data to the SQLite database.""" logger.info("Starting Library Conversion...") start_time = time.time() - folder: Folder = Folder(path=self.library_dir, uuid=str(uuid4())) # Tags for tag in json_lib.tags: @@ -312,7 +308,6 @@ def migrate_json_to_sqlite(self, json_lib: JsonLibrary): [ Entry( path=entry.path / entry.filename, - folder=folder, fields=[], id=entry.id + 1, # NOTE: JSON IDs start at 0 instead of 1 date_added=datetime.now(), @@ -479,16 +474,6 @@ def create_sqlite_library( session.add(Version(key=DB_VERSION_CURRENT_KEY, value=DB_VERSION)) session.flush() - # add folder for current path - folder = Folder( - path=library_dir, - uuid=str(uuid4()), - ) - session.add(folder) - session.expunge(folder) - session.flush() - self.folder = folder - # Generate default .ts_ignore file try: ts_ignore_template = ( @@ -584,6 +569,7 @@ def open_sqlite_library( (self.__apply_db200_migration, 200, None), # changes: field tables (self.__apply_db201_migration, 201, 200), # changes: field tables (self.__apply_db202_migration, 202, None), # changes: tag_parents + (self.__apply_db203_migration, 203, None), # changes: deletes folders ] for migration, v, iv in migrations: if loaded_db_version < v and (iv is None or initial_db_version < iv): @@ -601,22 +587,6 @@ def open_sqlite_library( ) logger.info(f"[Library] Library migrated to DB version {DB_VERSION}") - with Session(self.engine) as session: - # TODO: the folder logic has no use and was never finished, remove it - # check if folder matching current path exists already - # NOTE: this has been causing new Folders to be created when the library is moved, since - # its introduction - self.folder = session.scalar(select(Folder).where(Folder.path == library_dir)) - if not self.folder: - folder = Folder( - path=library_dir, - uuid=str(uuid4()), - ) - session.add(folder) - session.expunge(folder) - session.commit() - self.folder = folder - # everything is fine, set the library path self.library_dir = library_dir return LibraryStatus(success=True, library_path=library_dir) @@ -906,6 +876,42 @@ def __apply_db202_migration(self, session: Session, library_dir: Path): session.flush() logger.info("[Library][Migration][202] Verified TagParent table data") + def __apply_db203_migration(self, session: Session, library_dir: Path): + ## remove folder_id column from entries table + # create new table in the desired scheme (without folder_id column) + session.execute( + text(""" + CREATE TABLE entries_new ( + id INTEGER NOT NULL, + path VARCHAR NOT NULL, + suffix VARCHAR NOT NULL, + date_created DATETIME, + date_modified DATETIME, + date_added DATETIME, filename TEXT NOT NULL DEFAULT '', + PRIMARY KEY (id), + UNIQUE (path) + ) + """) + ) + session.flush() + # transfer data to new table + session.execute( + text(""" + INSERT INTO entries_new (id, path, suffix, date_created, date_modified, date_added) + SELECT id, path, suffix, date_created, date_modified, date_added + FROM entries + """) + ) + # delete old table + session.execute(text("DROP TABLE entries")) + # rename new table to old table + session.execute(text("ALTER TABLE entries_new RENAME TO entries")) + session.flush() + + ## drop table "folders" + session.execute(text("DROP TABLE folders")) + session.flush() + @property def field_templates(self) -> Sequence[BaseFieldTemplate]: with Session(self.engine) as session: diff --git a/src/tagstudio/core/library/alchemy/models.py b/src/tagstudio/core/library/alchemy/models.py index 668c80f5c..0b0a31c22 100644 --- a/src/tagstudio/core/library/alchemy/models.py +++ b/src/tagstudio/core/library/alchemy/models.py @@ -182,23 +182,11 @@ def __ge__(self, other: "Tag") -> bool: return self.name >= other.name -class Folder(Base): - __tablename__ = "folders" - - # TODO - implement this - id: Mapped[int] = mapped_column(primary_key=True) - path: Mapped[Path] = mapped_column(PathType, unique=True) - uuid: Mapped[str] = mapped_column(unique=True) - - class Entry(Base): __tablename__ = "entries" id: Mapped[int] = mapped_column(primary_key=True) - folder_id: Mapped[int] = mapped_column(ForeignKey("folders.id")) - folder: Mapped[Folder] = relationship("Folder") - path: Mapped[Path] = mapped_column(PathType, unique=True) filename: Mapped[str] = mapped_column() suffix: Mapped[str] = mapped_column() @@ -235,7 +223,6 @@ def is_archived(self) -> bool: def __init__( self, path: Path, - folder: Folder, fields: list[BaseField], id: int | None = None, date_created: dt | None = None, @@ -244,7 +231,6 @@ def __init__( ) -> None: super().__init__() self.path = path - self.folder = folder self.id = id # pyright: ignore[reportAttributeAccessIssue] self.filename = path.name self.suffix = path.suffix.lstrip(".").lower() From ab3b1f2f1720e3f4282b40195a0d841bca4a9c9b Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Sun, 12 Jul 2026 13:11:44 +0200 Subject: [PATCH 2/5] fix: remove folder param from all uses of Entry constructor --- src/tagstudio/core/library/refresh.py | 2 -- tests/conftest.py | 8 -------- .../.TagStudio/ts_library.sqlite | Bin 114688 -> 122880 bytes tests/macros/test_dupe_files.py | 4 ---- tests/test_library.py | 11 +---------- 5 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/tagstudio/core/library/refresh.py b/src/tagstudio/core/library/refresh.py index 824cae527..e0267b295 100644 --- a/src/tagstudio/core/library/refresh.py +++ b/src/tagstudio/core/library/refresh.py @@ -16,7 +16,6 @@ from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, Ignore, ignore_to_glob from tagstudio.core.utils.silent_subprocess import silent_run # pyright: ignore -from tagstudio.core.utils.types import unwrap logger = structlog.get_logger(__name__) @@ -41,7 +40,6 @@ def save_new_files(self) -> Iterator[int]: entries = [ Entry( path=entry_path, - folder=unwrap(self.library.folder), fields=[], date_added=dt.now(), ) diff --git a/tests/conftest.py b/tests/conftest.py index 28b1d5777..6a7a30706 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,6 @@ from tagstudio.core.constants import THUMB_CACHE_NAME, TS_FOLDER_NAME from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry, Tag -from tagstudio.core.utils.types import unwrap from tagstudio.qt.thumb_grid_layout import ThumbGridLayout from tagstudio.qt.ts_qt import QtDriver @@ -36,22 +35,18 @@ def file_mediatypes_library(): status = lib.open_library(Path(""), in_memory=True) assert status.success - folder = unwrap(lib.folder) entry1 = Entry( - folder=folder, path=Path("foo.png"), fields=[TextField(name="Title", value="I'm a Test Title")], ) entry2 = Entry( - folder=folder, path=Path("bar.png"), fields=[TextField(name="Title", value="I'm a Test Title")], ) entry3 = Entry( - folder=folder, path=Path("baz.apng"), fields=[TextField(name="Title", value="I'm a Test Title")], ) @@ -87,7 +82,6 @@ def library(request, library_dir: Path): # pyright: ignore lib = Library() status = lib.open_library(library_path, in_memory=True) assert status.success - folder = unwrap(lib.folder) tag = Tag( name="foo", @@ -116,7 +110,6 @@ def library(request, library_dir: Path): # pyright: ignore # default item with deterministic name entry = Entry( id=1, - folder=folder, path=Path("foo.txt"), fields=[TextField(name="Title", value="I'm a Test Title")], ) @@ -124,7 +117,6 @@ def library(request, library_dir: Path): # pyright: ignore entry2 = Entry( id=2, - folder=folder, path=Path("one/two/bar.md"), fields=[TextField(name="Title", value="I'm a Test Title")], ) diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index c8489518ac94c6381e9cde53fc96c7e3b9602f30..2c7c2f0ef07bdec9f3fcee03159a72210b2f319c 100644 GIT binary patch delta 1405 zcmaKpS!^3c7{_Ov%^tJswKr{Z_(;ZSn>dgLstVDEa=48PuI-vcwjdyA-R`znBq1qA zz*8;6^+Jg|NN!a`0|G<^C7~-~B8r5Nk~(o3KOhi-2G`;NDghDdLpcg%>~$8K2X^<{ z(fq#ucg>_yTqEkqkdbc&9ciC4^ZHqMyXCg^ znaQ&dXMgzsHbKiak=$u>>0e1PNPB>a?y*~;zT?Ks1k7izz5-=?xAX-N*;ybBN**ze zW`rQ$z@6f5VrS)7WsNB^k1$&rMl7Fay_4_~L*qY!+1YoXgDvMfPWQ*4WlJZyRqWl~ z{%pd-wK$#d`JQC_z_6x`CXXbvC#s8k*P!1Q_2Z~-XULB`tF#k)QD)){?8p?~g)%c1 zPd=I{(k=O0Y|Z@j2DBbk<4JAb!6O=7RNPN9L<3=eFTyoXMKbcZnmCmBe@$FfYX$gF zVptoEk7zjRe;|tYMySO?A>8lZ?Tdw?xVyU--Ca`$-!~Wt`v!+_&_9H|3ALxbjo9A6 zK+KQ5fbBfJ>Mr>k`IsEAUboIz6IQ2mQF=kTOZ-hdC#qsI zDxehFF8m~%76t?huk$Luft%-^;`%v-EzuJ{$=dq5c+jy90H0MG8B0DxR_8oTTQ~Dy zbA>%Qnv5Sltf^((wsq5gabNQ3B>jjo=Ro0&Jb1VgL{jrE^21yY3U}6_)89(uu|+ou zdup(kd^%4Nu0byH`omkb+A3o#d)gkU&n*sza{f40=YWZMkbdTlMAR1A1 zDRr**@`%=`y6knX$P!C$&0R?tvY9Oyb0}t1NUn2DEh%QLpw&D+CFdgNzeD7fi?ng6 zrg5iPBPiCX#UdgrRK1JkLBJSuJm(5y=$V1*VQJQu_U}8wDD8 k5HsS_#ZBan0!5Y@3pkHCcuSAc?SjH{8%@53#2UwqJK0~@Br6%N8s0Iq=~vQL>9W4cy_sXU`udFwvrB$= zC^6y~8VqX>^!5x`D|a*_Zf;g_axmc!hvkrJ(3)vzt=!S{C`=qn&>n;&xC!CmZZ(n~ zp9UNKWE!@TVB@;Cwez*%zbdP$fZYiIrVvjjUM>CDzFq5a{t4HBEg9 z`dLLcR5Vblq7JRD)G}1i^Gjf)t+PPbnBvFgshGj?y)~%4?*omhK`I~KdUQMM85GrI~3)%k^qTWq;R6!r+5Fxjbfl5ThbzR6n zImN|E`YI5;W-UbhnE{^S(n7pXA}TIZ>*j?y;Pc&R2kL#|QLQX*?3#xr`rq z_G-gGwX>O90!|-4>K~%18F*UkZtv{qD=#f(33!8ce??MRiR8;-#kjPPKB)v@i!84W z)rcI8#@<)wu1?G3711p24NW5-A5QueSsTJTS-cIe<#E>PJZaW*^Xi?M(JH>P6Z0Uk zVMUe)eXDi#2xt`bZqcB+gZ5A;>kui~s&)Y+!>WWwX(Rhs6!z2=t_$bGqzgoow0992 ziN}DrFdh|D9UtOb>+9{IF#UW1N5!Lmj7I-nzdO*%ymsn|viY*ap{U;YMC9e!pA=V8QQ@p;P$i^tL?+!4MZ zhlzvxjPs;Eyq>xiP48LhWzS~spGt2w-87~j{&Vj%WG4SQ4Hl^d!!7I-BkC+!^G)+P z;YVS=U?qP3Yi_SeXB;y=!%i7)8?H>+EHDk*lT#B=&C=y@XkL34*f4Po Gq<;YlILN&K diff --git a/tests/macros/test_dupe_files.py b/tests/macros/test_dupe_files.py index cd296e168..bc8b926f5 100644 --- a/tests/macros/test_dupe_files.py +++ b/tests/macros/test_dupe_files.py @@ -8,25 +8,21 @@ from tagstudio.core.library.alchemy.library import Library from tagstudio.core.library.alchemy.models import Entry from tagstudio.core.library.alchemy.registries.dupe_files_registry import DupeFilesRegistry -from tagstudio.core.utils.types import unwrap CWD = Path(__file__).parent def test_refresh_dupe_files(library: Library): library.library_dir = Path("/tmp/") - folder = unwrap(library.folder) fields: list[BaseField] = [TextField(name="Title", value="I'm a Test Title")] entry = Entry( - folder=folder, path=Path("bar/foo.txt"), fields=fields, ) entry2 = Entry( - folder=folder, path=Path("foo/foo.txt"), fields=fields, ) diff --git a/tests/test_library.py b/tests/test_library.py index 13b99dd58..e260225a8 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -77,7 +77,6 @@ def test_library_add_file(library: Library): """Check Entry.path handling for insert vs lookup""" entry = Entry( path=Path("bar.txt"), - folder=unwrap(library.folder), fields=[TextField(name="Title", value="I'm a Test Title")], ) @@ -139,8 +138,7 @@ def test_get_entry(library: Library, entry_min: Entry): def test_entries_count(library: Library): - folder = unwrap(library.folder) - entries = [Entry(path=Path(f"{x}.txt"), folder=folder, fields=[]) for x in range(10)] + entries = [Entry(path=Path(f"{x}.txt"), fields=[]) for x in range(10)] new_ids = library.add_entries(entries) assert len(new_ids) == 10 @@ -254,7 +252,6 @@ def test_update_entry_with_multiple_identical_text_fields(library: Library, entr def test_mirror_entry_fields(library: Library): # Create and add entries with fields entry_a = Entry( - folder=unwrap(library.folder), path=Path("title_and_date.txt"), fields=[ TextField(name="Title", value="I'm a Test Title"), @@ -262,7 +259,6 @@ def test_mirror_entry_fields(library: Library): ], ) entry_b = Entry( - folder=unwrap(library.folder), path=Path("notes.txt"), fields=[ TextField(name="Notes", value="These are my notes.\nNo peeking!", is_multiline=True), @@ -270,7 +266,6 @@ def test_mirror_entry_fields(library: Library): ], ) entry_c = Entry( - folder=unwrap(library.folder), path=Path("date_published.txt"), fields=[ DatetimeField(name="Date Published", value="2000-01-01 12:00:00"), @@ -319,14 +314,11 @@ def test_mirror_entry_fields(library: Library): def test_merge_entries(library: Library): - folder = unwrap(library.folder) - tag_0: Tag = unwrap(library.add_tag(Tag(id=1010, name="tag_0"))) tag_1: Tag = unwrap(library.add_tag(Tag(id=1011, name="tag_1"))) tag_2: Tag = unwrap(library.add_tag(Tag(id=1012, name="tag_2"))) entry_a = Entry( - folder=folder, path=Path("a"), fields=[ TextField(name="Author", value="Author McAuthorson"), @@ -334,7 +326,6 @@ def test_merge_entries(library: Library): ], ) entry_b = Entry( - folder=folder, path=Path("b"), fields=[TextField(name="Notes", value="test note", is_multiline=True)], ) From d7b2a5efa7296aa99cf5086731e19c64318c98c9 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Mon, 13 Jul 2026 11:17:45 +0200 Subject: [PATCH 3/5] fix: missing filename column after migration --- src/tagstudio/core/library/alchemy/library.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 93feb4877..11bd35295 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -887,7 +887,8 @@ def __apply_db203_migration(self, session: Session, library_dir: Path): suffix VARCHAR NOT NULL, date_created DATETIME, date_modified DATETIME, - date_added DATETIME, filename TEXT NOT NULL DEFAULT '', + date_added DATETIME, + filename TEXT NOT NULL DEFAULT '', PRIMARY KEY (id), UNIQUE (path) ) @@ -897,8 +898,9 @@ def __apply_db203_migration(self, session: Session, library_dir: Path): # transfer data to new table session.execute( text(""" - INSERT INTO entries_new (id, path, suffix, date_created, date_modified, date_added) - SELECT id, path, suffix, date_created, date_modified, date_added + INSERT INTO entries_new (id, path, suffix, date_created, date_modified, date_added, + filename) + SELECT id, path, suffix, date_created, date_modified, date_added, filename FROM entries """) ) From f1503f75fba0682ee54268e203f760c0cc551a83 Mon Sep 17 00:00:00 2001 From: Jann Stute Date: Mon, 13 Jul 2026 11:21:52 +0200 Subject: [PATCH 4/5] fix: bump db version to 300, since it is a breaking change --- .../core/library/alchemy/constants.py | 2 +- src/tagstudio/core/library/alchemy/library.py | 4 ++-- .../.TagStudio/ts_library.sqlite | Bin 122880 -> 122880 bytes 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tagstudio/core/library/alchemy/constants.py b/src/tagstudio/core/library/alchemy/constants.py index b2d42ebca..73493c9af 100644 --- a/src/tagstudio/core/library/alchemy/constants.py +++ b/src/tagstudio/core/library/alchemy/constants.py @@ -9,7 +9,7 @@ DB_VERSION_CURRENT_KEY: str = "CURRENT" DB_VERSION_INITIAL_KEY: str = "INITIAL" -DB_VERSION: int = 203 +DB_VERSION: int = 300 TAG_CHILDREN_QUERY = text(""" WITH RECURSIVE ChildTags AS ( diff --git a/src/tagstudio/core/library/alchemy/library.py b/src/tagstudio/core/library/alchemy/library.py index 11bd35295..33b6ffb63 100644 --- a/src/tagstudio/core/library/alchemy/library.py +++ b/src/tagstudio/core/library/alchemy/library.py @@ -569,7 +569,7 @@ def open_sqlite_library( (self.__apply_db200_migration, 200, None), # changes: field tables (self.__apply_db201_migration, 201, 200), # changes: field tables (self.__apply_db202_migration, 202, None), # changes: tag_parents - (self.__apply_db203_migration, 203, None), # changes: deletes folders + (self.__apply_db300_migration, 300, None), # changes: deletes folders ] for migration, v, iv in migrations: if loaded_db_version < v and (iv is None or initial_db_version < iv): @@ -876,7 +876,7 @@ def __apply_db202_migration(self, session: Session, library_dir: Path): session.flush() logger.info("[Library][Migration][202] Verified TagParent table data") - def __apply_db203_migration(self, session: Session, library_dir: Path): + def __apply_db300_migration(self, session: Session, library_dir: Path): ## remove folder_id column from entries table # create new table in the desired scheme (without folder_id column) session.execute( diff --git a/tests/fixtures/search_library/.TagStudio/ts_library.sqlite b/tests/fixtures/search_library/.TagStudio/ts_library.sqlite index 2c7c2f0ef07bdec9f3fcee03159a72210b2f319c..0cfb5bed4485b806fa87f83c1a82014ffc69cb8b 100644 GIT binary patch delta 1340 zcmZvaT}V@57{}iy_i^^UIh}52r?vbX`3Z%1A!JcuwnkvsSYvE@SjL)bZj{o627TC# zby2SukwPF9q0k2-=%TJR=f}JXOhvl$E+WB;C_3*s%X3Z&yV!Vs`~UyX|9wV3sYkib zJNPki02(qsC!pS3a^L9B9Gr%)inng_XUKe*0Y(LPxgWOyv$N2mNxRh*G!1t;@v~X@ zjYsCdka;=>1Ll)T3o^YoT{e{%Ul_ecWO#1q(eLOd_3gSX9oC)J?r0~q?V2skJW}J6s%_OnRf96899N!IS~#D1g{iHULM|&poi6rTA~lS^<-l#_zcwY}(T*Al z)sSfornBI&rQ@k^G|Kk#v+CX1dD$#=GZm?#P>}qu4Uc8vlq(tP8%XxW*ibSwbUhtT zvP2+qSPD7u)EemV%PcS=J}AZAa(r&meEKXYS zqfM~ksSQwL&pgyS2b1A=lnt@5*g#^4PamWy6d)@$3+%``STLOjhK?p#HZDa24hs1R z+9;qco1nn2*12s9^T0SFv9wItZ>LZ%!H*E!fz5fSwbe+LGh0lPJu{!trY4e&Mv`uVFNUHZ2=hT(3Nn4ui01nU$_m>;wL$5ma6BRq&EPz|hsgTSwD`5T$b!eNgtYXz52)ap||F~0HODJwd5jUb1e3R741clk$bM8C$<4o?YnB(7b_%`21 zs)g_K5U}+>P|}6Hi?D98WUnV@rjl1O$!t2i;FSq-Rn}aT!$JPk16Vu_!Kj!<%b z$7z8Rw@W0dX2;k7E?4BK_*aTSiTuuY%&H5V#14Z&(W Date: Mon, 13 Jul 2026 11:31:59 +0200 Subject: [PATCH 5/5] fix: add empty library fixture for version 202 --- .../.TagStudio/ts_library.sqlite | Bin 0 -> 106496 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite diff --git a/tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite b/tests/fixtures/empty_libraries/DB_VERSION_202/.TagStudio/ts_library.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..db9b1c395bdb2a0373093ad8e26260dc4a64a518 GIT binary patch literal 106496 zcmeI5Yit|Yb;oDOk%q(9crDYkOw%GYtF>r}7KapHaa@F?NUyrG?3LxNw`eL*;#eX~ zky?_n*IN{=yiJOtSftoI5Cr{{B5jZYMZYu%&<|O(!J^p~1&S@Y?T5BNk#6e%1&XAL z1stI1z4IVvMnmr=TVyNsKYnQDo^$TK=XcM2%%wPU^ZAu#Uy0r6ba(6hScW;luq^Yb zSd3v9H~Dv#{4op9+34@ZI2T|F;5P3EcIM z`+w8>dEeg%g16xr<4=$M?%20SCst|c8Qq$G)HHW4_}ek17EYOk_zuQ!L>dyxo&4wxMY-O!lTCc{ISC^`{W1|dW*H>dEqu4CXk>-@>oN+2} zuihmZy^XD%W~-qWsHaUj7HBsx3e?kMvqnydJ7GS2Im8|aW)FKC?fR~=(c9^C`#beE zb+C8FsUW+eUQRDIOEZ{{4RS~vJbRiCOA`C=g?@drrBLtdt!BNa^h~E=y^Qrzd8Min z(sg39UZL3_RM)Flt81~<>+7-ATPrK`F|R-iiv2=qt@3QiDxmp}u(`3ee66(hV(hu< zi?LZko#s=!zE)knx=Pb#HPb|Fty-(DRaYz3o3t{-E28U79Q04AuDrT1a7B*@YNwxu z4!NQWs4GvX8r7na;OMKR-dZN}F~i~~^1|-%AQQ9CgBq)DXG7!RWr=0fKd_4c;%!<>+*n>9)(m(Kb<&4YY?ekK|3AA#E9e%1~$lNK4uslVm8om zZhSkoK|W?|#4jUMvJ)S~O!sp`Wu{-8y7AO_cy5k;Q6;9M?kO+rEA1`Cc6ri5&L-4) zA+bz6ynliZheGUUo;O;7_PgFw7Hh5Ijy)}A4!zQ?_3O*4#PnKqb^VEbaM1sa9!j;h zI;~Du9btNV^{qkY9g2Fb{cW>BlZ<@zzOvovK8TgCUtg(~RxLvSk?%Ac4W;cY-RiW> zxiy--`tD|Pd%xaqcG{+}I@GwG(c6q|m9yZ}mGYA^}+Oc=stGD(o_|1*%G%gysS=_gd5)av+*4>x2 zKvQ1s>#p{Pr=rD8lR$N`x3Rn5>Ni`>w&L8N&V%E~iNL7!&yK-uKOc@n*w64P109h5 zBVh4;0MD`4(TKd$X*HCtDNHS)zgO??7{P53+-bIykwkj?ckVP_cC@20x3Se#$d5*B zi7aK-m#maF<{%1}8pkNv}W{D;(RU1EPvA0&2pTAX&%~D56Z%PKn z!==cmNl2%;A?qi>hnKy4I38zzqht(wdQYWukJe{>mi8E?(@-=IL>9V~tYz>zBz1&4 zz1MB-*1Hd;9i=^mj8qLGHl);lZOqy1bV(36_67@PTH*QFpmv9*x1q#5VGUBv0~mpw z@r{SC#z(;(qS7WW4X6LtJbXABWe>E8=AP2+kx{m1{1hy-ja15d`mQp9i#AL-t^F*% zs~M15F9_q|N_14MjUqQ>m4S zguWQHM7_Bi&|GLY;GfhD^jdDD^%GBW{KKfrx;lU0cdNbTU`quu43FU%XTdT|r!6_S za&(tU9R(H+gFI*$a!(4wfo5+*9-fbpyFCxR<2#sjkvpm@tnQ0-O5NxyyL&A%)Y<0Y zVY=Jm=O}A++WHA+C5yXiqaIvg`EWMNK8$LE)nL9HR%bX>4%1wgDmrPX)%)L`RS(xc zKydToHyH6f@saqC;@^sYE`Ez#;s5~<009sH0T2KI5C8!X009sH0TB3D2>5wUV4{`I z?k?FV?Fr-LD!keu+aTf?IhB@_-d4A{M?W6nInKjO+*(`ly2;68soQV%`d*iMzTYRG z4+|_g53e`-Ers6yv*P;<`HKStKmY_l00ck)1V8`;KmY_l00cnbcoE=PHxpVSzeGXr z|Hs7>jQEcD$Kr2`za+}!0tX0y00@8p2!H?xfB*=900@8p2pn?)Q{(PLf_|s6zu#zf zl5M5aUaxQ8RIg&I1&Li`mJ3r>*pc z%2k>eTc?SV+nr1f)q6YjhFSBbdOhQEFOaV=8+q>4dwr#4%I}7jaDjEp^xLVr8{~VV z^h-*OVYxT;6fbGdaq$&K{F?Zh_zL;`|8I-mKjuvg_doyyKmY_l00ck)1V8`;KmY_l z;AcqSq&vY5@6hQ+!+6-8F(nPQ7RDysc~iP+AAvjNPMRdzmVrCuUN9xQ^f875c;|7LnAOHd&00JNY z0w4eaAOHd&00JOD2;lx7VE_Uk00JNY0w4eaAOHd&00JNY0>_^K{r!KJ_&rAa0r?LH z2!H?xfB*=900@8p2!H?xfB*=9z>y{3cex|3%B{7v>gqc4mY*e8%d5-l%cT|OE&BPt zOMJwLkHq(mtb?dH2!H?xfB*=900@8p2!H?xfB*=9fJVUQjK2H{rvxm z`@b3S@5Qf*eeqfGZ16vW-wA#txEHJi&jh|7cq8yT0VPoI|JeT*{?GY;(J%QP`@ZY@ zf^W-L@VUKjd;h@utKR3llJH3Qw(z>}X(8?Tk>{^HU-aDZ%<+%;zvREbf0~~i|JU(v zjK4IV8~gFtn`56FyFC`?-sS#~>vMThG!75|fschi{t6#Y#3K=bK7KTLf2Y}3hK~rP zY4nXJu@`8%BxlHbYJUaZwp z#iH4?*={tqI}T+os{%>R64OPgkSC_uY{6XF&DOqCnPrkJrHXP&u1J}3Azw>n^JcTH z2X(^7c=qZ5Y=tCCsj^(8W=l)urE)fBHtQ?(mV?tI=;=~6mNnj$aNO2tyTQk2bQcDnWUwo_%Vs{%=` zrIu1zsk)R)rPHMpKbM#@BB!e~k|WL6ltxU?g7{mcSrjt)bS|^NM-x#~77mprwvt>( z2xJi$zI~8@>3DUv(@RelJt4LLo3r$d@Dp6qi+q8h`;$IBN_}g`v7{SRBGu$du2@-^<0tczM18Z>X*WzLO*Jd4BB@%d zy*?ZF~f&4A!0VdKvm6VvgvFo^E5w|q3y5wF+yK;LN!V+(m!*O?!#uQzGWI2sk!>5 zsWMHMWSNXMh1qF7oTLtu?$R|ym-Zy+GEJ0s#to8lP{jMqOG}V%tDv>FMblOYj_-Hapne2DB z_uGv_buR1aQm!gj(u8fptF8pPsEnfjTGEYK45clsLvI^op=|&!rV6E6E_23GA?uuDm}q^B&O14Rh?El6OX=NxkaRS; zdG|p~ou~@cTCKKt;WQswpv}75Z1)d2aE&BNxk9a0Ud)DgX<^v!ZL^D|KH3aZkZZMk zE_2G_g>`PRSgG?%rAUlQ`3ZhzVMfJD#~Ay}VlmTams+hV=QG)pmP*>@6^orVw-C=t z*?j&fOL_Kb!D6RR3$LxeUWU}DNWfltwQ4f|aQh;ej z*L5p2l`O2tkWsr7a=yyte|UJmqdo9x+2n+LbD!S z(>9VAud<_yAZ_(?k>|rQ?Tfb1G&0qqQd{uMj_bKv_KSg4&sk4 zYc6tn9c+J z=QgS9OBDL~{}s>I8S%ILulpI_zj*&H_)os5|Bt-w;B&#pzAL^r{I~rP-|va(;2QxU z@F+0rec$)%fmZ`x3;O(j6?oJCAL8}kc3{W4$elz&l;8(?W#qR_!1(wCr-q(Gb;@yws#s`Ih00@8p2!H?x903AR zE}mdV?NVNu;%0Q|p=C3*`Q$lnN|#%g$y9jxJSQ1K+sc@VKQR>Bm%>zB9A<4@`%+c?QThvl%c4%r{#!}(pu)_A0D-~ykIop=1RGJxg)LxQwmJ26Y$IYYZY3@Q&&3fc+QGAjMC0VC!q1btDN;6JcgIn9LnNo{& z-1Rvh;U<%;^EOX3%0)?SM{Mhyi*fM-X7_2p6UbP8&97&vQ{CJG3pPn@*bY zw)S&f;f!iOa_=R4mJ2Pgjyo-<( zNd2nRJ}?_npPH|=y$qRGm05>nLnf#)Tj1*Z4xV#dSSH=k@)3(+$g76dHZo-6YE|vc zV@Su;Y;7%L2szc%-YACD9p~aQ>ogu48m>uhLKcX|a5y$Zc>muKSp1-hAOHd&00JNY z0w4eaAOHd&00JQJ5eeY={~r+`%s~JIKmY_l00ck)1V8`;KmY_l;0O@F=l@5bkx)et z009sH0T2KI5C8!X009sH0TB3z1aSZV5dp#+1V8`;KmY_l00ck)1V8`;KmY`e00Dac z?-swri0_DB694Q7R2WqR0T2KI5C8!X009sH0T2KI5CDN=L*OFoWulBc>G3d3t$wf5 zwY-?icy^!mEWrtvmzg4-3qvD~8tRT4X{67Q)2!H?xfB*=900@8p2!H?xfB*;_ ze**OLf9AyT_Y)q100@8p2!H?xfB*=900@8p2!O!F@u4Py7e*Epmzj z1V8`;KmY_l00ck)1V8`;KmY_l;QtPRz!=9&3C8OIdI8RS*$~_u6JqHL0bBtW$3)dr z`fdQ1kL8$%M}I2-{rul8zC-W--z7hAfB*=900@8p2!H?xfB*=900@8p2pk;(L2il} z-0}BZelE%kb_(c)ar@uUp8xkgBmPKy|LC}fnt}ibfB*=900@8p2!H?xfB*=900?{t r0&ce;aBlJ#Kz#rIL(qZ>2!H?xfB*=900@8p2!H?xfB*;_aRUDX@~~bW literal 0 HcmV?d00001