diff options
| author | Simeon Simeonov | 2026-04-16 17:14:23 +0200 |
|---|---|---|
| committer | Simeon Simeonov | 2026-04-16 17:14:23 +0200 |
| commit | 3e4185913b584f5cc9a54f16f3c223861e11ec18 (patch) | |
| tree | 33877fd0f3e34e13d911d98641227eb5d03fbb9d | |
| parent | bba82a86b137c73832ad7b6a397d3db24fcc625f (diff) | |
Introduce a new marco - %e and migrate the project from GitHub to Codeberg2.2.0
| -rw-r--r-- | .ruff.toml | 8 | ||||
| -rw-r--r-- | CHANGELOG.md | 29 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | pyproject.toml | 54 | ||||
| -rw-r--r-- | setup.cfg | 45 | ||||
| -rw-r--r-- | src/etoolkit/__init__.py | 6 | ||||
| -rw-r--r-- | src/etoolkit/__main__.py | 18 | ||||
| -rw-r--r-- | src/etoolkit/etoolkit.py | 19 | ||||
| -rw-r--r-- | tests/conftest.py | 30 | ||||
| -rw-r--r-- | tests/test_cli.py | 11 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance.py | 2 | ||||
| -rw-r--r-- | tests/test_envtoolkit_instance_static.py | 2 |
12 files changed, 126 insertions, 116 deletions
| @@ -1,11 +1,11 @@ | |||
| 1 | cache-dir = "~/.cache/ruff" | 1 | cache-dir = "~/.cache/ruff" |
| 2 | indent-width = 4 | 2 | indent-width = 4 |
| 3 | line-length = 79 | 3 | line-length = 79 |
| 4 | target-version = "py312" | 4 | target-version = "py310" |
| 5 | 5 | ||
| 6 | [lint] | 6 | [lint] |
| 7 | select = ["ALL"] | 7 | select = ["ALL"] |
| 8 | ignore = ["ANN", "COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D401", "D403", "D415", "ERA001", "FBT001", "FBT002", "PTH111", "RUF012", "RUF013", "S101", "TRY300", "BLE001", "UP020", "C901", "D200", "D402", "EM101", "EM102", "FBT003", "INP001", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"] | 8 | ignore = ["COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400", "D401", "D403", "D415", "ERA001", "FBT001", "FBT002", "PTH111", "RUF012", "RUF013", "S101", "TRY300", "BLE001", "UP020", "C901", "D200", "D402", "EM101", "EM102", "FBT003", "INP001", "PLR0912", "PLR0913", "PLR0915", "PLR2004", "PLW2901", "S603", "T201", "TRY003", "TRY400"] |
| 9 | # D105 - Missing docstring in magic method | 9 | # D105 - Missing docstring in magic method |
| 10 | # D200 - One-line docstring should fit on one line | 10 | # D200 - One-line docstring should fit on one line |
| 11 | # D203 - 1 blank line required before class docstring | 11 | # D203 - 1 blank line required before class docstring |
| @@ -42,6 +42,10 @@ ignore = ["ANN", "COM812", "D105", "D202", "D203", "D205", "D211", "D212", "D400 | |||
| 42 | fixable = ["ALL"] | 42 | fixable = ["ALL"] |
| 43 | unfixable = [] | 43 | unfixable = [] |
| 44 | 44 | ||
| 45 | [lint.per-file-ignores] | ||
| 46 | "tests/*.py" = ["ANN"] # Do not require annotations for tests | ||
| 47 | |||
| 48 | |||
| 45 | [format] | 49 | [format] |
| 46 | # Like Black, use double quotes for strings. | 50 | # Like Black, use double quotes for strings. |
| 47 | quote-style = "single" | 51 | quote-style = "single" |
diff --git a/CHANGELOG.md b/CHANGELOG.md index 916a9a2..fe4373f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md | |||
| @@ -1,17 +1,28 @@ | |||
| 1 | # Changelog | 1 | # Changelog |
| 2 | 2 | ||
| 3 | ## [2.1.0](https://github.com/blackm0re/etoolkit/tree/2.1.0) (2024-12-26) | 3 | ## [2.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.2.0) (2026-04-16) |
| 4 | 4 | ||
| 5 | [Full Changelog](https://github.com/blackm0re/etoolkit/compare/2.0.0...2.1.0) | 5 | [Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.1.0...2.2.0) |
| 6 | |||
| 7 | **Changes:** | ||
| 8 | |||
| 9 | - new *%e* macro - current env. var. value (for the same key) | ||
| 10 | |||
| 11 | - migrated the project from GitHub to Codeberg | ||
| 12 | |||
| 13 | |||
| 14 | ## [2.1.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.1.0) (2024-12-26) | ||
| 15 | |||
| 16 | [Full Changelog](https://codeberg.org/sgs/etoolkit/compare/2.0.0...2.1.0) | ||
| 6 | 17 | ||
| 7 | **Changes:** | 18 | **Changes:** |
| 8 | 19 | ||
| 9 | - add encrypted values to *ETOOLKIT_SENSITIVE* implicitly | 20 | - add encrypted values to *ETOOLKIT_SENSITIVE* implicitly |
| 10 | 21 | ||
| 11 | 22 | ||
| 12 | ## [2.0.0](https://github.com/blackm0re/etoolkit/tree/2.0.0) (2024-05-13) | 23 | ## [2.0.0](https://codeberg.org/sgs/etoolkit/releases/tag/2.0.0) (2024-05-13) |
| 13 | 24 | ||
| 14 | [Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.2.0...2.0.0) | 25 | [Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.2.0...2.0.0) |
| 15 | 26 | ||
| 16 | **Changes:** | 27 | **Changes:** |
| 17 | 28 | ||
| @@ -24,9 +35,9 @@ | |||
| 24 | - new etoolkit.EtoolkitInstance API (not compatible with v1) | 35 | - new etoolkit.EtoolkitInstance API (not compatible with v1) |
| 25 | 36 | ||
| 26 | 37 | ||
| 27 | ## [1.2.0](https://github.com/blackm0re/etoolkit/tree/1.2.0) (2022-04-04) | 38 | ## [1.2.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.2.0) (2022-04-04) |
| 28 | 39 | ||
| 29 | [Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.1.0...1.2.0) | 40 | [Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.1.0...1.2.0) |
| 30 | 41 | ||
| 31 | **Changes:** | 42 | **Changes:** |
| 32 | 43 | ||
| @@ -35,9 +46,9 @@ | |||
| 35 | - new *%p* macro - parent value (for the same key) | 46 | - new *%p* macro - parent value (for the same key) |
| 36 | 47 | ||
| 37 | 48 | ||
| 38 | ## [1.1.0](https://github.com/blackm0re/etoolkit/tree/1.1.0) (2022-03-14) | 49 | ## [1.1.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.1.0) (2022-03-14) |
| 39 | 50 | ||
| 40 | [Full Changelog](https://github.com/blackm0re/etoolkit/compare/1.0.0...1.1.0) | 51 | [Full Changelog](https://codeberg.org/sgs/etoolkit/compare/1.0.0...1.1.0) |
| 41 | 52 | ||
| 42 | **Changes:** | 53 | **Changes:** |
| 43 | 54 | ||
| @@ -50,7 +61,7 @@ | |||
| 50 | - use *setuptools* instead of the deprecated *distutils* | 61 | - use *setuptools* instead of the deprecated *distutils* |
| 51 | 62 | ||
| 52 | 63 | ||
| 53 | # [1.0.0](https://github.com/blackm0re/etoolkit/tree/1.0.0) (2021-12-23) | 64 | # [1.0.0](https://codeberg.org/sgs/etoolkit/releases/tag/1.0.0) (2021-12-23) |
| 54 | 65 | ||
| 55 | **Changes:** | 66 | **Changes:** |
| 56 | 67 | ||
| @@ -21,7 +21,7 @@ goals were set: | |||
| 21 | 21 | ||
| 22 | ## Requirements | 22 | ## Requirements |
| 23 | 23 | ||
| 24 | Apart from Python >= 3.8, the only requirement is | 24 | Apart from Python >= 3.10, the only requirement is |
| 25 | [cryptography](https://pypi.org/project/cryptography/) | 25 | [cryptography](https://pypi.org/project/cryptography/) |
| 26 | 26 | ||
| 27 | 27 | ||
| @@ -183,7 +183,7 @@ Most users (including me) will simply use the command line interface (CLI). | |||
| 183 | 183 | ||
| 184 | The *etoolkit* CLI loads the configuration located by default in | 184 | The *etoolkit* CLI loads the configuration located by default in |
| 185 | *~/.etoolkit.json*. That file is based on | 185 | *~/.etoolkit.json*. That file is based on |
| 186 | [etoolkit_sample.json](https://github.com/blackm0re/etoolkit/blob/master/etoolkit_sample.json). | 186 | [etoolkit_sample.json](https://codeberg.org/sgs/etoolkit/src/branch/master/etoolkit_sample.json). |
| 187 | 187 | ||
| 188 | The *"instances"* part of the configuration defines unique instances, each of | 188 | The *"instances"* part of the configuration defines unique instances, each of |
| 189 | which represents its own environment with its own defined env. variables. | 189 | which represents its own environment with its own defined env. variables. |
| @@ -262,6 +262,8 @@ When all values are fetched from a given instance (and its parents) and then | |||
| 262 | decrypted, they are further processed by replacing macros with their | 262 | decrypted, they are further processed by replacing macros with their |
| 263 | corresponding values. Currently the following macros are supported: | 263 | corresponding values. Currently the following macros are supported: |
| 264 | 264 | ||
| 265 | - **%e** - the current value of the env. var. corresponding to the same key | ||
| 266 | |||
| 265 | - **%h** - the home directory of the user running *etoolkit* (~/) | 267 | - **%h** - the home directory of the user running *etoolkit* (~/) |
| 266 | 268 | ||
| 267 | - **%i** - the name of the instance that is about to be loaded | 269 | - **%i** - the name of the instance that is about to be loaded |
| @@ -395,18 +397,18 @@ bottom of your bash startup file: | |||
| 395 | ``` | 397 | ``` |
| 396 | 398 | ||
| 397 | A complete bash completion script for *etoolkit* can be found here: | 399 | A complete bash completion script for *etoolkit* can be found here: |
| 398 | [https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash](https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash) | 400 | [https://codeberg.org/sgs/etoolkit/src/branch/master/completion/etoolkit.bash](https://codeberg.org/sgs/etoolkit/src/branch/master/completion/etoolkit.bash) |
| 399 | 401 | ||
| 400 | 402 | ||
| 401 | ## Changelog | 403 | ## Changelog |
| 402 | 404 | ||
| 403 | A complete changelog can be found at: | 405 | A complete changelog can be found at: |
| 404 | [https://github.com/blackm0re/etoolkit/blob/master/CHANGELOG.md](https://github.com/blackm0re/etoolkit/blob/master/CHANGELOG.md) | 406 | [https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md](https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md) |
| 405 | 407 | ||
| 406 | 408 | ||
| 407 | ## Support and contributing | 409 | ## Support and contributing |
| 408 | 410 | ||
| 409 | *etoolkit* is hosted on GitHub: https://github.com/blackm0re/etoolkit | 411 | *etoolkit* is hosted on Codeberg: https://codeberg.org/sgs/etoolkit |
| 410 | 412 | ||
| 411 | 413 | ||
| 412 | ## Author | 414 | ## Author |
| @@ -414,11 +416,11 @@ A complete changelog can be found at: | |||
| 414 | Simeon Simeonov - sgs @ LiberaChat | 416 | Simeon Simeonov - sgs @ LiberaChat |
| 415 | 417 | ||
| 416 | 418 | ||
| 417 | ## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) | 419 | ## [License](https://codeberg.org/sgs/etoolkit/src/branch/master/LICENSE) |
| 418 | 420 | ||
| 419 | Copyright (C) 2021-2024 Simeon Simeonov | 421 | Copyright (C) 2021-2026 Simeon Simeonov |
| 420 | All rights reserved. | 422 | All rights reserved. |
| 421 | 423 | ||
| 422 | [Licensed](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) under the | 424 | [Licensed](https://codeberg.org/sgs/etoolkit/src/branch/master/LICENSE) under the |
| 423 | GNU General Public License v3.0 or later. | 425 | GNU General Public License v3.0 or later. |
| 424 | SPDX-License-Identifier: GPL-3.0-or-later | 426 | SPDX-License-Identifier: GPL-3.0-or-later |
diff --git a/pyproject.toml b/pyproject.toml index 86a1527..e44adbb 100644 --- a/pyproject.toml +++ b/pyproject.toml | |||
| @@ -1,10 +1,42 @@ | |||
| 1 | [build-system] | 1 | [project] |
| 2 | requires = [ | 2 | name = "etoolkit" |
| 3 | "setuptools>=51", | 3 | description = "A simple toolkit for setting environment variables in a flexible way" |
| 4 | "wheel" | 4 | dynamic = ["version"] |
| 5 | license = "GPL-3.0-or-later" | ||
| 6 | authors = [{name = "Simeon Simeonov", email = "sgs@pichove.org"}] | ||
| 7 | readme = "README.md" | ||
| 8 | requires-python = ">= 3.10" | ||
| 9 | |||
| 10 | classifiers = [ | ||
| 11 | "Development Status :: 5 - Production/Stable", | ||
| 12 | "Environment :: Console", | ||
| 13 | "Intended Audience :: Developers", | ||
| 14 | "Intended Audience :: System Administrators", | ||
| 15 | "Programming Language :: Python :: 3", | ||
| 16 | "Programming Language :: Python :: 3.10", | ||
| 17 | "Programming Language :: Python :: 3.11", | ||
| 18 | "Programming Language :: Python :: 3.12", | ||
| 19 | "Programming Language :: Python :: 3.13", | ||
| 20 | "Programming Language :: Python :: 3.14", | ||
| 21 | "Programming Language :: Python :: 3.15", | ||
| 22 | "Operating System :: POSIX", | ||
| 23 | "Topic :: Security :: Cryptography", | ||
| 5 | ] | 24 | ] |
| 6 | 25 | ||
| 7 | build-backend = "setuptools.build_meta" | 26 | dependencies = [ |
| 27 | "cryptography>=3.2" | ||
| 28 | ] | ||
| 29 | |||
| 30 | |||
| 31 | [project.scripts] | ||
| 32 | etoolkit = "etoolkit.__main__:main" | ||
| 33 | |||
| 34 | |||
| 35 | [project.urls] | ||
| 36 | Homepage = "https://codeberg.org/sgs/etoolkit" | ||
| 37 | Repository = "https://codeberg.org/sgs/etoolkit" | ||
| 38 | Issues = "https://codeberg.org/sgs/etoolkit/issues" | ||
| 39 | Changelog = "https://codeberg.org/sgs/etoolkit/src/branch/master/CHANGELOG.md" | ||
| 8 | 40 | ||
| 9 | 41 | ||
| 10 | [tool.pytest.ini_options] | 42 | [tool.pytest.ini_options] |
| @@ -16,3 +48,15 @@ testpaths = [ | |||
| 16 | pythonpath = [ | 48 | pythonpath = [ |
| 17 | "src" | 49 | "src" |
| 18 | ] | 50 | ] |
| 51 | |||
| 52 | |||
| 53 | [tool.setuptools.dynamic] | ||
| 54 | version = {attr = "etoolkit.__version__"} | ||
| 55 | |||
| 56 | |||
| 57 | [build-system] | ||
| 58 | requires = [ | ||
| 59 | "setuptools >= 77.0.3", | ||
| 60 | "wheel" | ||
| 61 | ] | ||
| 62 | build-backend = "setuptools.build_meta" | ||
diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1838e18..0000000 --- a/setup.cfg +++ /dev/null | |||
| @@ -1,45 +0,0 @@ | |||
| 1 | [metadata] | ||
| 2 | name = etoolkit | ||
| 3 | version = attr: etoolkit.__version__ | ||
| 4 | author = attr: etoolkit.__author__ | ||
| 5 | author_email = sgs@pichove.org | ||
| 6 | description = A simple toolkit for setting environment variables in a flexible way | ||
| 7 | long_description = file: README.md | ||
| 8 | long_description_content_type = text/markdown | ||
| 9 | url = https://github.com/blackm0re/etoolkit | ||
| 10 | |||
| 11 | classifiers = | ||
| 12 | Development Status :: 5 - Production/Stable | ||
| 13 | Environment :: Console | ||
| 14 | Intended Audience :: Developers | ||
| 15 | Intended Audience :: System Administrators | ||
| 16 | License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) | ||
| 17 | Programming Language :: Python :: 3 | ||
| 18 | Programming Language :: Python :: 3.9 | ||
| 19 | Programming Language :: Python :: 3.10 | ||
| 20 | Programming Language :: Python :: 3.11 | ||
| 21 | Programming Language :: Python :: 3.12 | ||
| 22 | Programming Language :: Python :: 3.13 | ||
| 23 | Programming Language :: Python :: 3.14 | ||
| 24 | Operating System :: POSIX | ||
| 25 | Topic :: Security :: Cryptography | ||
| 26 | |||
| 27 | project_urls = | ||
| 28 | Bug Tracker = https://github.com/blackm0re/etoolkit/issues | ||
| 29 | Source = https://github.com/blackm0re/etoolkit | ||
| 30 | |||
| 31 | [options] | ||
| 32 | package_dir = | ||
| 33 | = src | ||
| 34 | packages = find: | ||
| 35 | python_requires = >=3.9 | ||
| 36 | |||
| 37 | install_requires = | ||
| 38 | cryptography>=3.2 | ||
| 39 | |||
| 40 | [options.packages.find] | ||
| 41 | where = src | ||
| 42 | |||
| 43 | [options.entry_points] | ||
| 44 | console_scripts = | ||
| 45 | etoolkit = etoolkit.__main__:main | ||
diff --git a/src/etoolkit/__init__.py b/src/etoolkit/__init__.py index 4cf5d6e..fc32b61 100644 --- a/src/etoolkit/__init__.py +++ b/src/etoolkit/__init__.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -18,11 +18,11 @@ | |||
| 18 | from .etoolkit import EtoolkitInstance, EtoolkitInstanceError | 18 | from .etoolkit import EtoolkitInstance, EtoolkitInstanceError |
| 19 | 19 | ||
| 20 | __author__ = 'Simeon Simeonov' | 20 | __author__ = 'Simeon Simeonov' |
| 21 | __version__ = '2.1.0' | 21 | __version__ = '2.2.0' |
| 22 | __license__ = 'GPL3' | 22 | __license__ = 'GPL3' |
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | def int_or_str(value): | 25 | def int_or_str(value: int | str) -> int | str: |
| 26 | """Returns int value of value when possible""" | 26 | """Returns int value of value when possible""" |
| 27 | try: | 27 | try: |
| 28 | return int(value) | 28 | return int(value) |
diff --git a/src/etoolkit/__main__.py b/src/etoolkit/__main__.py index a2f2f20..cef3d42 100644 --- a/src/etoolkit/__main__.py +++ b/src/etoolkit/__main__.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -50,7 +50,7 @@ class EtoolkitCLIHandler: | |||
| 50 | a part of the etoolkit API | 50 | a part of the etoolkit API |
| 51 | """ | 51 | """ |
| 52 | 52 | ||
| 53 | def __init__(self, args: argparse.Namespace, config_dict: dict): | 53 | def __init__(self, args: argparse.Namespace, config_dict: dict) -> None: |
| 54 | """ | 54 | """ |
| 55 | :param args: The parsed argparse arguments sent by the caller | 55 | :param args: The parsed argparse arguments sent by the caller |
| 56 | :type args: argparse.Namespace | 56 | :type args: argparse.Namespace |
| @@ -69,7 +69,7 @@ class EtoolkitCLIHandler: | |||
| 69 | 69 | ||
| 70 | self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD') | 70 | self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD') |
| 71 | 71 | ||
| 72 | def decrypt_value(self): | 72 | def decrypt_value(self) -> None: |
| 73 | """ | 73 | """ |
| 74 | Interactive method for decrypting value(s) | 74 | Interactive method for decrypting value(s) |
| 75 | 75 | ||
| @@ -110,7 +110,7 @@ class EtoolkitCLIHandler: | |||
| 110 | break | 110 | break |
| 111 | return | 111 | return |
| 112 | 112 | ||
| 113 | def encrypt_value(self): | 113 | def encrypt_value(self) -> None: |
| 114 | """ | 114 | """ |
| 115 | Interactive method for encrypting value(s) | 115 | Interactive method for encrypting value(s) |
| 116 | 116 | ||
| @@ -155,7 +155,7 @@ class EtoolkitCLIHandler: | |||
| 155 | break | 155 | break |
| 156 | return | 156 | return |
| 157 | 157 | ||
| 158 | def generate_master_password_hash(self): | 158 | def generate_master_password_hash(self) -> None: |
| 159 | """ | 159 | """ |
| 160 | Interactive method for generating password hash | 160 | Interactive method for generating password hash |
| 161 | 161 | ||
| @@ -168,7 +168,7 @@ class EtoolkitCLIHandler: | |||
| 168 | ) | 168 | ) |
| 169 | print(f'Master password hash: {phash}') | 169 | print(f'Master password hash: {phash}') |
| 170 | 170 | ||
| 171 | def list(self): | 171 | def list(self) -> None: |
| 172 | """Lists all instances defined in the config file""" | 172 | """Lists all instances defined in the config file""" |
| 173 | 173 | ||
| 174 | for instance_name in sorted( | 174 | for instance_name in sorted( |
| @@ -179,7 +179,7 @@ class EtoolkitCLIHandler: | |||
| 179 | ): | 179 | ): |
| 180 | print(instance_name) | 180 | print(instance_name) |
| 181 | 181 | ||
| 182 | def load_instance(self): | 182 | def load_instance(self) -> None: |
| 183 | """Loads a single specified instance from the config file""" | 183 | """Loads a single specified instance from the config file""" |
| 184 | 184 | ||
| 185 | inst = etoolkit.EtoolkitInstance( | 185 | inst = etoolkit.EtoolkitInstance( |
| @@ -208,7 +208,7 @@ class EtoolkitCLIHandler: | |||
| 208 | os.environ.get('SHELL', 'bash').split(), check=False | 208 | os.environ.get('SHELL', 'bash').split(), check=False |
| 209 | ) | 209 | ) |
| 210 | 210 | ||
| 211 | def reencrypt(self): | 211 | def reencrypt(self) -> None: |
| 212 | """ | 212 | """ |
| 213 | Interactive method that prints new configuration data (JSON) to stdout | 213 | Interactive method that prints new configuration data (JSON) to stdout |
| 214 | 214 | ||
| @@ -277,7 +277,7 @@ class EtoolkitCLIHandler: | |||
| 277 | ) | 277 | ) |
| 278 | 278 | ||
| 279 | 279 | ||
| 280 | def main(inargs=None): | 280 | def main(inargs: list = None) -> None: |
| 281 | """main entry point""" | 281 | """main entry point""" |
| 282 | 282 | ||
| 283 | parser = argparse.ArgumentParser( | 283 | parser = argparse.ArgumentParser( |
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py index a933a01..f5ce2a6 100644 --- a/src/etoolkit/etoolkit.py +++ b/src/etoolkit/etoolkit.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -19,6 +19,7 @@ import base64 | |||
| 19 | import getpass | 19 | import getpass |
| 20 | import hashlib | 20 | import hashlib |
| 21 | import os | 21 | import os |
| 22 | from collections.abc import Callable | ||
| 22 | 23 | ||
| 23 | from cryptography.exceptions import InvalidTag | 24 | from cryptography.exceptions import InvalidTag |
| 24 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM | 25 | from cryptography.hazmat.primitives.ciphers.aead import AESGCM |
| @@ -33,7 +34,7 @@ class EtoolkitInstanceError(Exception): | |||
| 33 | class EtoolkitInstance: | 34 | class EtoolkitInstance: |
| 34 | """A basic class representing a single instance""" | 35 | """A basic class representing a single instance""" |
| 35 | 36 | ||
| 36 | def __init__(self, name: str, data: dict): | 37 | def __init__(self, name: str, data: dict) -> None: |
| 37 | """ | 38 | """ |
| 38 | :param name: Instance name | 39 | :param name: Instance name |
| 39 | :type name: str | 40 | :type name: str |
| @@ -76,14 +77,14 @@ class EtoolkitInstance: | |||
| 76 | ] | 77 | ] |
| 77 | 78 | ||
| 78 | @property | 79 | @property |
| 79 | def master_password(self) -> str: | 80 | def master_password(self) -> str | None: |
| 80 | """master_password-property""" | 81 | """master_password-property""" |
| 81 | if self._master_password is not None: | 82 | if self._master_password is not None: |
| 82 | return self._master_password | 83 | return self._master_password |
| 83 | return None if self._parent is None else self._parent.master_password | 84 | return None if self._parent is None else self._parent.master_password |
| 84 | 85 | ||
| 85 | @master_password.setter | 86 | @master_password.setter |
| 86 | def master_password(self, value): | 87 | def master_password(self, value: str) -> None: |
| 87 | """master_password-property setter""" | 88 | """master_password-property setter""" |
| 88 | self._master_password = value | 89 | self._master_password = value |
| 89 | if self._parent is not None and self._parent.master_password is None: | 90 | if self._parent is not None and self._parent.master_password is None: |
| @@ -95,7 +96,7 @@ class EtoolkitInstance: | |||
| 95 | return self._master_password_hash | 96 | return self._master_password_hash |
| 96 | 97 | ||
| 97 | @master_password_hash.setter | 98 | @master_password_hash.setter |
| 98 | def master_password_hash(self, value): | 99 | def master_password_hash(self, value: str) -> None: |
| 99 | """master_password_hash-property setter""" | 100 | """master_password_hash-property setter""" |
| 100 | self._master_password_hash = value | 101 | self._master_password_hash = value |
| 101 | 102 | ||
| @@ -105,12 +106,12 @@ class EtoolkitInstance: | |||
| 105 | return self._name | 106 | return self._name |
| 106 | 107 | ||
| 107 | @property | 108 | @property |
| 108 | def prompt_func(self): | 109 | def prompt_func(self) -> Callable[[str, bool], str]: |
| 109 | """prompt_func-property""" | 110 | """prompt_func-property""" |
| 110 | return self._prompt_func | 111 | return self._prompt_func |
| 111 | 112 | ||
| 112 | @prompt_func.setter | 113 | @prompt_func.setter |
| 113 | def prompt_func(self, value): | 114 | def prompt_func(self, value: Callable[[str, bool], str]) -> None: |
| 114 | """prompt_func-property setter""" | 115 | """prompt_func-property setter""" |
| 115 | self._prompt_func = value | 116 | self._prompt_func = value |
| 116 | if self._parent is not None and self._parent.prompt_func is None: | 117 | if self._parent is not None and self._parent.prompt_func is None: |
| @@ -294,7 +295,7 @@ class EtoolkitInstance: | |||
| 294 | ) | 295 | ) |
| 295 | 296 | ||
| 296 | @staticmethod | 297 | @staticmethod |
| 297 | def parse_value(value, macros: dict): | 298 | def parse_value(value: object, macros: dict) -> object: |
| 298 | """ | 299 | """ |
| 299 | Returns the value with all macros replaced by their values | 300 | Returns the value with all macros replaced by their values |
| 300 | 301 | ||
| @@ -412,6 +413,8 @@ class EtoolkitInstance: | |||
| 412 | if not value: | 413 | if not value: |
| 413 | # perhaps unset instead of skipping? | 414 | # perhaps unset instead of skipping? |
| 414 | continue | 415 | continue |
| 416 | if isinstance(value, str) and '%e' in value: | ||
| 417 | macros['%e'] = os.environ.get(key, '') | ||
| 415 | if isinstance(value, str) and '%p' in value: | 418 | if isinstance(value, str) and '%p' in value: |
| 416 | macros['%p'] = ( | 419 | macros['%p'] = ( |
| 417 | self._parent.get_environ().get(key, '') | 420 | self._parent.get_environ().get(key, '') |
diff --git a/tests/conftest.py b/tests/conftest.py index a4463fd..6cc8b9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -20,7 +20,7 @@ import json | |||
| 20 | import pytest | 20 | import pytest |
| 21 | 21 | ||
| 22 | 22 | ||
| 23 | @pytest.fixture() | 23 | @pytest.fixture |
| 24 | def config_data(): | 24 | def config_data(): |
| 25 | """config_data for testing""" | 25 | """config_data for testing""" |
| 26 | 26 | ||
| @@ -59,7 +59,7 @@ def config_data(): | |||
| 59 | } | 59 | } |
| 60 | 60 | ||
| 61 | 61 | ||
| 62 | @pytest.fixture() | 62 | @pytest.fixture |
| 63 | def config_file(tmp_path, config_data): | 63 | def config_file(tmp_path, config_data): |
| 64 | """Temporary config file for testing that includes config_data""" | 64 | """Temporary config file for testing that includes config_data""" |
| 65 | 65 | ||
| @@ -68,7 +68,7 @@ def config_file(tmp_path, config_data): | |||
| 68 | return str(cf) | 68 | return str(cf) |
| 69 | 69 | ||
| 70 | 70 | ||
| 71 | @pytest.fixture() | 71 | @pytest.fixture |
| 72 | def long_encrypted_value(): | 72 | def long_encrypted_value(): |
| 73 | """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" | 73 | """enc. value corresponding to 'Nobody expects the Spanish inquisition'""" |
| 74 | 74 | ||
| @@ -79,25 +79,25 @@ def long_encrypted_value(): | |||
| 79 | ) | 79 | ) |
| 80 | 80 | ||
| 81 | 81 | ||
| 82 | @pytest.fixture() | 82 | @pytest.fixture |
| 83 | def long_value(): | 83 | def long_value(): |
| 84 | """standard value (> 32 bytes)""" | 84 | """standard value (> 32 bytes)""" |
| 85 | return 'Nobody expects the Spanish inquisition' | 85 | return 'Nobody expects the Spanish inquisition' |
| 86 | 86 | ||
| 87 | 87 | ||
| 88 | @pytest.fixture() | 88 | @pytest.fixture |
| 89 | def master_password(): | 89 | def master_password(): |
| 90 | """Master passord""" | 90 | """Master passord""" |
| 91 | return 'The very secret passwd' | 91 | return 'The very secret passwd' |
| 92 | 92 | ||
| 93 | 93 | ||
| 94 | @pytest.fixture() | 94 | @pytest.fixture |
| 95 | def new_master_password(): | 95 | def new_master_password(): |
| 96 | """Master passord""" | 96 | """Master passord""" |
| 97 | return 'New very secret passwd' | 97 | return 'New very secret passwd' |
| 98 | 98 | ||
| 99 | 99 | ||
| 100 | @pytest.fixture() | 100 | @pytest.fixture |
| 101 | def non_random_bytes_32(): | 101 | def non_random_bytes_32(): |
| 102 | """always use the same bytes instead of os.urandom(32)""" | 102 | """always use the same bytes instead of os.urandom(32)""" |
| 103 | 103 | ||
| @@ -107,7 +107,7 @@ def non_random_bytes_32(): | |||
| 107 | ) | 107 | ) |
| 108 | 108 | ||
| 109 | 109 | ||
| 110 | @pytest.fixture() | 110 | @pytest.fixture |
| 111 | def non_random_bytes_57(): | 111 | def non_random_bytes_57(): |
| 112 | """always use the same bytes instead of os.urandom(57)""" | 112 | """always use the same bytes instead of os.urandom(57)""" |
| 113 | 113 | ||
| @@ -118,14 +118,14 @@ def non_random_bytes_57(): | |||
| 118 | ) | 118 | ) |
| 119 | 119 | ||
| 120 | 120 | ||
| 121 | @pytest.fixture() | 121 | @pytest.fixture |
| 122 | def nonexistent_config_file(tmp_path): | 122 | def nonexistent_config_file(tmp_path): |
| 123 | """temporary config file for testing that includes config_data""" | 123 | """temporary config file for testing that includes config_data""" |
| 124 | 124 | ||
| 125 | return str(tmp_path / 'etoolkitt.json') | 125 | return str(tmp_path / 'etoolkitt.json') |
| 126 | 126 | ||
| 127 | 127 | ||
| 128 | @pytest.fixture() | 128 | @pytest.fixture |
| 129 | def password_hash(): | 129 | def password_hash(): |
| 130 | """password hash for testing, corresponding to 'The very secret passwd'""" | 130 | """password hash for testing, corresponding to 'The very secret passwd'""" |
| 131 | 131 | ||
| @@ -135,7 +135,7 @@ def password_hash(): | |||
| 135 | ) | 135 | ) |
| 136 | 136 | ||
| 137 | 137 | ||
| 138 | @pytest.fixture() | 138 | @pytest.fixture |
| 139 | def short_encrypted_value(): | 139 | def short_encrypted_value(): |
| 140 | """enc. value corresponding to 'secret1'""" | 140 | """enc. value corresponding to 'secret1'""" |
| 141 | 141 | ||
| @@ -145,7 +145,7 @@ def short_encrypted_value(): | |||
| 145 | ) | 145 | ) |
| 146 | 146 | ||
| 147 | 147 | ||
| 148 | @pytest.fixture() | 148 | @pytest.fixture |
| 149 | def short_encrypted_value_v1(): | 149 | def short_encrypted_value_v1(): |
| 150 | """enc. value (enc-val 1) corresponding to 'secret1'""" | 150 | """enc. value (enc-val 1) corresponding to 'secret1'""" |
| 151 | 151 | ||
| @@ -155,13 +155,13 @@ def short_encrypted_value_v1(): | |||
| 155 | ) | 155 | ) |
| 156 | 156 | ||
| 157 | 157 | ||
| 158 | @pytest.fixture() | 158 | @pytest.fixture |
| 159 | def short_value(): | 159 | def short_value(): |
| 160 | """standard value (< 32 bytes)""" | 160 | """standard value (< 32 bytes)""" |
| 161 | return 'secret1' | 161 | return 'secret1' |
| 162 | 162 | ||
| 163 | 163 | ||
| 164 | @pytest.fixture() | 164 | @pytest.fixture |
| 165 | def wrong_master_password(): | 165 | def wrong_master_password(): |
| 166 | """Wrong master passord""" | 166 | """Wrong master passord""" |
| 167 | return 'the very secret passwd' | 167 | return 'the very secret passwd' |
diff --git a/tests/test_cli.py b/tests/test_cli.py index 953abf4..b7913da 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
| @@ -38,7 +38,6 @@ def test_decrypt_v1(binput, capsys, config_file, master_password): | |||
| 38 | ): | 38 | ): |
| 39 | with pytest.raises(SystemExit) as exit_info: | 39 | with pytest.raises(SystemExit) as exit_info: |
| 40 | main(['-c', f'{config_file}', '-d']) | 40 | main(['-c', f'{config_file}', '-d']) |
| 41 | assert exit_info.type == SystemExit | ||
| 42 | assert exit_info.value.code == 0 | 41 | assert exit_info.value.code == 0 |
| 43 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' | 42 | assert capsys.readouterr().out.strip() == 'Decrypted value: bar' |
| 44 | 43 | ||
| @@ -60,7 +59,6 @@ def test_decrypt_v2( | |||
| 60 | ): | 59 | ): |
| 61 | with pytest.raises(SystemExit) as exit_info: | 60 | with pytest.raises(SystemExit) as exit_info: |
| 62 | main(['-c', f'{config_file}', '-d']) | 61 | main(['-c', f'{config_file}', '-d']) |
| 63 | assert exit_info.type == SystemExit | ||
| 64 | assert exit_info.value.code == 0 | 62 | assert exit_info.value.code == 0 |
| 65 | assert capsys.readouterr().out.strip() == ( | 63 | assert capsys.readouterr().out.strip() == ( |
| 66 | f'Decrypted value: {short_value}' | 64 | f'Decrypted value: {short_value}' |
| @@ -88,7 +86,6 @@ def test_encrypt_with_echo( | |||
| 88 | ): | 86 | ): |
| 89 | with pytest.raises(SystemExit) as exit_info: | 87 | with pytest.raises(SystemExit) as exit_info: |
| 90 | main(['-c', f'{config_file}', '-e', '-E']) | 88 | main(['-c', f'{config_file}', '-e', '-E']) |
| 91 | assert exit_info.type == SystemExit | ||
| 92 | assert exit_info.value.code == 0 | 89 | assert exit_info.value.code == 0 |
| 93 | assert capsys.readouterr().out.strip() == ( | 90 | assert capsys.readouterr().out.strip() == ( |
| 94 | f'Encrypted value: {short_encrypted_value}' | 91 | f'Encrypted value: {short_encrypted_value}' |
| @@ -116,7 +113,6 @@ def test_encrypt_without_echo( | |||
| 116 | ): | 113 | ): |
| 117 | with pytest.raises(SystemExit) as exit_info: | 114 | with pytest.raises(SystemExit) as exit_info: |
| 118 | main(['-c', f'{config_file}', '-e']) | 115 | main(['-c', f'{config_file}', '-e']) |
| 119 | assert exit_info.type == SystemExit | ||
| 120 | assert exit_info.value.code == 0 | 116 | assert exit_info.value.code == 0 |
| 121 | assert capsys.readouterr().out.strip() == ( | 117 | assert capsys.readouterr().out.strip() == ( |
| 122 | f'Encrypted value: {short_encrypted_value}' | 118 | f'Encrypted value: {short_encrypted_value}' |
| @@ -139,11 +135,9 @@ def test_list(capsys, config_file, nonexistent_config_file): | |||
| 139 | 135 | ||
| 140 | with pytest.raises(SystemExit) as exit_info: | 136 | with pytest.raises(SystemExit) as exit_info: |
| 141 | main(['-c', nonexistent_config_file, '-l']) | 137 | main(['-c', nonexistent_config_file, '-l']) |
| 142 | assert exit_info.type == SystemExit | ||
| 143 | assert exit_info.value.code == errno.EIO | 138 | assert exit_info.value.code == errno.EIO |
| 144 | with pytest.raises(SystemExit) as exit_info: | 139 | with pytest.raises(SystemExit) as exit_info: |
| 145 | main(['-c', config_file, '-l']) | 140 | main(['-c', config_file, '-l']) |
| 146 | assert exit_info.type == SystemExit | ||
| 147 | assert exit_info.value.code == 0 | 141 | assert exit_info.value.code == 0 |
| 148 | assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' | 142 | assert capsys.readouterr().out.strip() == f'dev{os.linesep}secret' |
| 149 | 143 | ||
| @@ -153,7 +147,6 @@ def test_help(capsys): | |||
| 153 | 147 | ||
| 154 | with pytest.raises(SystemExit) as exit_info: | 148 | with pytest.raises(SystemExit) as exit_info: |
| 155 | main(['-h']) | 149 | main(['-h']) |
| 156 | assert exit_info.type == SystemExit | ||
| 157 | assert exit_info.value.code == 0 | 150 | assert exit_info.value.code == 0 |
| 158 | assert capsys.readouterr().out.startswith('usage: etoolkit') | 151 | assert capsys.readouterr().out.startswith('usage: etoolkit') |
| 159 | 152 | ||
| @@ -169,7 +162,6 @@ def test_generate_master_password_hash( | |||
| 169 | urandom.return_value = non_random_bytes_32 | 162 | urandom.return_value = non_random_bytes_32 |
| 170 | with pytest.raises(SystemExit) as exit_info: | 163 | with pytest.raises(SystemExit) as exit_info: |
| 171 | main(['--generate-master-password-hash']) | 164 | main(['--generate-master-password-hash']) |
| 172 | assert exit_info.type == SystemExit | ||
| 173 | assert exit_info.value.code == 0 | 165 | assert exit_info.value.code == 0 |
| 174 | assert capsys.readouterr().out.strip() == ( | 166 | assert capsys.readouterr().out.strip() == ( |
| 175 | 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076' | 167 | 'Master password hash: pbkdf2_sha256$500000$uYpZM1VfAGq0CDZL2duITs076' |
| @@ -182,7 +174,6 @@ def test_version(capsys): | |||
| 182 | 174 | ||
| 183 | with pytest.raises(SystemExit) as exit_info: | 175 | with pytest.raises(SystemExit) as exit_info: |
| 184 | main(['-v']) | 176 | main(['-v']) |
| 185 | assert exit_info.type == SystemExit | ||
| 186 | assert exit_info.value.code == 0 | 177 | assert exit_info.value.code == 0 |
| 187 | assert capsys.readouterr().out.startswith( | 178 | assert capsys.readouterr().out.startswith( |
| 188 | f'etoolkit {etoolkit.__version__}' | 179 | f'etoolkit {etoolkit.__version__}' |
diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index f555f81..5137e9f 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index ee5cf9e..e13d439 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | # etoolkit | 1 | # etoolkit |
| 2 | # Copyright (C) 2021-2024 Simeon Simeonov | 2 | # Copyright (C) 2021-2026 Simeon Simeonov |
| 3 | 3 | ||
| 4 | # This program is free software: you can redistribute it and/or modify | 4 | # This program is free software: you can redistribute it and/or modify |
| 5 | # it under the terms of the GNU General Public License as published by | 5 | # it under the terms of the GNU General Public License as published by |
