From 90d82c1e4ef62d8f90d03c6719e714c019f437ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 6 Jul 2026 23:29:02 +0700 Subject: [PATCH 1/7] refactor: Removed filesystem.py and migrated to justin_utils --- src/justin/actions/mixins.py | 6 +- src/justin/cms_2/sub_cms/photosets_cms.py | 2 +- src/justin/one_timers/embrace_tradition.py | 2 +- src/justin/one_timers/progress_inverter.py | 2 +- src/justin/postmypost/postmypost.py | 2 +- src/justin/shared/filesystem.py | 616 ------------------ src/justin/shared/helpers/parts.py | 2 +- src/justin/shared/metafiles/metafile.py | 2 +- src/justin/shared/models/exif.py | 2 +- src/justin/shared/models/photoset.py | 2 +- src/justin/shared/models/sources.py | 3 +- src/justin/shared/world.py | 2 +- .../destinations_aware_command.py | 2 +- src/justin/typer/date_split_command.py | 2 +- src/justin/typer/populate_command.py | 2 +- src/justin/typer/register_people_command.py | 2 +- src/justin/typer/sequence_command.py | 2 +- .../stage_command/abstracts/file_mover.py | 3 +- .../checks/extracting/structure.py | 5 +- .../checks/simple/metafile_state.py | 2 +- .../typer/stage_command/hooks/candidates.py | 2 +- .../typer/stage_command/hooks/progress.py | 2 +- 22 files changed, 25 insertions(+), 642 deletions(-) delete mode 100644 src/justin/shared/filesystem.py diff --git a/src/justin/actions/mixins.py b/src/justin/actions/mixins.py index 3ad71a5..2d4c61f 100644 --- a/src/justin/actions/mixins.py +++ b/src/justin/actions/mixins.py @@ -1,5 +1,5 @@ -from justin.shared import filesystem -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder, open_file_manager + from justin.shared.metafiles.metafile import GroupMetafile from justin.shared.models.photoset import Photoset @@ -22,7 +22,7 @@ def get_community_id(posts_folder: Folder, root_photoset: Photoset) -> str | Non ) if not answer: - filesystem.open_file_manager(posts_folder.path) + open_file_manager(posts_folder.path) elif not EventUtils.__validate(answer): print("This was not event url. Try another.") else: diff --git a/src/justin/cms_2/sub_cms/photosets_cms.py b/src/justin/cms_2/sub_cms/photosets_cms.py index 5143750..4b60ce6 100644 --- a/src/justin/cms_2/sub_cms/photosets_cms.py +++ b/src/justin/cms_2/sub_cms/photosets_cms.py @@ -3,7 +3,7 @@ from justin.cms_2.storage.sqlite.sqlite_database import SQLiteDatabase, SQLiteEntry from justin.cms_2.storage.sqlite.sqlite_entries import Closed, Photoset as PhotosetEntry, PersonPhotos, Drive, MyPeople -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.metafiles.metafile import PhotosetMetafile from justin.shared.models.photoset import Photoset from justin_utils.util import bfs diff --git a/src/justin/one_timers/embrace_tradition.py b/src/justin/one_timers/embrace_tradition.py index 727c8c7..21ff60d 100644 --- a/src/justin/one_timers/embrace_tradition.py +++ b/src/justin/one_timers/embrace_tradition.py @@ -1,7 +1,7 @@ from collections import defaultdict from pathlib import Path -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder def main(): diff --git a/src/justin/one_timers/progress_inverter.py b/src/justin/one_timers/progress_inverter.py index 2310712..254984c 100644 --- a/src/justin/one_timers/progress_inverter.py +++ b/src/justin/one_timers/progress_inverter.py @@ -1,6 +1,6 @@ from pathlib import Path -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder def main(): diff --git a/src/justin/postmypost/postmypost.py b/src/justin/postmypost/postmypost.py index 8147063..4af9258 100644 --- a/src/justin/postmypost/postmypost.py +++ b/src/justin/postmypost/postmypost.py @@ -8,7 +8,7 @@ from justin.postmypost.channels import get_channels from justin.postmypost.utils import get_all -from justin.shared.filesystem import File +from justin_utils.filesystem import File configuration = Configuration( access_token=os.environ["BEARER_TOKEN"] diff --git a/src/justin/shared/filesystem.py b/src/justin/shared/filesystem.py deleted file mode 100644 index 51864db..0000000 --- a/src/justin/shared/filesystem.py +++ /dev/null @@ -1,616 +0,0 @@ -import platform -import shutil -import webbrowser -from abc import ABC, abstractmethod -from functools import partial -from pathlib import Path -from typing import List, Dict, Callable, Self - -from justin_utils.data import DataSize -from justin_utils.time_formatter import format_time -from justin_utils.transfer import TransferSpeedMeter, TransferTimeEstimator - - -def __subfolders(path: Path) -> List[Path]: - if path.exists(): - return [i for i in path.iterdir() if i.is_dir()] - - return [] - - -def __subfiles(path: Path) -> List[Path]: - return [i for i in path.iterdir() if i.is_file()] - - -def __tree_is_empty(path: Path): - return len(__subfiles(path)) == 0 and all([__tree_is_empty(subfolder) for subfolder in __subfolders(path)]) - - -def __flatten(path: Path) -> List[Path]: - files = [] - - for entry in path.iterdir(): - if entry.is_file(): - files.append(entry) - elif entry.is_dir(): - files += __flatten(entry) - - return files - - -def __check_paths(src_path: Path, dst_path: Path): - assert isinstance(src_path, Path) - assert isinstance(dst_path, Path) - - if dst_path == src_path.parent: - return - - assert src_path.exists() - - -def open_file_manager(path: Path): - # noinspection PyTypeChecker - webbrowser.open(str(path)) - - -# endregion - -# region determining drives - -def __get_unix_mount(path: Path) -> Path: - while True: - if path.is_mount(): - return path - - path /= ".." - path = path.resolve() - - -def __get_windows_mount(path: Path) -> Path: - path = path.absolute() - - return list(path.parents)[-1] - - -def __get_mount(path: Path) -> Path: - system_name = platform.system() - path = path.resolve().absolute() - - if system_name == "Darwin": - return __get_unix_mount(path) - elif system_name == "Windows": - return __get_windows_mount(path) - else: - assert False - - -# endregion - -# region generic operations - -def __handle_tree(src_path: Path, dst_path: Path, file_handler: Callable[[Path, Path], None], action_name: str): - assert src_path.is_dir() - - dst_path = dst_path.resolve() - - files = __flatten(src_path) - files.sort() - - total_size = DataSize.from_bytes(sum(file.stat().st_size for file in files)) - total_copied = DataSize.from_bytes(0) - - speed_meter = TransferSpeedMeter() - - print(f"{action_name} {src_path.name} from {src_path.parent} to {dst_path}...") - - speed_meter.start() - - for index, file in enumerate(files): - assert file.is_file() - - relative_path = file.relative_to(src_path.parent) - file_size = file.stat().st_size - - speed_meter.feed(file_size) - - current_speed = speed_meter.current_value - - log_string = f"{action_name} {relative_path} ({index}/{len(files)})" \ - f" ({total_copied} / {total_size}) {current_speed}." - - estimated_time = TransferTimeEstimator.estimate(current_speed, total_size - total_copied) - - if estimated_time is not None: - log_string += f" {format_time(estimated_time)} remaining." - - print(log_string, flush=True) - - file_handler(file, dst_path / relative_path) - - total_copied.add_bytes(file_size) - - assert __tree_is_empty(src_path) - - __remove_tree(src_path) - - print(f"Processed {len(files)}/{len(files)} files, {total_copied} / {total_size}, {speed_meter.average_value}") - - print("Finished") - - -# endregion - -# region move operations - -def __move_file(file_path: Path, new_path: Path): - assert __get_mount(file_path) != __get_mount(new_path) - - try: - __copy_file(file_path, new_path) - except: - __remove_file(new_path) - - raise - - try: - __remove_file(file_path) - except: - __remove_file(file_path) - - raise - - -__move_tree = partial(__handle_tree, file_handler=__move_file, action_name="Moving") - - -def move(src_path: Path, dst_path: Path): - __check_paths(src_path, dst_path) - - new_file_path = dst_path / src_path.name - - if src_path == new_file_path: - return - - if __get_mount(src_path) == __get_mount(dst_path): - new_file_path.parent.mkdir(parents=True, exist_ok=True) - - src_path.rename(new_file_path) - elif src_path.is_dir(): - __move_tree(src_path, new_file_path) - elif src_path.is_file(): - __move_file(src_path, new_file_path) - else: - assert False - - -# endregion - -# region copy operations - -def __copy_file(file_path: Path, new_path: Path): - new_path = new_path.resolve() - - assert not new_path.exists() - - new_path.parent.mkdir(parents=True, exist_ok=True) - - assert new_path.parent.exists() - assert new_path.parent.is_dir() - - # noinspection PyTypeChecker - shutil.copy2(file_path, new_path) - - -__copy_tree = partial(__handle_tree, file_handler=__copy_file, action_name="Copying") - - -def copy(src_path: Path, dst_path: Path): - __check_paths(src_path, dst_path) - - new_item_path = dst_path / src_path.name - - if src_path.is_file(): - __copy_file(src_path, new_item_path) - elif src_path.is_dir(): - __copy_tree(src_path, new_item_path) - else: - assert False - - -# endregion - -# region remove operations - -def __remove_file(file_path: Path): - file_path.unlink() - - -def __remove_tree(path: Path) -> None: - assert isinstance(path, Path) - assert path.is_dir() - - for entry in path.iterdir(): - __remove_tree(entry) - - path.rmdir() - - -# endregion - - -class Movable(ABC): - @abstractmethod - def move(self, path: Path): - pass - - @abstractmethod - def move_down(self, subfolder: str) -> None: - pass - - @abstractmethod - def move_up(self) -> None: - pass - - @abstractmethod - def copy(self, path: Path) -> None: - pass - - -class PathBased(Movable): - def __init__(self, path: Path) -> None: - super().__init__() - - self.__path = path.absolute() - - @property - def path(self) -> Path: - return self.__path - - def move(self, path: Path) -> None: - # files and folders are copied differently. Also having same drive matters - move(self.path, path) - - self.__path = path / self.path.name - - def copy(self, path: Path) -> None: - copy(self.path, path) - - def move_down(self, subfolder: str) -> None: - self.move(self.path.parent / subfolder) - - def move_up(self) -> None: - self.move(self.path.parent.parent) - - def rename(self, new_name: str) -> None: - new_path = self.path.with_stem(new_name) - - self.path.rename(new_path) - - self.__path = new_path - - -class File(PathBased): - - @property - def name(self) -> str: - return self.path.name - - @property - def size(self) -> int: - return self.path.stat().st_size - - def is_file(self) -> bool: - return self.path.is_file() - - def is_dir(self) -> bool: - return self.path.is_dir() - - @property - def mtime(self) -> float: - return self.path.stat().st_mtime - - @property - def stem(self) -> str: - return self.path.stem - - @property - def suffix(self) -> str: - return self.path.suffix - - @property - def extension(self) -> str: - return self.path.suffix - - def __str__(self) -> str: - return f"File({self.path})" - - def __repr__(self) -> str: - return str(self) - - def __hash__(self): - return hash(self.path) - - def __eq__(self, o: object) -> bool: - if not isinstance(o, File): - return False - - return o.path == self.path - - def __lt__(self, other: 'File') -> bool: - return self.name < other.name - - -class Folder(PathBased): - __FILES_TO_UNLINK = [name.lower() for name in [ - ".DS_store", - "NC_FLLST.DAT", - ]] - - # noinspection PyTypeChecker - def __init__(self, path: Path) -> None: - super().__init__(path) - - self.__subfolder_mapping: Dict[str, Folder] = None - self.__files: List[File] = None - - @property - def __subfolders(self) -> Dict[str, Self]: - if self.__subfolder_mapping is None: - self.refresh() - - return self.__subfolder_mapping - - @property - def name(self) -> str: - return self.path.name - - @property - def stem(self) -> str: - return self.path.stem - - @property - def files(self) -> List[File]: - if self.__files is None: - self.refresh() - - return self.__files - - @property - def total_size(self) -> int: - return sum(file.size for file in self.files) + \ - sum(subfolder.total_size for subfolder in self.subfolders) - - @property - def subfolders(self) -> List[Self]: - return sorted(list(self.__subfolders.values()), key=lambda x: x.name) - - def __contains__(self, key: str) -> bool: - return key in self.__subfolders - - def __getitem__(self, key: str | Path) -> Self | None: - if isinstance(key, str): - return self.__get_by_str(key) - elif isinstance(key, Path): - return self.__get_by_path(key) - - return None - - def __get_by_str(self, key: str) -> Self | None: - first, *rest = key.split("/", maxsplit=1) - - if first not in self: - return None - - if rest: - return self[first][rest[0]] - else: - return self.__subfolders.get(key) - - def __get_by_path(self, path: Path) -> Self | None: - root, *rest = path.parts - - if root in self: - return self[root][Path(*rest)] - else: - return None - - def flatten(self) -> List[File]: - result = self.files.copy() - - for subtree in self.subfolders: - result += subtree.flatten() - - return result - - def file_count(self) -> int: - return sum(subtree.file_count() for subtree in self.subfolders) + len(self.files) - - def size(self) -> int: - return sum(file.size for file in self.flatten()) - - def empty(self) -> bool: - return self.file_count() == 0 - - def exists(self) -> bool: - return self.path.exists() - - def remove(self, *, with_files: bool = False) -> None: - if with_files: - for file in self.files: - file.path.unlink() - - self.refresh() - - assert len(self.files) == 0 - - for subtree in self.subfolders: - subtree.remove(with_files=with_files) - - self.path.rmdir() - - def refresh(self): - self.__subfolder_mapping = {} - self.__files = [] - - for child in self.path.iterdir(): - if child.is_dir(): - child_tree = self.from_path(child) - - if not child_tree.empty(): - self.__subfolders[child.name] = child_tree - else: - try: - child_tree.remove() - except Exception: - print(f"Failed to remove empty tree: \"{child_tree}\"") - - self.__subfolders[child.name] = child_tree - - elif child.is_file(): - if child.name.lower() in Folder.__FILES_TO_UNLINK: - child.unlink() - elif child.stem.lower() == "_meta": - continue # metafile not included in files - else: - self.files.append(File(child)) - - else: - print("Path is neither file nor dir") - - exit(1) - - self.__files.sort(key=lambda x: x.name) - - def move(self, path: Path) -> None: - if isinstance(path, Folder): - path = path.path - - super().move(path) - - self.refresh() - - def rename(self, new_name: str) -> None: - new_path = self.path.with_stem(new_name) - - if not new_path.exists(): - super().rename(new_name) - elif new_path.is_dir(): - self.merge_into(new_path) - else: - assert False - - self.refresh() - - def merge_into(self, new_path): - for subtree in self.subfolders: - subtree.move(new_path) - - for file in self.files: - file.move(new_path) - - self.__path = new_path - - def __str__(self) -> str: - return f"FolderTree: {self.path}" - - def __repr__(self) -> str: - return str(self) - - @property - def __key(self): - return self.path - - def __hash__(self): - return hash(self.__key) - - def __eq__(self, other: Self) -> bool: - return isinstance(other, type(self)) and other.__key == self.__key - - def __type_copy(self, path: Path) -> Self: - return type(self).from_path(path) - - def __truediv__(self, other) -> Self: - return self.__type_copy(self.path / other) - - def mkdir(self) -> None: - self.path.mkdir(parents=True, exist_ok=True) - - @property - def parent(self) -> Self: - return self.__type_copy(self.path.parent) - - @classmethod - def from_path(cls, path: Path) -> Self: - return cls(path) - - -class FolderBased(PathBased): - def __init__(self, folder: Folder) -> None: - super().__init__(folder.path) - - self.__folder = folder - - @property - def folder(self) -> Folder: - return self.__folder - - @property - def name(self) -> str: - return self.folder.name - - @property - def path(self) -> Path: - return self.folder.path - - def move(self, path: Path) -> None: - self.folder.move(path) - - -def parse_paths(paths: List[Path]) -> List[PathBased]: - result = [] - - for path in paths: - if path.is_file(): - result.append(File(path)) - elif path.is_dir(): - result.append(Folder(path)) - else: - assert False - - return result - - -class RelativeFileset(Movable): - - def __init__(self, root: Path, files: List[PathBased]) -> None: - super().__init__() - - self.__root = root - self.__files = files - - def move(self, path: Path) -> None: - for file in self.__files: - absolute_path = file.path.parent - - relative_path = absolute_path.relative_to(self.__root) - - new_path = path / relative_path - - file.move(new_path) - - self.__root = path - - def move_down(self, subfolder: str) -> None: - self.move(self.__root / subfolder) - - def move_up(self) -> None: - self.move(self.__root.parent) - - def copy(self, path: Path) -> None: - for file in self.__files: - file_parent_path = file.path.parent - file_relative_path = file_parent_path.relative_to(self.__root) - - new_path = path / file_relative_path - - file.copy(new_path) diff --git a/src/justin/shared/helpers/parts.py b/src/justin/shared/helpers/parts.py index f769c7b..b31a8f5 100644 --- a/src/justin/shared/helpers/parts.py +++ b/src/justin/shared/helpers/parts.py @@ -1,7 +1,7 @@ from abc import abstractmethod from typing import List, TypeVar -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder def is_part_name(name: str) -> bool: diff --git a/src/justin/shared/metafiles/metafile.py b/src/justin/shared/metafiles/metafile.py index ae6e1de..f4cc05a 100644 --- a/src/justin/shared/metafiles/metafile.py +++ b/src/justin/shared/metafiles/metafile.py @@ -6,7 +6,7 @@ from typing import Type, TypeVar, List, Self, ClassVar from uuid import UUID, uuid4 -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.helpers.utils import Json, fromdict from justin_utils import util diff --git a/src/justin/shared/models/exif.py b/src/justin/shared/models/exif.py index 4fbcd6c..156b992 100644 --- a/src/justin/shared/models/exif.py +++ b/src/justin/shared/models/exif.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Iterable -from justin.shared.filesystem import File +from justin_utils.filesystem import File class Exif: diff --git a/src/justin/shared/models/photoset.py b/src/justin/shared/models/photoset.py index a32c0d6..390896b 100644 --- a/src/justin/shared/models/photoset.py +++ b/src/justin/shared/models/photoset.py @@ -2,7 +2,7 @@ from typing import List, Self from uuid import UUID -from justin.shared.filesystem import File, FolderBased, Folder +from justin_utils.filesystem import File, FolderBased, Folder from justin.shared.helpers.parts import PartsMixin from justin.shared.metafiles.metafile import PhotosetMetafile from justin.shared.models import sources diff --git a/src/justin/shared/models/sources.py b/src/justin/shared/models/sources.py index 42e0b07..46052e9 100644 --- a/src/justin/shared/models/sources.py +++ b/src/justin/shared/models/sources.py @@ -1,12 +1,13 @@ from abc import abstractmethod from functools import cached_property from pathlib import Path + +from justin_utils.filesystem import Movable, File from typing import List, Iterable from justin.shared.models.exif import parse_exif, Exif from justin_utils import util, joins -from justin.shared.filesystem import File, Movable class Source(Movable): diff --git a/src/justin/shared/world.py b/src/justin/shared/world.py index c0d0e43..ab6de5f 100644 --- a/src/justin/shared/world.py +++ b/src/justin/shared/world.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import List, Iterable, Type -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.helpers.utils import Json from justin.shared.metafiles.metafile import LocationMetafile from justin.shared.models.photoset import Photoset diff --git a/src/justin/typer/base_commands/destinations_aware_command.py b/src/justin/typer/base_commands/destinations_aware_command.py index 1bbdb4d..2822a61 100644 --- a/src/justin/typer/base_commands/destinations_aware_command.py +++ b/src/justin/typer/base_commands/destinations_aware_command.py @@ -1,5 +1,5 @@ from justin.typer.base_commands.pattern_command import Extra -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.helpers.parts import folder_tree_parts from justin.shared.models.photoset import Photoset from justin.typer.base_commands.pattern_command import PatternCommand diff --git a/src/justin/typer/date_split_command.py b/src/justin/typer/date_split_command.py index 0452409..cc63eb6 100644 --- a/src/justin/typer/date_split_command.py +++ b/src/justin/typer/date_split_command.py @@ -9,7 +9,7 @@ from justin.typer.base_commands.pattern_command import Extra from justin.shared.context import Context -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.models.sources import parse_sources from justin.typer.base_commands.pattern_command import PatternCommand diff --git a/src/justin/typer/populate_command.py b/src/justin/typer/populate_command.py index 77ca082..20cea30 100644 --- a/src/justin/typer/populate_command.py +++ b/src/justin/typer/populate_command.py @@ -6,7 +6,7 @@ from typer import Typer, Argument from justin.typer.base_commands.pattern_command import Extra -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.metafiles.metafile import PostMetafile, AlbumMetafile, GroupMetafile from justin.typer.base_commands.pattern_command import PatternCommand diff --git a/src/justin/typer/register_people_command.py b/src/justin/typer/register_people_command.py index 7560fc0..1e0154e 100644 --- a/src/justin/typer/register_people_command.py +++ b/src/justin/typer/register_people_command.py @@ -7,7 +7,7 @@ from justin.typer.base_commands.pattern_command import Extra from justin.shared.context import Context -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.typer.base_commands.destinations_aware_command import DestinationsAwareCommand from pyvko.aspects.groups import Group from pyvko.entities.user import User diff --git a/src/justin/typer/sequence_command.py b/src/justin/typer/sequence_command.py index dc33cf1..ff49dcd 100644 --- a/src/justin/typer/sequence_command.py +++ b/src/justin/typer/sequence_command.py @@ -6,7 +6,7 @@ from justin.typer.base_commands.pattern_command import Extra from justin.shared.context import Context -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.models.exif import exif_sorted from justin.typer.base_commands.pattern_command import PatternCommand diff --git a/src/justin/typer/stage_command/abstracts/file_mover.py b/src/justin/typer/stage_command/abstracts/file_mover.py index d44772f..264b61c 100644 --- a/src/justin/typer/stage_command/abstracts/file_mover.py +++ b/src/justin/typer/stage_command/abstracts/file_mover.py @@ -1,9 +1,8 @@ from abc import ABC, abstractmethod from typing import Iterable, List, TYPE_CHECKING -from justin_utils.filesystem import File, RelativeFileset +from justin_utils.filesystem import File, RelativeFileset, PathBased -from justin.shared.filesystem import PathBased from justin.shared.metafiles.metafile import RootMetafile from justin.shared.models.photoset import Photoset from justin.typer.stage_command.checks.base import StageCheckError diff --git a/src/justin/typer/stage_command/checks/extracting/structure.py b/src/justin/typer/stage_command/checks/extracting/structure.py index 04181eb..d85e96b 100644 --- a/src/justin/typer/stage_command/checks/extracting/structure.py +++ b/src/justin/typer/stage_command/checks/extracting/structure.py @@ -5,8 +5,7 @@ from justin.typer.stage_command.abstracts.check import Check from justin.typer.stage_command.abstracts.extracting_check import ExtractingCheck -from justin.shared import filesystem -from justin.shared.filesystem import PathBased +from justin_utils.filesystem import PathBased, parse_paths from justin.shared.models.photoset import Photoset from justin.shared.structure import Structure, StructureVisitor, XorStructure, TopStructure from justin.typer.stage_command.problems.paths_problem import PathsProblem @@ -59,7 +58,7 @@ def get_problems(self, photoset: Photoset) -> List[PathsProblem]: def files_to_extract(self, photoset: Photoset) -> List[PathBased]: wrong_paths = self.__visitor.validate(photoset.path) - return filesystem.parse_paths(wrong_paths) + return parse_paths(wrong_paths) class _ValidateStructureVisitor(StructureVisitor[List[Path]]): diff --git a/src/justin/typer/stage_command/checks/simple/metafile_state.py b/src/justin/typer/stage_command/checks/simple/metafile_state.py index 7b7228f..4b81459 100644 --- a/src/justin/typer/stage_command/checks/simple/metafile_state.py +++ b/src/justin/typer/stage_command/checks/simple/metafile_state.py @@ -7,7 +7,7 @@ from justin.typer.stage_command.abstracts.simple_check import SimpleCheck from justin.typer.stage_command.problems.problem import Problem from justin.typer.stage_command.problems.path_problem import PathProblem -from justin.shared.filesystem import Folder +from justin_utils.filesystem import Folder from justin.shared.helpers.parts import folder_tree_parts from justin.shared.metafiles.metafile import PostMetafile, GroupMetafile, PostStatus, PersonMetafile, NoPostMetafile from justin.shared.models.photoset import Photoset diff --git a/src/justin/typer/stage_command/hooks/candidates.py b/src/justin/typer/stage_command/hooks/candidates.py index f0355eb..0bb9a41 100644 --- a/src/justin/typer/stage_command/hooks/candidates.py +++ b/src/justin/typer/stage_command/hooks/candidates.py @@ -1,8 +1,8 @@ +from justin_utils.filesystem import PathBased from typing import List from justin_utils.util import flat_map -from justin.shared.filesystem import PathBased from justin.shared.models.photoset import Photoset from justin.typer.stage_command.abstracts.file_mover import FileMover from justin.typer.stage_command.abstracts.hook import Hook diff --git a/src/justin/typer/stage_command/hooks/progress.py b/src/justin/typer/stage_command/hooks/progress.py index 320c410..62f6b4d 100644 --- a/src/justin/typer/stage_command/hooks/progress.py +++ b/src/justin/typer/stage_command/hooks/progress.py @@ -1,4 +1,4 @@ -from justin.shared.filesystem import File, RelativeFileset +from justin_utils.filesystem import File, RelativeFileset from justin.shared.metafiles.metafile import RootMetafile from justin.shared.models.photoset import Photoset from justin.typer.stage_command.abstracts.hook import Hook From e6bbca1228c64e3fe3ac559031a37affa0e97bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 6 Jul 2026 23:34:59 +0700 Subject: [PATCH 2/7] refactor: Removed sources.py and migrated to justin_utils --- src/justin/shared/models/photoset.py | 12 +- src/justin/shared/models/sources.py | 159 ------------------------- src/justin/typer/date_split_command.py | 3 +- 3 files changed, 8 insertions(+), 166 deletions(-) delete mode 100644 src/justin/shared/models/sources.py diff --git a/src/justin/shared/models/photoset.py b/src/justin/shared/models/photoset.py index 390896b..f58fa27 100644 --- a/src/justin/shared/models/photoset.py +++ b/src/justin/shared/models/photoset.py @@ -1,13 +1,13 @@ from pathlib import Path + +from justin_utils.filesystem import File, FolderBased, Folder +from justin_utils.sources import Source, parse_sources +from justin_utils.util import flat_map from typing import List, Self from uuid import UUID -from justin_utils.filesystem import File, FolderBased, Folder from justin.shared.helpers.parts import PartsMixin from justin.shared.metafiles.metafile import PhotosetMetafile -from justin.shared.models import sources -from justin.shared.models.sources import Source -from justin_utils import util class Photoset(FolderBased, PartsMixin): @@ -43,7 +43,7 @@ def parts(self) -> List['Photoset']: @property def sources(self) -> List[Source]: - sources_ = sources.parse_sources(self.folder.files) + sources_ = parse_sources(self.folder.files) return sources_ @@ -115,7 +115,7 @@ def results(self) -> List[File]: results_lists = [sub.flatten() for sub in possible_subtrees] - result = util.flat_map(results_lists) + result = flat_map(results_lists) return result diff --git a/src/justin/shared/models/sources.py b/src/justin/shared/models/sources.py deleted file mode 100644 index 46052e9..0000000 --- a/src/justin/shared/models/sources.py +++ /dev/null @@ -1,159 +0,0 @@ -from abc import abstractmethod -from functools import cached_property -from pathlib import Path - -from justin_utils.filesystem import Movable, File -from typing import List, Iterable - -from justin.shared.models.exif import parse_exif, Exif -from justin_utils import util, joins - - - -class Source(Movable): - @property - @abstractmethod - def mtime(self): - pass - - @property - @abstractmethod - def name(self) -> str: - pass - - @property - @abstractmethod - def exif(self) -> Exif: - pass - - @property - def stem(self) -> str: - return self.name - - def move(self, path: Path): - for file in self.files(): - file.move(path) - - def move_down(self, subfolder: str) -> None: - for file in self.files(): - file.move_down(subfolder) - - def move_up(self) -> None: - for file in self.files(): - file.move_up() - - def copy(self, path: Path) -> None: - for file in self.files(): - file.copy(path) - - @property - def size(self): - return sum([f.size for f in self.files()]) - - @abstractmethod - def files(self) -> List[File]: - pass - - def __str__(self) -> str: - return f"{type(self).__name__}: {self.name}" - - def __repr__(self) -> str: - return f"{type(self).__name__}: {self.name}" - - -class InternalMetadataSource(Source): - TYPES = [ - ".jpg", - ".tif", - ".dng", - ".heic", - ] - - def __init__(self, file: File): - super().__init__() - - self.__file = file - - @property - def mtime(self): - return self.__file.mtime - - @property - def name(self): - return self.__file.stem - - def files(self) -> List[File]: - return [self.__file] - - @cached_property - def exif(self) -> Exif: - return parse_exif(self.__file.path) - - -class ExternalMetadataSource(Source): - RAW_TYPES = [ - ".nef", # Nikon - ".raf", # Fujifilm GFX - ".arw", # Sony - ] - - METADATA_TYPES = [ - ".xmp", - ] - - def __init__(self, raw: File, metadata: File): - super().__init__() - - assert raw.extension != ".jpg" - - if metadata is not None: - assert raw.stem == metadata.stem - - self.raw = raw - self.metadata = metadata - - @property - def mtime(self): - if self.metadata is not None: - return self.metadata.mtime - else: - return -1 - - @property - def name(self): - return self.raw.stem - - @cached_property - def exif(self) -> Exif: - return parse_exif(self.raw.path) - - def files(self) -> List[File]: - files = [self.raw] - - if self.metadata is not None: - files.append(self.metadata) - - return files - - -def parse_sources(seq: Iterable[File]) -> List[Source]: - split = list(util.split_by_predicates( - seq, - lambda file: file.extension.lower() in ExternalMetadataSource.RAW_TYPES, - lambda file: file.extension.lower() in ExternalMetadataSource.METADATA_TYPES, - lambda file: file.extension.lower() in InternalMetadataSource.TYPES - )) - - join = joins.left( - split[0], - split[1], - lambda raw, xmp: raw.stem == xmp.stem - ) - - raws = [ExternalMetadataSource(raw, meta) for raw, meta in join] - jpegs = [InternalMetadataSource(jpeg) for jpeg in split[2]] - - # noinspection PyTypeChecker - sources = raws + jpegs - - return sources diff --git a/src/justin/typer/date_split_command.py b/src/justin/typer/date_split_command.py index cc63eb6..3f1677b 100644 --- a/src/justin/typer/date_split_command.py +++ b/src/justin/typer/date_split_command.py @@ -1,5 +1,7 @@ from datetime import timedelta from pathlib import Path + +from justin_utils.sources import parse_sources from typing import Annotated, Iterable from typing import List @@ -10,7 +12,6 @@ from justin.typer.base_commands.pattern_command import Extra from justin.shared.context import Context from justin_utils.filesystem import Folder -from justin.shared.models.sources import parse_sources from justin.typer.base_commands.pattern_command import PatternCommand From 61211d8e4de1eea85c1ca87cd2b0ab0398fbd9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 6 Jul 2026 23:35:31 +0700 Subject: [PATCH 3/7] fix: Tuned upload batch size --- src/justin/typer/upload_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/justin/typer/upload_command.py b/src/justin/typer/upload_command.py index 740c61f..7c483ba 100644 --- a/src/justin/typer/upload_command.py +++ b/src/justin/typer/upload_command.py @@ -642,7 +642,7 @@ def __get_album(community: Albums, folder: Folder, params: UploadParams) -> Albu print(f"Uploading {file_count} photos...") - batch_size = 10 + batch_size = 5 while not_uploaded_files: try: From d68167d8599dc04267deea84d3758ef69f940ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Mon, 6 Jul 2026 23:36:06 +0700 Subject: [PATCH 4/7] chore: Updated typer version It allows empty parameters list --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fae2ed5..fe581d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ "google-api-python-client", "google-auth-oauthlib", "frozendict", - "typer[all]>=0.9.0", + "typer[all]>=0.26.8", # "face_recognition", # Опциональная зависимость, может быть добавлена при необходимости "coverage" ] From 117b06d9c86399ac1045299ef10ee3b608386a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Tue, 7 Jul 2026 02:04:51 +0700 Subject: [PATCH 5/7] fix: Dropped usage of removed api call --- src/justin/typer/web_sync_command.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/justin/typer/web_sync_command.py b/src/justin/typer/web_sync_command.py index 02e3b4e..981aa79 100644 --- a/src/justin/typer/web_sync_command.py +++ b/src/justin/typer/web_sync_command.py @@ -102,8 +102,6 @@ def handle_my_people(self, my_people_folder: Folder, extra: Extra) -> None: print("Syncing my people...") - all_sent = True - for person_folder in my_people_folder.subfolders: if not PersonMetafile.has(person_folder): continue @@ -140,11 +138,6 @@ def handle_my_people(self, my_people_folder: Folder, extra: Extra) -> None: else: print(f" {publish_count}/{total_count} sent.") - all_sent = False - - if all_sent: - post.like() - def handle_timelapse(self, timelapse_folder: Folder, extra: Extra) -> None: pass From 78ba62efa4d2f38fc52ec817fe3e78855092b8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=94=D1=8C=D1=8F=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Tue, 7 Jul 2026 02:05:42 +0700 Subject: [PATCH 6/7] fix: Suppressed error for web syncing cullen sets --- src/justin/typer/web_sync_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/justin/typer/web_sync_command.py b/src/justin/typer/web_sync_command.py index 981aa79..3857841 100644 --- a/src/justin/typer/web_sync_command.py +++ b/src/justin/typer/web_sync_command.py @@ -144,6 +144,9 @@ def handle_timelapse(self, timelapse_folder: Folder, extra: Extra) -> None: def handle_drive(self, drive_folder: Folder, extra: Extra) -> None: pass + def handle_cullen(self, cullen_folder: Folder, extra: Extra) -> None: + pass # cullen is being not removed for now for culling training purposes + def __warmup_cache(self, group_id: int): if group_id in self.__cache: return From ea2c0ee82f805e2784daf1fd2722f3395fa97432 Mon Sep 17 00:00:00 2001 From: Igor Djachenko Date: Tue, 7 Jul 2026 14:28:25 +0700 Subject: [PATCH 7/7] chore: Removed extra blank line --- src/justin/typer/upload_command.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/justin/typer/upload_command.py b/src/justin/typer/upload_command.py index 7c483ba..4fa0e33 100644 --- a/src/justin/typer/upload_command.py +++ b/src/justin/typer/upload_command.py @@ -466,7 +466,6 @@ def handle_cullen(self, cullen_folder: Folder, extra: Extra) -> None: - # endregion upload strategies # region event