justin-utils 0.1.0__tar.gz

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.
@@ -0,0 +1,40 @@
1
+ name: Integration
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ integration:
10
+ uses: djachenko/repokit/.github/workflows/python-integration.yml@0.5
11
+ secrets: inherit
12
+
13
+ publish:
14
+ needs: integration
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ id-token: write
18
+
19
+ steps:
20
+ - uses: actions/download-artifact@v8
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ - name: Publish to TestPyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ repository-url: https://test.pypi.org/legacy/
29
+ skip-existing: true
30
+
31
+ - name: Smoke test
32
+ run: |
33
+ PACKAGE=$(ls dist/*.whl | head -1 | xargs basename | sed 's/-[0-9].*//')
34
+ VERSION=$(ls dist/*.tar.gz | sed 's/.*-\(.*\)\.tar\.gz/\1/')
35
+ for i in {1..5}; do
36
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $PACKAGE==$VERSION && break
37
+ echo "Attempt $i failed, retrying in 10s..."
38
+ sleep 10
39
+ done
40
+ python -c "import $PACKAGE"
@@ -0,0 +1,40 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ release:
10
+ uses: djachenko/repokit/.github/workflows/python-release.yml@0.5
11
+ secrets: inherit
12
+ permissions:
13
+ contents: write
14
+
15
+ publish:
16
+ needs: release
17
+ if: needs.release.outputs.released == 'true'
18
+ runs-on: ubuntu-latest
19
+ permissions:
20
+ id-token: write
21
+
22
+ steps:
23
+ - uses: actions/download-artifact@v8
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
30
+
31
+ - name: Smoke test
32
+ run: |
33
+ PACKAGE=$(ls dist/*.whl | head -1 | xargs basename | sed 's/-[0-9].*//')
34
+ VERSION=$(ls dist/*.tar.gz | sed 's/.*-\(.*\)\.tar\.gz/\1/')
35
+ for i in {1..5}; do
36
+ pip install $PACKAGE==$VERSION && break
37
+ echo "Attempt $i failed, retrying in 10s..."
38
+ sleep 10
39
+ done
40
+ python -c "import $PACKAGE"
@@ -0,0 +1,13 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ tags-ignore:
8
+ - '**'
9
+
10
+ jobs:
11
+ tests:
12
+ uses: djachenko/repokit/.github/workflows/python-tests.yml@0.5
13
+ secrets: inherit
@@ -0,0 +1,137 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+
53
+ # Translations
54
+ *.mo
55
+ *.pot
56
+
57
+ # Django stuff:
58
+ *.log
59
+ local_settings.py
60
+ db.sqlite3
61
+ db.sqlite3-journal
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # pipenv
87
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
88
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
89
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
90
+ # install all needed dependencies.
91
+ #Pipfile.lock
92
+
93
+ # celery beat schedule file
94
+ celerybeat-schedule
95
+
96
+ # SageMath parsed files
97
+ *.sage.py
98
+
99
+ # Environments
100
+ .env
101
+ .venv
102
+ env/
103
+ venv/
104
+ ENV/
105
+ env.bak/
106
+ venv.bak/
107
+
108
+ # Spyder project settings
109
+ .spyderproject
110
+ .spyproject
111
+
112
+ # Rope project settings
113
+ .ropeproject
114
+
115
+ # mkdocs documentation
116
+ /site
117
+
118
+ # mypy
119
+ .mypy_cache/
120
+ .dmypy.json
121
+ dmypy.json
122
+
123
+ # Pyre type checker
124
+ .pyre/
125
+
126
+ # Session memory
127
+ memory/
128
+
129
+ # IDE
130
+ .idea/
131
+
132
+ # Claude Code
133
+ .claude/
134
+
135
+ # Repo dump
136
+ justin_utils.txt
137
+ .repokit
@@ -0,0 +1,7 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v0.1.0 (2026-06-25)
6
+
7
+ - Initial Release
@@ -0,0 +1,55 @@
1
+ # justin_utils — Project Guide
2
+
3
+ ## Что это
4
+
5
+ Общие утилиты Python-экосистемы. Библиотека без зависимостей, которую можно установить отдельно и использовать в любом проекте экосистемы.
6
+
7
+ ---
8
+
9
+ ## Место в экосистеме
10
+
11
+ ```
12
+ justin ──→ justin_utils
13
+ pyvko ──→ justin_utils (планируется)
14
+ ```
15
+
16
+ Цель: вобрать весь переиспользуемый код из `justin` и `pyvko`, чтобы шарить между проектами без лишних зависимостей.
17
+
18
+ ---
19
+
20
+ ## Текущие модули
21
+
22
+ | Модуль | Содержимое |
23
+ |--------|------------|
24
+ | `filesystem.py` | `Folder`, `File` — обёртки над `Path` с удобным доступом к подпапкам |
25
+ | `parts.py` | CLI-утилита `parts` |
26
+ | `subfolder.py` | CLI-утилита `sf` |
27
+ | `transfer.py` | `TransferSpeedMeter`, `TransferTimeEstimator` |
28
+ | `time_formatter.py` | Форматирование времени |
29
+ | `data.py` | `DataSize` |
30
+ | `exif.py` | EXIF-утилиты |
31
+ | `singleton.py` | Singleton паттерн |
32
+ | `joins.py`, `pylinq.py` | LINQ-подобные операции над коллекциями |
33
+ | `json_migration.py` | Утилиты для JSON-миграций |
34
+
35
+ ---
36
+
37
+ ## Текущее состояние
38
+
39
+ - Версия `0.0.1` — никогда не апдейтилась с релиза
40
+ - 26 незакоммиченных файлов — давно не синхронизировалось с реальным состоянием
41
+ - Установка в других проектах: `pip install -e ../justin_utils`
42
+
43
+ ---
44
+
45
+ ## Что нужно сделать
46
+
47
+ - Разобрать 26 dirty-файлов
48
+ - При следующей работе с justin или pyvko — выявить код который стоит сюда перенести
49
+ - Рассмотреть разбивку на отдельные install-extras чтобы не тянуть всё целиком
50
+
51
+ ---
52
+
53
+ ## Git
54
+
55
+ Semantic commits: `feat:`, `fix:`, `refactor:`, `chore:`
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: justin_utils
3
+ Version: 0.1.0
4
+ Summary: Utilities for the Justin project
5
+ Project-URL: Homepage, https://github.com/djachenko/justin_utils
6
+ Project-URL: Repository, https://github.com/djachenko/justin_utils
7
+ Project-URL: Issues, https://github.com/djachenko/justin_utils/issues
8
+ Author: Igor Djachenko
9
+ License-Expression: MIT
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: justin-utils[cli]
12
+ Requires-Dist: justin-utils[exif]
13
+ Requires-Dist: justin-utils[filesystem]
14
+ Requires-Dist: justin-utils[joins]
15
+ Requires-Dist: justin-utils[other]
16
+ Requires-Dist: justin-utils[parts]
17
+ Requires-Dist: justin-utils[pylinq]
18
+ Requires-Dist: justin-utils[singleton]
19
+ Requires-Dist: justin-utils[sources]
20
+ Requires-Dist: justin-utils[util]
21
+ Requires-Dist: typing-extensions
22
+ Provides-Extra: cli
23
+ Provides-Extra: exif
24
+ Requires-Dist: exif; extra == 'exif'
25
+ Requires-Dist: pillow; extra == 'exif'
26
+ Provides-Extra: filesystem
27
+ Provides-Extra: joins
28
+ Provides-Extra: other
29
+ Provides-Extra: parts
30
+ Provides-Extra: pylinq
31
+ Provides-Extra: release
32
+ Requires-Dist: build; extra == 'release'
33
+ Requires-Dist: python-semantic-release; extra == 'release'
34
+ Provides-Extra: singleton
35
+ Provides-Extra: sources
36
+ Requires-Dist: exif; extra == 'sources'
37
+ Requires-Dist: pillow; extra == 'sources'
38
+ Provides-Extra: test
39
+ Requires-Dist: mypy; extra == 'test'
40
+ Requires-Dist: pytest; extra == 'test'
41
+ Requires-Dist: ruff; extra == 'test'
42
+ Provides-Extra: util
43
+ Description-Content-Type: text/markdown
44
+
45
+ justin_utils
@@ -0,0 +1 @@
1
+ justin_utils
@@ -0,0 +1,92 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/justin_utils"]
7
+
8
+ [project]
9
+ name = "justin_utils"
10
+ version = "0.1.0"
11
+ description = "Utilities for the Justin project"
12
+ readme = "README.md"
13
+ license = "MIT"
14
+ authors = [{ name = "Igor Djachenko" }]
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ "typing_extensions",
18
+ "justin_utils[util]",
19
+ "justin_utils[joins]",
20
+ "justin_utils[singleton]",
21
+ "justin_utils[pylinq]",
22
+ "justin_utils[other]",
23
+ "justin_utils[filesystem]",
24
+ "justin_utils[cli]",
25
+ "justin_utils[parts]",
26
+ "justin_utils[exif]",
27
+ "justin_utils[sources]",
28
+ ]
29
+
30
+ [project.scripts]
31
+ parts = "justin_utils.parts:__run"
32
+ sf = "justin_utils.subfolder:__run"
33
+
34
+ [project.optional-dependencies]
35
+ util = []
36
+ joins = []
37
+ singleton = []
38
+ pylinq = []
39
+ other = [
40
+ "justin_utils[singleton]",
41
+ ]
42
+ filesystem = [
43
+ "justin_utils[other]",
44
+ ]
45
+ cli = [
46
+ "justin_utils[util]",
47
+ ]
48
+ parts = [
49
+ "justin_utils[cli]",
50
+ ]
51
+ exif = [
52
+ "Pillow",
53
+ "exif",
54
+ ]
55
+ sources = [
56
+ "justin_utils[util]",
57
+ "justin_utils[joins]",
58
+ "justin_utils[exif]",
59
+ "justin_utils[filesystem]",
60
+ ]
61
+ test = [
62
+ "pytest",
63
+ "ruff",
64
+ "mypy",
65
+ ]
66
+ release = [
67
+ "build",
68
+ "python-semantic-release",
69
+ ]
70
+
71
+ [project.urls]
72
+ Homepage = "https://github.com/djachenko/justin_utils"
73
+ Repository = "https://github.com/djachenko/justin_utils"
74
+ Issues = "https://github.com/djachenko/justin_utils/issues"
75
+
76
+ [tool.mypy]
77
+ python_version = "3.10"
78
+
79
+ [[tool.mypy.overrides]]
80
+ module = [
81
+ "justin_utils.pylinq",
82
+ "justin_utils.cli",
83
+ "justin_utils.json_migration",
84
+ ]
85
+ ignore_errors = true
86
+
87
+ [tool.semantic_release]
88
+ version_toml = ["pyproject.toml:project.version"]
89
+ branch = "master"
90
+ commit_message = "chore(release): v{version} [no ci]"
91
+ major_on_zero = false
92
+ allow_zero_version = true
File without changes
@@ -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))
@@ -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)