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.
- justin_utils-0.1.0/.github/workflows/integration.yml +40 -0
- justin_utils-0.1.0/.github/workflows/release.yml +40 -0
- justin_utils-0.1.0/.github/workflows/tests.yml +13 -0
- justin_utils-0.1.0/.gitignore +137 -0
- justin_utils-0.1.0/CHANGELOG.md +7 -0
- justin_utils-0.1.0/CLAUDE.md +55 -0
- justin_utils-0.1.0/PKG-INFO +45 -0
- justin_utils-0.1.0/README.md +1 -0
- justin_utils-0.1.0/pyproject.toml +92 -0
- justin_utils-0.1.0/src/justin_utils/__init__.py +0 -0
- justin_utils-0.1.0/src/justin_utils/cd.py +18 -0
- justin_utils-0.1.0/src/justin_utils/cli.py +143 -0
- justin_utils-0.1.0/src/justin_utils/data.py +137 -0
- justin_utils-0.1.0/src/justin_utils/exif.py +117 -0
- justin_utils-0.1.0/src/justin_utils/filesystem.py +621 -0
- justin_utils-0.1.0/src/justin_utils/joins.py +61 -0
- justin_utils-0.1.0/src/justin_utils/json_migration.py +51 -0
- justin_utils-0.1.0/src/justin_utils/parts.py +240 -0
- justin_utils-0.1.0/src/justin_utils/pylinq.py +166 -0
- justin_utils-0.1.0/src/justin_utils/singleton.py +25 -0
- justin_utils-0.1.0/src/justin_utils/sources.py +158 -0
- justin_utils-0.1.0/src/justin_utils/subfolder.py +25 -0
- justin_utils-0.1.0/src/justin_utils/time_formatter.py +24 -0
- justin_utils-0.1.0/src/justin_utils/transfer.py +60 -0
- justin_utils-0.1.0/src/justin_utils/util.py +326 -0
|
@@ -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,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,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)
|