justin-utils 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
justin_utils/cd.py ADDED
@@ -0,0 +1,18 @@
1
+ import os
2
+ from contextlib import contextmanager
3
+ from pathlib import Path
4
+
5
+
6
+ @contextmanager
7
+ def cd(new_path: Path):
8
+ assert new_path.exists()
9
+ assert new_path.is_dir()
10
+
11
+ previous_path = Path.cwd()
12
+
13
+ os.chdir(str(new_path.expanduser()))
14
+
15
+ try:
16
+ yield
17
+ finally:
18
+ os.chdir(str(previous_path))
justin_utils/cli.py ADDED
@@ -0,0 +1,143 @@
1
+ from abc import abstractmethod
2
+ from argparse import ArgumentParser, Namespace
3
+ from dataclasses import dataclass, asdict
4
+ from enum import Enum
5
+ from typing import Any, Iterable, Dict, ClassVar, Type, Callable, List, TypeVar
6
+
7
+ from justin_utils.util import is_distinct
8
+
9
+ Context = Any
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ @dataclass
15
+ class Parameter:
16
+ class Action(str, Enum):
17
+ STORE_TRUE = "store_true"
18
+
19
+ name: str = None
20
+ flags: Iterable[str] = None
21
+ nargs: str = None
22
+ default: Any = None
23
+ action: Action = None
24
+ type: Type | Callable[[str], T] = None
25
+ choices: Iterable[T] = None
26
+
27
+ not_kw_fields: ClassVar[Iterable[str]] = [
28
+ "name",
29
+ "flags",
30
+ ]
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.flags is None:
34
+ self.flags = ()
35
+ else:
36
+ self.flags = tuple(self.flags)
37
+
38
+ if self.action is not None:
39
+ self.action = self.action.value
40
+
41
+ @property
42
+ def name_or_flags(self) -> Iterable[str]:
43
+ # noinspection PyTypeChecker
44
+ return tuple(i for i in (self.name,) + self.flags if i)
45
+
46
+ @property
47
+ def params(self) -> Dict[str, Any]:
48
+ return {k: v for k, v in asdict(self).items() if k not in Parameter.not_kw_fields and v}
49
+
50
+
51
+ class Action:
52
+ def configure_subparser(self, subparser: ArgumentParser) -> None:
53
+ pass
54
+
55
+ @property
56
+ def parameters(self) -> List[Parameter]:
57
+ return []
58
+
59
+ @abstractmethod
60
+ def perform(self, args: Namespace, context: Context) -> None:
61
+ pass
62
+
63
+
64
+ class Command:
65
+ def __init__(self, name: str, actions: Iterable[Action], allowed_same_parameters: Iterable[str] = ()) -> None:
66
+ super().__init__()
67
+
68
+ name = name.strip()
69
+
70
+ assert " " not in name
71
+
72
+ params_set = set()
73
+
74
+ for action in actions:
75
+ for param in action.parameters:
76
+ param_name = param.name_or_flags
77
+
78
+ assert param_name not in params_set or any(i in allowed_same_parameters for i in param_name)
79
+
80
+ params_set.add(param_name)
81
+
82
+ self.__name = name
83
+ self.__actions = actions
84
+
85
+ @property
86
+ def name(self) -> str:
87
+ return self.__name
88
+
89
+ def configure_parser(self, parser_adder) -> None:
90
+ subparser: ArgumentParser = parser_adder.add_parser(self.__name)
91
+
92
+ self.configure_subparser(subparser)
93
+
94
+ self.__setup_callback(subparser)
95
+
96
+ def configure_subparser(self, subparser: ArgumentParser) -> None:
97
+ params_set = set()
98
+
99
+ for action in self.__actions:
100
+ for parameter in action.parameters:
101
+ if parameter.name_or_flags in params_set:
102
+ continue
103
+
104
+ subparser.add_argument(*parameter.name_or_flags, **parameter.params)
105
+ params_set.add(parameter.name_or_flags)
106
+
107
+ action.configure_subparser(subparser)
108
+
109
+ def __setup_callback(self, parser: ArgumentParser) -> None:
110
+ parser.set_defaults(command=self)
111
+
112
+ def __call__(self, args: Namespace, context: Context) -> None:
113
+ for action in self.__actions:
114
+ action.perform(args, context)
115
+
116
+
117
+ class App:
118
+ def __init__(self, commands: Iterable[Command], context: Context = None) -> None:
119
+ super().__init__()
120
+
121
+ assert is_distinct([command.name for command in commands])
122
+
123
+ self.__commands = commands
124
+ self.__context = context
125
+
126
+ def run(self, args: Iterable[str] = None) -> None:
127
+ parser = ArgumentParser()
128
+
129
+ parser_adder = parser.add_subparsers()
130
+
131
+ for command in self.__commands:
132
+ command.configure_parser(parser_adder)
133
+
134
+ namespace = parser.parse_args(args)
135
+
136
+ if not hasattr(namespace, "command"):
137
+ print("No parameters is bad")
138
+ elif not namespace.command:
139
+ print("No command found.")
140
+ elif not isinstance(namespace.command, Command):
141
+ print("Wrong command class")
142
+ else:
143
+ namespace.command(namespace, self.__context)
justin_utils/data.py ADDED
@@ -0,0 +1,137 @@
1
+ from datetime import timedelta
2
+ from enum import Enum
3
+ from functools import cache
4
+ from typing import List, Optional, Union
5
+
6
+
7
+ class DataSize:
8
+ class Unit(Enum):
9
+ BYTE = (2 ** 0, "B")
10
+ KILOBYTE = (2 ** 10, "KB")
11
+ MEGABYTE = (2 ** 20, "MB")
12
+ GIGABYTE = (2 ** 30, "GB")
13
+
14
+ def __init__(self, size: int, acronym: str) -> None:
15
+ super().__init__()
16
+
17
+ self.size = size
18
+ self.acronym = acronym
19
+
20
+ @staticmethod
21
+ @cache
22
+ def sorted_units() -> List['DataSize.Unit']:
23
+ return sorted([
24
+ DataSize.Unit.BYTE,
25
+ DataSize.Unit.KILOBYTE,
26
+ DataSize.Unit.MEGABYTE,
27
+ DataSize.Unit.GIGABYTE,
28
+ ],
29
+ key=lambda u: u.size,
30
+ reverse=True)
31
+
32
+ @staticmethod
33
+ def for_value(value: int) -> 'DataSize.Unit':
34
+ for unit in DataSize.Unit.sorted_units():
35
+ if value > unit.size:
36
+ return unit
37
+
38
+ return DataSize.Unit.BYTE
39
+
40
+ def __init__(self, size_in_bytes: int) -> None:
41
+ super().__init__()
42
+
43
+ assert isinstance(size_in_bytes, int)
44
+
45
+ self.__bytes = size_in_bytes
46
+
47
+ def __str__(self) -> str:
48
+ return self.formatted()
49
+
50
+ def formatted(self) -> str:
51
+ unit = DataSize.Unit.for_value(self.__bytes)
52
+
53
+ converted_size = round(self.__bytes / unit.size, 2)
54
+
55
+ result = f"{converted_size:.2f} {unit.acronym}"
56
+
57
+ return result
58
+
59
+ def canonic_value(self) -> int:
60
+ return round(self.__as_unit(DataSize.Unit.BYTE))
61
+
62
+ def __as_unit(self, unit: Unit) -> float:
63
+ return self.__bytes / unit.size
64
+
65
+ def add_bytes(self, bytes_: int) -> None:
66
+ self.__bytes += bytes_
67
+
68
+ @classmethod
69
+ def __from_unit(cls, size: float, unit: Unit) -> 'DataSize':
70
+ return DataSize(round(size * unit.size))
71
+
72
+ @classmethod
73
+ def from_bytes(cls, size: int) -> 'DataSize':
74
+ return DataSize.__from_unit(size, DataSize.Unit.BYTE)
75
+
76
+ def __truediv__(self, other):
77
+ if isinstance(other, timedelta):
78
+ return DataSpeed(self, other)
79
+ elif isinstance(other, DataSpeed):
80
+ speed_canonic = other.canonic_value()
81
+
82
+ if speed_canonic is None:
83
+ return None
84
+
85
+ self_canonic = self.canonic_value()
86
+
87
+ time_in_seconds = self_canonic / speed_canonic
88
+
89
+ return timedelta(seconds=time_in_seconds)
90
+ else:
91
+ assert False
92
+
93
+ def __sub__(self, other) -> 'DataSize':
94
+ if isinstance(other, DataSize):
95
+ return DataSize.from_bytes(self.canonic_value() - other.canonic_value())
96
+ else:
97
+ assert False
98
+
99
+ def __lt__(self, other: Union['DataSize', int]) -> bool:
100
+ if isinstance(other, DataSize):
101
+ other = other.__bytes
102
+
103
+ return self.__bytes < other
104
+
105
+
106
+ class DataSpeed:
107
+ def __init__(self, amount: DataSize, time: timedelta) -> None:
108
+ super().__init__()
109
+
110
+ assert isinstance(amount, DataSize)
111
+ assert isinstance(time, timedelta)
112
+
113
+ self.__amount = amount
114
+ self.__time = time
115
+
116
+ def __str__(self) -> str:
117
+ return self.formatted()
118
+
119
+ def formatted(self) -> str:
120
+ speed = self.canonic_value()
121
+
122
+ if speed is None:
123
+ return "N/A"
124
+
125
+ unit = DataSize.Unit.for_value(round(speed))
126
+
127
+ converted_size = speed / unit.size
128
+
129
+ result = f"{converted_size:.2f} {unit.acronym}/s"
130
+
131
+ return result
132
+
133
+ def canonic_value(self) -> Optional[float]:
134
+ if self.__time.total_seconds() == 0:
135
+ return None
136
+
137
+ return self.__amount.canonic_value() / self.__time.total_seconds()
justin_utils/exif.py ADDED
@@ -0,0 +1,117 @@
1
+ from abc import abstractmethod, ABC
2
+ from datetime import datetime
3
+ from functools import cache
4
+ from pathlib import Path
5
+ from typing import Iterable
6
+ from typing_extensions import Self
7
+
8
+ from exif import Image # type: ignore[import-untyped]
9
+
10
+ from PIL import ExifTags
11
+ from PIL.Image import Exif as PilExif
12
+ from PIL import Image as ImageModule
13
+
14
+ class Exif(ABC):
15
+ @property
16
+ @abstractmethod
17
+ def date_taken(self) -> datetime:
18
+ pass
19
+
20
+ def __lt__(self, other: 'Exif') -> bool:
21
+ return self.date_taken < other.date_taken
22
+
23
+ @classmethod
24
+ @abstractmethod
25
+ def from_path(cls, path: Path) -> Self:
26
+ pass
27
+
28
+
29
+ class PillowExif(Exif):
30
+ __reverse_mapping = {v: k for k, v in ExifTags.TAGS.items()}
31
+
32
+ @property
33
+ @cache
34
+ def date_taken(self) -> datetime:
35
+ date_str = self.__get_tag_value("DateTimeOriginal") or self.__get_tag_value("DateTime")
36
+ assert date_str is not None
37
+ return datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
38
+
39
+ def __get_tag_value(self, tag: str) -> str | None:
40
+ return self.source_exif.get(PillowExif.__reverse_mapping[tag])
41
+
42
+ def __init__(self, exif: PilExif) -> None:
43
+ super().__init__()
44
+
45
+ self.source_exif = exif
46
+
47
+ @classmethod
48
+ def from_path(cls, path: Path) -> Self:
49
+ return cls(ImageModule.open(path).getexif())
50
+
51
+
52
+ class NativeExif(Exif):
53
+ @property
54
+ @cache
55
+ def date_taken(self) -> datetime:
56
+ if hasattr(self.source_exif, "datetime_original"):
57
+ return datetime.strptime(
58
+ self.source_exif.datetime_original,
59
+ "%Y:%m:%d %H:%M:%S"
60
+ )
61
+ elif hasattr(self.source_exif, "datetime_digitized"):
62
+ return datetime.strptime(
63
+ self.source_exif.datetime_digitized,
64
+ "%Y:%m:%d %H:%M:%S"
65
+ )
66
+ else:
67
+ assert False
68
+
69
+ def __init__(self, exif: Image) -> None:
70
+ super().__init__()
71
+
72
+ self.source_exif = exif
73
+
74
+ @classmethod
75
+ def from_path(cls, path: Path) -> Self:
76
+ with path.open("rb") as image_file:
77
+ my_image = Image(image_file)
78
+
79
+ return cls(my_image)
80
+
81
+
82
+ def parse_exif(path: Path) -> Exif | None:
83
+ if path is None:
84
+ return None
85
+
86
+ if path.is_dir():
87
+ return None
88
+
89
+ suffix = path.suffix.lower()
90
+
91
+ exif_class: type[PillowExif] | type[NativeExif]
92
+
93
+ if suffix in [".nef", ".dng", ]:
94
+ exif_class = PillowExif
95
+ elif suffix in [".jpg", ]:
96
+ exif_class = NativeExif
97
+ else:
98
+ return None
99
+
100
+ return exif_class.from_path(path)
101
+
102
+
103
+ def exif_sorted(seq: Iterable[Path]) -> Iterable[Path]:
104
+ class Comparator:
105
+ def __init__(self, path: Path) -> None:
106
+ super().__init__()
107
+
108
+ self.exif = parse_exif(path)
109
+ self.name = path.name
110
+
111
+ def __lt__(self, other: 'Comparator') -> bool:
112
+ if other.exif and self.exif:
113
+ return self.exif.date_taken < other.exif.date_taken
114
+
115
+ return self.name < other.name
116
+
117
+ return sorted(seq, key=Comparator)