├── .bumpversion.cfg ├── .dockerignore ├── .flake8 ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── pre-commit.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── cli-runner.py ├── deps ├── __init__.py ├── __main__.py ├── cli │ ├── __init__.py │ └── main.py ├── config.py ├── environment_configuration │ ├── __init__.py │ └── view.py ├── pipenv │ ├── __init__.py │ ├── resolver.py │ └── view.py ├── storage.py └── version.py ├── docker-compose.yml ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── scripts └── __init__.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py └── test_pass.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.3 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker ignore whitelist 2 | 3 | # Ignore everything 4 | * 5 | 6 | # except these items 7 | !.codespellrc 8 | !.flake8 9 | !.isort.cfg 10 | !.pylintrc 11 | !.pylintrc 12 | !cli-runner.py 13 | !cli.py 14 | !Makefile 15 | !mypy.ini 16 | !requirements_dev.txt 17 | !requirements.txt 18 | !deps 19 | !setup.cfg 20 | !setup.py 21 | !tests 22 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .svn,CVS,.bzr,.hg,.git,__pycache,.venv,migrations,settings,tests,.tox,build 3 | max-complexity = 20 4 | max-line-length = 119 5 | select = C,E,F,W,B,B950 6 | ignore = E203,E501,D107,D102,W503 7 | # Ignore unused import warnings in __init__.py files 8 | per-file-ignores = __init__.py:F401 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # If you wish to be added as a codeowner, feel free to submit a pull request. 2 | * @apoclyps 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 16 | **Expected behaviour** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. iOS] 24 | - Version [e.g. 22] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What's Changed 2 | 3 | A description of the issue/feature; reference the issue number (if one exists). The Pull Request should not include fixes for issues other than the main issue/feature request. 4 | 5 | ### Technical Description 6 | 7 | Any specific technical detail that may provide additional context to aid code review. 8 | 9 | > Before opening a Pull Request you should read and agreed to the Contributor Code of Conduct (see `CONTRIBUTING.md`\) 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: docker 4 | directory: / 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 2 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: daily 12 | - package-ecosystem: pip 13 | directory: / 14 | schedule: 15 | interval: daily 16 | open-pull-requests-limit: 6 17 | ignore: 18 | - dependency-name: idna 19 | versions: 20 | - '3.1' 21 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit auto-update 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | auto-update: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.x" 16 | - name: Install pre-commit 17 | run: pip install pre-commit 18 | - name: Run pre-commit autoupdate 19 | run: pre-commit autoupdate 20 | - name: Create Pull Request 21 | uses: peter-evans/create-pull-request@v5.0.2 22 | with: 23 | token: ${{ secrets.CPR_GITHUB_TOKEN }} 24 | base: main 25 | branch: update/pre-commit-autoupdate 26 | title: Auto-update pre-commit hooks 27 | commit-message: Auto-update pre-commit hooks 28 | body: Update versions of tools in pre-commit configs to latest version 29 | labels: dependencies 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up Python 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.x" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install setuptools wheel twine 19 | - name: Build and publish 20 | env: 21 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 22 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 23 | run: | 24 | python setup.py sdist bdist_wheel 25 | twine check dist/* 26 | twine upload --skip-existing --repository-url https://upload.pypi.org/legacy/ dist/* --verbose 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10", "3.11"] 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | architecture: x64 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Linting 28 | run: | 29 | black --line-length 119 --check --diff . 30 | isort --check-only --diff . 31 | flake8 . 32 | pylint --rcfile=.pylintrc deps 33 | codespell deps 34 | find . -name '*.py' -exec pyupgrade {} + 35 | - name: Typecheck with mypy 36 | run: mypy deps --ignore-missing-imports --disallow-untyped-defs 37 | - name: Test with pytest 38 | run: pytest -vvv -s 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # OS 51 | .DS_Store 52 | 53 | # pyenv 54 | .python-version 55 | 56 | # dotenv 57 | .env 58 | 59 | # virtualenv 60 | .venv 61 | venv/ 62 | ENV/ 63 | 64 | # editors 65 | .vscode/* 66 | .idea/* 67 | 68 | # mypy 69 | .mypy_cache/ 70 | 71 | # exports 72 | dependencies.svg 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # .pre-commit-config.yaml 3 | # ======================== 4 | # 5 | # pre-commit clean 6 | # pre-commit install 7 | # pre-commit install-hooks 8 | # 9 | # precommit hooks installation 10 | # 11 | # - pre-commit autoupdate 12 | # 13 | # - pre-commit run black 14 | # 15 | # continuous integration 16 | # ====================== 17 | # 18 | # - pre-commit run --all-files 19 | # 20 | 21 | repos: 22 | - repo: https://github.com/pre-commit/pre-commit-hooks 23 | rev: v5.0.0 24 | hooks: 25 | - id: trailing-whitespace 26 | - id: end-of-file-fixer 27 | - id: debug-statements 28 | - id: check-merge-conflict 29 | - id: sort-simple-yaml 30 | - id: fix-encoding-pragma 31 | args: ["--remove"] 32 | - id: forbid-new-submodules 33 | - id: mixed-line-ending 34 | args: ["--fix=lf"] 35 | description: Forces to replace line ending by the UNIX 'lf' character. 36 | - id: check-added-large-files 37 | args: ["--maxkb=500"] 38 | - id: no-commit-to-branch 39 | args: [--branch, master] 40 | - id: check-yaml 41 | - id: check-json 42 | files: ^tests/app/ 43 | - id: pretty-format-json 44 | args: ["--no-sort-keys", "--autofix"] 45 | files: ^tests/app/ 46 | 47 | - repo: meta 48 | hooks: 49 | - id: check-useless-excludes 50 | 51 | - repo: local 52 | hooks: 53 | - id: pylint 54 | name: pylint 55 | entry: pylint 56 | language: system 57 | types: [python] 58 | exclude: tests 59 | 60 | - repo: https://github.com/ambv/black 61 | rev: 25.1.0 62 | hooks: 63 | - id: black 64 | args: 65 | - --line-length=119 66 | 67 | - repo: https://github.com/PyCQA/bandit 68 | rev: 1.8.3 69 | hooks: 70 | - id: bandit 71 | description: Security oriented static analyser for python code 72 | exclude: tests/|scripts/|lib/helpers 73 | args: 74 | - -s 75 | - B101,B311,B105 76 | 77 | - repo: https://github.com/jendrikseipp/vulture 78 | rev: v2.14 79 | hooks: 80 | - id: vulture 81 | description: Find dead Python code 82 | args: 83 | [ 84 | "--min-confidence", 85 | "90", 86 | "--exclude", 87 | "tests,env,lib/schemas/fields.py", 88 | "--ignore-names", 89 | "einfo,task_id,retval,logger,log_method,exc,args", 90 | ".", 91 | ] 92 | 93 | - repo: https://github.com/codespell-project/codespell 94 | rev: v2.4.1 95 | hooks: 96 | - id: codespell 97 | name: codespell 98 | description: Checks for common misspellings in text files. 99 | entry: codespell -L som 100 | language: python 101 | types: [text] 102 | additional_dependencies: 103 | - tomli 104 | 105 | - repo: https://github.com/asottile/pyupgrade 106 | rev: v3.20.0 107 | hooks: 108 | - id: pyupgrade 109 | 110 | - repo: https://github.com/pycqa/flake8 111 | rev: 7.2.0 112 | hooks: 113 | - id: flake8 114 | 115 | - repo: https://github.com/pycqa/isort 116 | rev: 6.0.1 117 | hooks: 118 | - id: isort 119 | name: isort (python) 120 | args: ["--settings-path", "pyproject.toml", "."] 121 | 122 | - repo: https://github.com/igorshubovych/markdownlint-cli 123 | rev: v0.45.0 124 | hooks: 125 | - id: markdownlint-fix 126 | args: ["--disable", "line-length"] 127 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # Default - Fresh Open Source Software Archive: https://fossies.org/linux/pylint/examples/pylintrc 2 | 3 | [MASTER] 4 | 5 | # A comma-separated list of package or module names from where C extensions may 6 | # be loaded. Extensions are loading into the active Python interpreter and may 7 | # run arbitrary code. 8 | extension-pkg-whitelist= 9 | 10 | # Specify a score threshold to be exceeded before program exits with error. 11 | fail-under=10 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Add files or directories matching the regex patterns to the blacklist. The 18 | # regex matches against base names, not paths. 19 | ignore-patterns= 20 | 21 | # Python code to execute, usually for sys.path manipulation such as 22 | # pygtk.require(). 23 | #init-hook= 24 | 25 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 26 | # number of processors available to use. 27 | jobs=1 28 | 29 | # Control the amount of potential inferred values when inferring a single 30 | # object. This can help the performance when dealing with large functions or 31 | # complex, nested conditions. 32 | limit-inference-results=100 33 | 34 | # List of plugins (as comma separated values of python module names) to load, 35 | # usually to register additional checkers. 36 | load-plugins= 37 | 38 | # Pickle collected data for later comparisons. 39 | persistent=yes 40 | 41 | # When enabled, pylint would attempt to guess common misconfiguration and emit 42 | # user-friendly hints instead of false-positive error messages. 43 | suggestion-mode=yes 44 | 45 | # Allow loading of arbitrary C extensions. Extensions are imported into the 46 | # active Python interpreter and may run arbitrary code. 47 | unsafe-load-any-extension=no 48 | 49 | 50 | [MESSAGES CONTROL] 51 | 52 | # Only show warnings with the listed confidence levels. Leave empty to show 53 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 54 | confidence= 55 | 56 | # Disable the message, report, category or checker with the given id(s). You 57 | # can either give multiple identifiers separated by comma (,) or put this 58 | # option multiple times (only on the command line, not in the configuration 59 | # file where it should appear only once). You can also use "--disable=all" to 60 | # disable everything first and then re-enable specific checks. For example, if 61 | # you want to run only the similarities checker, you can use "--disable=all 62 | # --enable=similarities". If you want to run only the classes checker, but have 63 | # no Warning level messages displayed, use "--disable=all --enable=classes 64 | # --disable=W". 65 | disable=raw-checker-failed, 66 | bad-inline-option, 67 | locally-disabled, 68 | file-ignored, 69 | suppressed-message, 70 | useless-suppression, 71 | deprecated-pragma, 72 | use-symbolic-message-instead, 73 | missing-module-docstring, 74 | invalid-name, 75 | raise-missing-from, 76 | unused-argument, 77 | unsubscriptable-object, 78 | use-a-generator, 79 | import-error, 80 | global-statement, 81 | too-few-public-methods, 82 | too-many-instance-attributes, 83 | duplicate-code, 84 | 85 | # Enable the message, report, category or checker with the given id(s). You can 86 | # either give multiple identifier separated by comma (,) or put this option 87 | # multiple time (only on the command line, not in the configuration file where 88 | # it should appear only once). See also the "--disable" option for examples. 89 | enable=c-extension-no-member 90 | 91 | 92 | [REPORTS] 93 | 94 | # Python expression which should return a score less than or equal to 10. You 95 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 96 | # which contain the number of messages in each category, as well as 'statement' 97 | # which is the total number of statements analyzed. This score is used by the 98 | # global evaluation report (RP0004). 99 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 100 | 101 | # Template used to display messages. This is a python new-style format string 102 | # used to format the message information. See doc for all details. 103 | #msg-template= 104 | 105 | # Set the output format. Available formats are text, parseable, colorized, json 106 | # and msvs (visual studio). You can also give a reporter class, e.g. 107 | # mypackage.mymodule.MyReporterClass. 108 | output-format=text 109 | 110 | # Tells whether to display a full report or only the messages. 111 | reports=no 112 | 113 | # Activate the evaluation score. 114 | score=yes 115 | 116 | 117 | [REFACTORING] 118 | 119 | # Maximum number of nested blocks for function / method body 120 | max-nested-blocks=5 121 | 122 | # Complete name of functions that never returns. When checking for 123 | # inconsistent-return-statements if a never returning function is called then 124 | # it will be considered as an explicit return statement and no message will be 125 | # printed. 126 | never-returning-functions=sys.exit 127 | 128 | 129 | [LOGGING] 130 | 131 | # Format style used to check logging format string. `old` means using % 132 | # formatting and `new` is for `{}` formatting. 133 | logging-format-style=old 134 | 135 | # Logging modules to check that the string format arguments are in logging 136 | # function parameter format. 137 | logging-modules=logging 138 | 139 | 140 | [SPELLING] 141 | 142 | # Limits count of emitted suggestions for spelling mistakes. 143 | max-spelling-suggestions=4 144 | 145 | # Spelling dictionary name. Available dictionaries: none. To make it work, 146 | # install the python-enchant package. 147 | spelling-dict= 148 | 149 | # List of comma separated words that should not be checked. 150 | spelling-ignore-words= 151 | 152 | # A path to a file that contains the private dictionary; one word per line. 153 | spelling-private-dict-file= 154 | 155 | # Tells whether to store unknown words to the private dictionary (see the 156 | # --spelling-private-dict-file option) instead of raising a message. 157 | spelling-store-unknown-words=no 158 | 159 | 160 | [MISCELLANEOUS] 161 | 162 | # List of note tags to take in consideration, separated by a comma. 163 | notes=FIXME, 164 | XXX, 165 | TODO 166 | 167 | # Regular expression of note tags to take in consideration. 168 | #notes-rgx= 169 | 170 | 171 | [TYPECHECK] 172 | 173 | # List of decorators that produce context managers, such as 174 | # contextlib.contextmanager. Add to this list to register other decorators that 175 | # produce valid context managers. 176 | contextmanager-decorators=contextlib.contextmanager 177 | 178 | # List of members which are set dynamically and missed by pylint inference 179 | # system, and so shouldn't trigger E1101 when accessed. Python regular 180 | # expressions are accepted. 181 | generated-members= 182 | 183 | # Tells whether missing members accessed in mixin class should be ignored. A 184 | # mixin class is detected if its name ends with "mixin" (case insensitive). 185 | ignore-mixin-members=yes 186 | 187 | # Tells whether to warn about missing members when the owner of the attribute 188 | # is inferred to be None. 189 | ignore-none=yes 190 | 191 | # This flag controls whether pylint should warn about no-member and similar 192 | # checks whenever an opaque object is returned when inferring. The inference 193 | # can return multiple potential results while evaluating a Python object, but 194 | # some branches might not be evaluated, which results in partial inference. In 195 | # that case, it might be useful to still emit no-member and other checks for 196 | # the rest of the inferred objects. 197 | ignore-on-opaque-inference=yes 198 | 199 | # List of class names for which member attributes should not be checked (useful 200 | # for classes with dynamically set attributes). This supports the use of 201 | # qualified names. 202 | ignored-classes=optparse.Values,thread._local,_thread._local 203 | 204 | # List of module names for which member attributes should not be checked 205 | # (useful for modules/projects where namespaces are manipulated during runtime 206 | # and thus existing member attributes cannot be deduced by static analysis). It 207 | # supports qualified module names, as well as Unix pattern matching. 208 | ignored-modules= 209 | 210 | # Show a hint with possible names when a member name was not found. The aspect 211 | # of finding the hint is based on edit distance. 212 | missing-member-hint=yes 213 | 214 | # The minimum edit distance a name should have in order to be considered a 215 | # similar match for a missing member name. 216 | missing-member-hint-distance=1 217 | 218 | # The total number of similar names that should be taken in consideration when 219 | # showing a hint for a missing member. 220 | missing-member-max-choices=1 221 | 222 | # List of decorators that change the signature of a decorated function. 223 | signature-mutators= 224 | 225 | 226 | [VARIABLES] 227 | 228 | # List of additional names supposed to be defined in builtins. Remember that 229 | # you should avoid defining new builtins when possible. 230 | additional-builtins= 231 | 232 | # Tells whether unused global variables should be treated as a violation. 233 | allow-global-unused-variables=yes 234 | 235 | # List of strings which can identify a callback function by name. A callback 236 | # name must start or end with one of those strings. 237 | callbacks=cb_, 238 | _cb 239 | 240 | # A regular expression matching the name of dummy variables (i.e. expected to 241 | # not be used). 242 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 243 | 244 | # Argument names that match this expression will be ignored. Default to name 245 | # with leading underscore. 246 | ignored-argument-names=_.*|^ignored_|^unused_ 247 | 248 | # Tells whether we should check for unused import in __init__ files. 249 | init-import=no 250 | 251 | # List of qualified module names which can have objects that can redefine 252 | # builtins. 253 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 254 | 255 | 256 | [FORMAT] 257 | 258 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 259 | expected-line-ending-format= 260 | 261 | # Regexp for a line that is allowed to be longer than the limit. 262 | ignore-long-lines=^\s*(# )??$ 263 | 264 | # Number of spaces of indent required inside a hanging or continued line. 265 | indent-after-paren=4 266 | 267 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 268 | # tab). 269 | indent-string=' ' 270 | 271 | # Maximum number of characters on a single line. 272 | max-line-length=119 273 | 274 | # Maximum number of lines in a module. 275 | max-module-lines=1000 276 | 277 | # Allow the body of a class to be on the same line as the declaration if body 278 | # contains single statement. 279 | single-line-class-stmt=no 280 | 281 | # Allow the body of an if to be on the same line as the test if there is no 282 | # else. 283 | single-line-if-stmt=no 284 | 285 | 286 | [SIMILARITIES] 287 | 288 | # Ignore comments when computing similarities. 289 | ignore-comments=yes 290 | 291 | # Ignore docstrings when computing similarities. 292 | ignore-docstrings=yes 293 | 294 | # Ignore imports when computing similarities. 295 | ignore-imports=no 296 | 297 | # Minimum lines number of a similarity. 298 | min-similarity-lines=4 299 | 300 | 301 | [BASIC] 302 | 303 | # Naming style matching correct argument names. 304 | argument-naming-style=snake_case 305 | 306 | # Regular expression matching correct argument names. Overrides argument- 307 | # naming-style. 308 | #argument-rgx= 309 | 310 | # Naming style matching correct attribute names. 311 | attr-naming-style=snake_case 312 | 313 | # Regular expression matching correct attribute names. Overrides attr-naming- 314 | # style. 315 | #attr-rgx= 316 | 317 | # Bad variable names which should always be refused, separated by a comma. 318 | bad-names=foo, 319 | bar, 320 | baz, 321 | toto, 322 | tutu, 323 | tata 324 | 325 | # Bad variable names regexes, separated by a comma. If names match any regex, 326 | # they will always be refused 327 | bad-names-rgxs= 328 | 329 | # Naming style matching correct class attribute names. 330 | class-attribute-naming-style=any 331 | 332 | # Regular expression matching correct class attribute names. Overrides class- 333 | # attribute-naming-style. 334 | #class-attribute-rgx= 335 | 336 | # Naming style matching correct class names. 337 | class-naming-style=PascalCase 338 | 339 | # Regular expression matching correct class names. Overrides class-naming- 340 | # style. 341 | #class-rgx= 342 | 343 | # Naming style matching correct constant names. 344 | const-naming-style=UPPER_CASE 345 | 346 | # Regular expression matching correct constant names. Overrides const-naming- 347 | # style. 348 | #const-rgx= 349 | 350 | # Minimum line length for functions/classes that require docstrings, shorter 351 | # ones are exempt. 352 | docstring-min-length=-1 353 | 354 | # Naming style matching correct function names. 355 | function-naming-style=snake_case 356 | 357 | # Regular expression matching correct function names. Overrides function- 358 | # naming-style. 359 | #function-rgx= 360 | 361 | # Good variable names which should always be accepted, separated by a comma. 362 | good-names=i, 363 | j, 364 | k, 365 | ex, 366 | Run, 367 | _ 368 | 369 | # Good variable names regexes, separated by a comma. If names match any regex, 370 | # they will always be accepted 371 | good-names-rgxs= 372 | 373 | # Include a hint for the correct naming format with invalid-name. 374 | include-naming-hint=no 375 | 376 | # Naming style matching correct inline iteration names. 377 | inlinevar-naming-style=any 378 | 379 | # Regular expression matching correct inline iteration names. Overrides 380 | # inlinevar-naming-style. 381 | #inlinevar-rgx= 382 | 383 | # Naming style matching correct method names. 384 | method-naming-style=snake_case 385 | 386 | # Regular expression matching correct method names. Overrides method-naming- 387 | # style. 388 | #method-rgx= 389 | 390 | # Naming style matching correct module names. 391 | module-naming-style=snake_case 392 | 393 | # Regular expression matching correct module names. Overrides module-naming- 394 | # style. 395 | #module-rgx= 396 | 397 | # Colon-delimited sets of names that determine each other's naming style when 398 | # the name regexes allow several styles. 399 | name-group= 400 | 401 | # Regular expression which should only match function or class names that do 402 | # not require a docstring. 403 | no-docstring-rgx=^_ 404 | 405 | # List of decorators that produce properties, such as abc.abstractproperty. Add 406 | # to this list to register other decorators that produce valid properties. 407 | # These decorators are taken in consideration only for invalid-name. 408 | property-classes=abc.abstractproperty 409 | 410 | # Naming style matching correct variable names. 411 | variable-naming-style=snake_case 412 | 413 | # Regular expression matching correct variable names. Overrides variable- 414 | # naming-style. 415 | #variable-rgx= 416 | 417 | 418 | [STRING] 419 | 420 | # This flag controls whether inconsistent-quotes generates a warning when the 421 | # character used as a quote delimiter is used inconsistently within a module. 422 | check-quote-consistency=no 423 | 424 | # This flag controls whether the implicit-str-concat should generate a warning 425 | # on implicit string concatenation in sequences defined over several lines. 426 | check-str-concat-over-line-jumps=no 427 | 428 | 429 | [IMPORTS] 430 | 431 | # List of modules that can be imported at any level, not just the top level 432 | # one. 433 | allow-any-import-level= 434 | 435 | # Allow wildcard imports from modules that define __all__. 436 | allow-wildcard-with-all=no 437 | 438 | # Analyse import fallback blocks. This can be used to support both Python 2 and 439 | # 3 compatible code, which means that the block might have code that exists 440 | # only in one or another interpreter, leading to false positives when analysed. 441 | analyse-fallback-blocks=no 442 | 443 | # Deprecated modules which should not be used, separated by a comma. 444 | deprecated-modules=optparse,tkinter.tix 445 | 446 | # Create a graph of external dependencies in the given file (report RP0402 must 447 | # not be disabled). 448 | ext-import-graph= 449 | 450 | # Create a graph of every (i.e. internal and external) dependencies in the 451 | # given file (report RP0402 must not be disabled). 452 | import-graph= 453 | 454 | # Create a graph of internal dependencies in the given file (report RP0402 must 455 | # not be disabled). 456 | int-import-graph= 457 | 458 | # Force import order to recognize a module as part of the standard 459 | # compatibility libraries. 460 | known-standard-library= 461 | 462 | # Force import order to recognize a module as part of a third party library. 463 | known-third-party=enchant 464 | 465 | # Couples of modules and preferred modules, separated by a comma. 466 | preferred-modules= 467 | 468 | 469 | [CLASSES] 470 | 471 | # List of method names used to declare (i.e. assign) instance attributes. 472 | defining-attr-methods=__init__, 473 | __new__, 474 | setUp, 475 | __post_init__ 476 | 477 | # List of member names, which should be excluded from the protected access 478 | # warning. 479 | exclude-protected=_asdict, 480 | _fields, 481 | _replace, 482 | _source, 483 | _make 484 | 485 | # List of valid names for the first argument in a class method. 486 | valid-classmethod-first-arg=cls 487 | 488 | # List of valid names for the first argument in a metaclass class method. 489 | valid-metaclass-classmethod-first-arg=cls 490 | 491 | 492 | [DESIGN] 493 | 494 | # Maximum number of arguments for function / method. 495 | max-args=5 496 | 497 | # Maximum number of attributes for a class (see R0902). 498 | max-attributes=7 499 | 500 | # Maximum number of boolean expressions in an if statement (see R0916). 501 | max-bool-expr=5 502 | 503 | # Maximum number of branch for function / method body. 504 | max-branches=12 505 | 506 | # Maximum number of locals for function / method body. 507 | max-locals=15 508 | 509 | # Maximum number of parents for a class (see R0901). 510 | max-parents=7 511 | 512 | # Maximum number of public methods for a class (see R0904). 513 | max-public-methods=20 514 | 515 | # Maximum number of return / yield for function / method body. 516 | max-returns=6 517 | 518 | # Maximum number of statements in function / method body. 519 | max-statements=50 520 | 521 | # Minimum number of public methods for a class (see R0903). 522 | min-public-methods=2 523 | 524 | 525 | [EXCEPTIONS] 526 | 527 | # Exceptions that will emit a warning when being caught. Defaults to 528 | # "BaseException, Exception". 529 | overgeneral-exceptions=builtins.BaseException, 530 | builtins.Exception 531 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kyle.harrison.dev@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.5-alpine 2 | 3 | # Don't write .pyc files (or __pycache__ dirs) inside the container 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | 6 | RUN /sbin/apk add --no-cache --virtual .deps gcc musl-dev libffi-dev make 7 | 8 | # Install Python dependencies from PyPI 9 | COPY requirements*.txt ./ 10 | 11 | RUN pip install --upgrade pip setuptools wheel \ 12 | && pip install --no-cache --force-reinstall --ignore-installed -r requirements_dev.txt 13 | 14 | 15 | # Copy application source code into container 16 | WORKDIR /usr/src/app 17 | COPY . . 18 | 19 | CMD [ "bash" ] 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kyle Harrison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help build.test build.cli run start debug stop clean logs shell lint test 2 | 3 | GIT_SHA = $(shell git rev-parse HEAD) 4 | DOCKER_REPOTAG = $(DOCKER_REGISTRY)/$(DOCKER_IMAGE_NAME):$(GIT_SHA) 5 | 6 | default: help 7 | 8 | build: build.cli build.test ## Build the cli and test containers 9 | 10 | build.cli: ## Build the cli container 11 | @docker-compose build cli 12 | 13 | build.test: ## Build the test container 14 | @docker-compose build test 15 | 16 | build.lint: ## Build the lint container 17 | @docker-compose build test 18 | 19 | build.all: build.cli build.test build.lint ## Build all containers 20 | 21 | help: ## show this help 22 | @echo 23 | @fgrep -h " ## " $(MAKEFILE_LIST) | fgrep -v fgrep | sed -Ee 's/([a-z.]*):[^#]*##(.*)/\1##\2/' | column -t -s "##" 24 | @echo 25 | 26 | run: start ## run the cli locally 27 | 28 | start: ## run the cli locally in the background 29 | @docker-compose up --build cli 30 | 31 | stop: ## stop the cli 32 | @docker-compose down --remove-orphans 33 | 34 | clean: ## delete all data from the local databases 35 | @docker-compose down --remove-orphans --volumes 36 | 37 | network: ## Create the deps network if it doesn't exist 38 | docker network create --driver bridge deps || true 39 | 40 | shell: ## shell into a development container 41 | @docker-compose build cli 42 | @docker-compose run --rm cli sh 43 | 44 | test: build.test network ## Run the unit tests and linters 45 | @docker-compose -f docker-compose.yml run --rm test 46 | @docker-compose down 47 | 48 | lint: ## lint and autocorrect the code 49 | @docker-compose build test 50 | @docker-compose run --rm --no-deps test isort . && black --check . && mypy . && flake8 . && pylint --rcfile=.pylintrc deps && bandit deps && vulture --min-confidence 90 deps && codespell deps && find . -name '*.py' -exec pyupgrade {} + 51 | 52 | 53 | install: ## build and install the cli 54 | sudo pip install -e . 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deps 2 | 3 | [![Tests](https://github.com/apoclyps/deps/actions/workflows/test.yml/badge.svg)](https://github.com/apoclyps/deps/actions/workflows/test.yml) 4 | ![pypi](https://img.shields.io/pypi/v/deps.svg) 5 | ![versions](https://img.shields.io/pypi/pyversions/deps.svg) 6 | 7 | ![](https://banners.beyondco.de/deps.png?theme=light&packageManager=pip+install&packageName=deps&pattern=architect&style=style_1&description=Improve+visibility+of+your+dependencies&md=1&showWatermark=1&fontSize=100px&images=https%3A%2F%2Flaravel.com%2Fimg%2Flogomark.min.svg) 8 | 9 | Simplify managing dependencies within an all-in-one TUI dashboard. 10 | 11 | ## How to use deps 12 | 13 | You will need to create a GITHUB_TOKEN with permissions via [Github > Settings > Developer Settings](https://github.com/settings/tokens/new) with the `repo` permissions to read public/private repositories and and `admin:org` for `read:org` if you wish to access an organisation that is not public. 14 | 15 | ```bash 16 | pip install deps 17 | 18 | # your github username 19 | export GITHUB_USER="user" 20 | # an individual or an organisation in which the repository exists 21 | export GITHUB_ORG="org" 22 | # a comma separated list of repositories 23 | export GITHUB_REPOSITORIES="repo_1,repo_2" 24 | # your personal github token 25 | export GITHUB_TOKEN="secret" 26 | 27 | # optional - export to svg 28 | export DEPS_EXPORT_TO_SVG=false 29 | 30 | deps check 31 | ``` 32 | 33 | ### Configuration 34 | 35 | Deps supports both .ini and .env files. Deps always searches for configuration in this order: 36 | 37 | - Environment variables; 38 | - Repository: ini or .env file; 39 | - Configuration Path 40 | - Review Defaults 41 | 42 | The following steps are used to provide the configuration using a `.env` or `.ini` file. The configuration can be read from within the module/repository (default location set by decouple) using the `.env` file or via a location specified by an environmental variable that points to a `.ini` file located in the root of the project or in a location specified by `PATH_TO_CONFIG`. 43 | 44 | #### Using an `.env` file within the repository 45 | 46 | ```bash 47 | cd /home//workspace/apoclyps/deps 48 | touch .env 49 | 50 | echo "GITHUB_ORG=apoclyps" >> .env 51 | echo "GITHUB_REPOSITORIES=micropython-by-example" >> .env 52 | python -m deps config 53 | ``` 54 | 55 | #### Using an `.ini` file within the repository 56 | 57 | ```bash 58 | cd /home//workspace/apoclyps/deps 59 | touch settings.ini 60 | echo "[settings]\nGITHUB_ORG=apoclyps\nGITHUB_REPOSITORIES=micropython-by-example" >> settings.ini 61 | 62 | python -m deps config 63 | ``` 64 | 65 | #### Providing a configuration path 66 | 67 | If you wish to set the configuration path to use an `ini` or `.env` file when running the application, you can use the configuration of a specific file by supplying the path to the configuration like so: 68 | 69 | ```bash 70 | cd /home/apoclyps/ 71 | touch settings.ini 72 | echo "[settings]\nGITHUB_ORG=apoclyps\nGITHUB_REPOSITORIES=micropython-by-example" >> settings.ini 73 | 74 | cd /home//workspace/apoclyps/deps 75 | export DEPS_PATH_TO_CONFIG=/home// 76 | 77 | python -m deps config 78 | ``` 79 | 80 | If at any time, you want to confirm your configuration reflects the file you have provided, you can use `deps config` to view what current configuration of Deps. 81 | 82 | #### Configuring Layout 83 | 84 | ## Getting started with local development 85 | 86 | To build and run the CLI on your host, you will need Python 3.9 or greater, pip, and virtualenv to build and run `deps`. 87 | If you wish to publish a PR with your changes, first create a fork on Github and clone that code. 88 | 89 | ```bash 90 | $ gh repo clone apoclyps/deps 91 | $ cd deps 92 | $ python3 -m venv env 93 | $ source env/bin/activate 94 | (env)$ pip install -r requirements_dev.txt 95 | (env)$ pip install -r requirements.txt 96 | (env)$ python -m deps check 97 | ``` 98 | 99 | If you wish to keep a copy of Deps on your host system, you can install and run it using: 100 | 101 | ```bash 102 | python -m venv env 103 | source env/bin/activate 104 | python -m pip install -e . 105 | deps -h 106 | ``` 107 | 108 | You can run the Deps within Docker: 109 | 110 | ```bash 111 | docker-compose build cli && docker-compose run --rm cli python -m deps check 112 | ``` 113 | 114 | To build an image and run that image with all of the necessary dependencies using the following commands: 115 | 116 | ```bash 117 | docker-compose build cli 118 | docker-compose run --rm cli python -m deps check 119 | ``` 120 | 121 | ## Testing 122 | 123 | A test suite has been included to ensure Deps functions correctly. 124 | 125 | To run the entire test suite with verbose output, run the following: 126 | 127 | ```bash 128 | make test 129 | ``` 130 | 131 | ## Linting 132 | 133 | To run individual linting steps: 134 | 135 | ```bash 136 | make lint 137 | ``` 138 | 139 | You can also set up `pre-commit` to run the linting steps automatically during the commit phase, 140 | the pre-commit pipeline can be set up by running the following command on the project root: 141 | 142 | ```bash 143 | pre-commit install 144 | ``` 145 | 146 | To run all checks manually: 147 | 148 | ```bash 149 | pre-commit run --all 150 | ``` 151 | 152 | ## Contributions 153 | 154 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 155 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Deps Open Source Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the deps Open Source project as found on [https://github.com/atomist](https://github.com/apoclyps/deps). 4 | 5 | ## Reporting a Vulnerability 6 | 7 | The deps team and community take all security vulnerabilities seriously. 8 | Thank you for improving the security of our open source software. 9 | We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. 10 | 11 | Report security vulnerabilities by emailing the maintain of this project at: 12 | 13 | kyle.harrison.dev+security@gmail.com 14 | 15 | The maintainer will acknowledge your email within 72 hours, and will send a more detailed response within 7 days indicating the next steps in handling your report. After the initial reply to your report, the maintainer will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. 16 | 17 | Report security vulnerabilities in third-party modules to the person or team maintaining the module. 18 | 19 | ## Disclosure Policy 20 | 21 | When the maintainer receives a security bug report, they will coordinate the fix and release process, involving the following steps: 22 | 23 | * Confirm the problem and determine the affected versions. 24 | * Audit code to find any potential similar problems. 25 | * Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible to PyPi. 26 | -------------------------------------------------------------------------------- /cli-runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from deps.cli.main import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /deps/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ # NOQA: F401 2 | -------------------------------------------------------------------------------- /deps/__main__.py: -------------------------------------------------------------------------------- 1 | """Deps CLI - Main script.""" 2 | 3 | from .cli.main import main 4 | 5 | if __name__ == "__main__": 6 | main() 7 | -------------------------------------------------------------------------------- /deps/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/deps/cli/__init__.py -------------------------------------------------------------------------------- /deps/cli/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | from rich.console import Console 3 | 4 | from deps.environment_configuration.view import EnvironmentConfigurationView 5 | from deps.pipenv.view import DependenciesView 6 | 7 | from ..version import __version__ 8 | 9 | CONTEXT_SETTINGS: dict[str, list[str]] = {"help_option_names": ["-h", "--help"]} 10 | 11 | 12 | @click.group(context_settings=CONTEXT_SETTINGS) 13 | @click.version_option(__version__, "-v", "--version", message="version %(version)s") 14 | def cli() -> None: 15 | """Deps - A terminal UI Dashboard for monitoring dependencies across multiple repositories.\n 16 | 17 | For feature requests or bug reports: https://github.com/apoclyps/deps/issues 18 | """ 19 | 20 | 21 | @cli.command(help="List out-dated dependencies") 22 | def check() -> None: 23 | """ 24 | Command:\n 25 | deps check 26 | """ 27 | console = Console() 28 | DependenciesView(console=console).render() 29 | 30 | 31 | @cli.command(help="Show the current configuration used by Deps") 32 | @click.option("-show", "--show/--hide", default=False, is_flag=True) 33 | def config(show: bool) -> None: 34 | """ 35 | Command:\n 36 | deps config 37 | Usage:\n 38 | deps config --show \n 39 | deps config --hide \n 40 | """ 41 | console = Console() 42 | EnvironmentConfigurationView(console=console).render(show=show) 43 | 44 | 45 | def main() -> None: 46 | """Entry point to CLI""" 47 | cli() 48 | -------------------------------------------------------------------------------- /deps/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from decouple import AutoConfig, Csv 4 | 5 | # configures decouple to use settings.ini or .env file from another directory 6 | if DEPS_PATH_TO_CONFIG := os.environ.get("DEPS_PATH_TO_CONFIG", None): 7 | config = AutoConfig(search_path=DEPS_PATH_TO_CONFIG) 8 | else: 9 | config = AutoConfig(search_path=".") 10 | 11 | 12 | # Github Config 13 | GITHUB_DEFAULT_PAGE_SIZE = config("GITHUB_DEFAULT_PAGE_SIZE", cast=int, default=100) 14 | GITHUB_ORG = config("GITHUB_ORG", cast=str) 15 | GITHUB_REPOSITORIES = config("GITHUB_REPOSITORIES", cast=Csv(post_process=list[str], strip=" ")) 16 | GITHUB_TOKEN = config("GITHUB_TOKEN", cast=str) 17 | GITHUB_URL = config("GITHUB_URL", cast=str, default="https://api.github.com") 18 | GITHUB_USER = config("GITHUB_USER", cast=str) 19 | 20 | # External Requests Config 21 | EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS = config("EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS", cast=int, default=3) 22 | EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS = config( 23 | "EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS", cast=int, default=250 24 | ) 25 | 26 | # Deps Config 27 | DEPS_EXPORT_TO_SVG = config("DEPS_EXPORT_TO_SVG", cast=bool, default=False) 28 | -------------------------------------------------------------------------------- /deps/environment_configuration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/deps/environment_configuration/__init__.py -------------------------------------------------------------------------------- /deps/environment_configuration/view.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from rich.console import Console 4 | from rich.table import Table 5 | 6 | from deps import config 7 | 8 | 9 | @dataclass 10 | class EnvironmentConfigurationView: 11 | """Responsible for rendering a table of all dependencies grouped by service""" 12 | 13 | console: Console 14 | 15 | def __init__(self, console: Console) -> None: 16 | """Initialize the view""" 17 | 18 | self.console = console 19 | 20 | def render(self, show: bool) -> None: 21 | """Render the view""" 22 | 23 | configurations = [ 24 | { 25 | "name": "GITHUB_TOKEN", 26 | "value": config.GITHUB_TOKEN if show else "".join("*" for _ in range(len(config.GITHUB_TOKEN))), 27 | }, 28 | {"name": "GITHUB_USER", "value": config.GITHUB_USER}, 29 | {"name": "GITHUB_URL", "value": config.GITHUB_URL}, 30 | { 31 | "name": "DEPS_PATH_TO_CONFIG", 32 | "value": f"{config.DEPS_PATH_TO_CONFIG}", 33 | }, 34 | { 35 | "name": "GITHUB_DEFAULT_PAGE_SIZE", 36 | "value": f"{config.GITHUB_DEFAULT_PAGE_SIZE}", 37 | }, 38 | { 39 | "name": "EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS", 40 | "value": f"{config.EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS}", 41 | }, 42 | { 43 | "name": "EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS", 44 | "value": f"{config.EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS}", 45 | }, 46 | {"name": "DEPS_EXPORT_TO_SVG", "value": f"{config.DEPS_EXPORT_TO_SVG}"}, 47 | { 48 | "name": "GITHUB_REPOSITORIES", 49 | "value": ", ".join(config.GITHUB_REPOSITORIES), 50 | }, 51 | ] 52 | 53 | table = Table() 54 | table.add_column("Name", style="white", no_wrap=True) 55 | table.add_column("Value", style="cyan") 56 | 57 | for configuration in configurations: 58 | table.add_row(configuration["name"], configuration["value"]) 59 | 60 | self.console.print(table) 61 | -------------------------------------------------------------------------------- /deps/pipenv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/deps/pipenv/__init__.py -------------------------------------------------------------------------------- /deps/pipenv/resolver.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from re import match 3 | from typing import Optional 4 | 5 | import toml 6 | from requests import get 7 | from requests.auth import HTTPBasicAuth 8 | 9 | from deps.config import GITHUB_TOKEN, GITHUB_USER 10 | from deps.storage import cache 11 | 12 | REGEX_PIPENV_DEPENDENCY_LINE = r"^(?P.*)\s*\=\s*\"\=\=?(?P.*)\"$" 13 | REGEX_PIP_DEPENDENCY_LINE = r"^(?P.*)==(?P.*)$" 14 | 15 | 16 | @dataclass 17 | class DependenciesResolver: 18 | """Resolves package versions from Pipfile against PyPI""" 19 | 20 | versions_by_service: dict 21 | auth = None 22 | 23 | def __init__(self) -> None: 24 | """Initialize the resolver""" 25 | self.versions_by_service = {} 26 | self.auth = HTTPBasicAuth(GITHUB_USER, GITHUB_TOKEN) 27 | 28 | def retrieve_file(self, repo: str, urls: list[str]) -> str | None: 29 | """Retrieve the Pipfile from GitHub""" 30 | for url in urls: 31 | response = get(url=url, auth=self.auth, timeout=10) 32 | 33 | if response.status_code == 200: 34 | info: str = response.content.decode(encoding="UTF8") 35 | 36 | return info 37 | 38 | return None 39 | 40 | def compare_package_versions(self, repo: str, lines: str | None) -> dict: 41 | """Compare the package versions""" 42 | if not lines: 43 | return self.versions_by_service 44 | 45 | for line in lines.split("\n"): 46 | name: Optional[str] = None 47 | version: Optional[str] = None 48 | 49 | if pypi_match := match(REGEX_PIPENV_DEPENDENCY_LINE, line): 50 | name = pypi_match["name"].strip() 51 | version = pypi_match["version"].strip() 52 | elif pypi_match := match(REGEX_PIP_DEPENDENCY_LINE, line): 53 | name = pypi_match["name"].strip() 54 | version = pypi_match["version"].strip() 55 | else: 56 | # dependency does not match either pipfile or requirements.txt - skipping 57 | continue 58 | 59 | # replace invalid characters in the name 60 | name = name.replace("=", "") 61 | 62 | self._add_version_by_service(repo=repo, name=name, version=version) 63 | 64 | return self.versions_by_service 65 | 66 | def _add_version_by_service(self, repo: str, name: str, version: str) -> None: 67 | """Adds a package name and version to the versions used by that service""" 68 | if repo not in self.versions_by_service: 69 | self.versions_by_service[repo] = [] 70 | 71 | info: dict[str, str] | None = cache.get(name=name) 72 | 73 | if info: 74 | available_version: str = info["available_version"] 75 | release_url: str = info["release_url"] 76 | 77 | self.versions_by_service[repo].append( 78 | { 79 | "dependency_name": name, 80 | "current_version": version, 81 | "available_version": available_version, 82 | "release_url": release_url, 83 | } 84 | ) 85 | 86 | def extract_package_versions(self, repo: str, content: str | None) -> dict: 87 | """Compare the package versions""" 88 | if not content: 89 | return self.versions_by_service 90 | 91 | data: dict = toml.loads(content) 92 | poetry_sections: dict = data.get("tool", {}).get("poetry", {}) 93 | 94 | for name, version in poetry_sections.get("dependencies", {}).items(): 95 | if not isinstance(version, str): 96 | continue 97 | 98 | version = version.replace("=", "") 99 | self._add_version_by_service(repo=repo, name=name, version=version) 100 | 101 | for name, version in poetry_sections.get("group", {}).get("dev", {}).get("dependencies", {}).items(): 102 | if not isinstance(version, str): 103 | continue 104 | 105 | version = version.replace("=", "") 106 | self._add_version_by_service(repo=repo, name=name, version=version) 107 | 108 | return self.versions_by_service 109 | -------------------------------------------------------------------------------- /deps/pipenv/view.py: -------------------------------------------------------------------------------- 1 | import os 2 | import webbrowser 3 | from concurrent.futures import ThreadPoolExecutor 4 | from dataclasses import dataclass 5 | from typing import Any 6 | 7 | from rich.console import Console 8 | from rich.table import Table 9 | 10 | from deps.config import DEPS_EXPORT_TO_SVG, GITHUB_ORG, GITHUB_REPOSITORIES 11 | from deps.pipenv.resolver import DependenciesResolver 12 | 13 | 14 | @dataclass 15 | class DependenciesView: 16 | """Responsible for rendering a table of all dependencies grouped by service""" 17 | 18 | console: Console 19 | used_versions_by_service: dict 20 | resolver: DependenciesResolver 21 | 22 | def __init__(self, console: Console) -> None: 23 | """Initialize the view""" 24 | console.record = DEPS_EXPORT_TO_SVG 25 | 26 | self.used_versions_by_service = {} 27 | self.console = console 28 | self.resolver = DependenciesResolver() 29 | 30 | def _retrieve(self, repo: str) -> dict: 31 | is_requirements: bool = False 32 | is_pipenv: bool = False 33 | is_poetry: bool = False 34 | 35 | requirements_urls: list[str] = [ 36 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/main/requirements.txt", 37 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/master/requirements.txt", 38 | ] 39 | 40 | pipenv_urls: list[str] = [ 41 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/main/Pipfile", 42 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/master/Pipfile", 43 | ] 44 | 45 | poetry_urls: list[str] = [ 46 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/main/pyproject.toml", 47 | f"https://raw.githubusercontent.com/{GITHUB_ORG}/{repo}/master/pyproject.toml", 48 | ] 49 | 50 | file_dependencies: str | None = self.resolver.retrieve_file(repo=repo, urls=requirements_urls) 51 | is_requirements = bool(file_dependencies) 52 | 53 | if not is_requirements: 54 | file_dependencies = self.resolver.retrieve_file(repo=repo, urls=pipenv_urls) 55 | is_pipenv = bool(file_dependencies) 56 | 57 | if not is_pipenv: 58 | file_dependencies = self.resolver.retrieve_file(repo=repo, urls=poetry_urls) 59 | is_poetry = bool(file_dependencies) 60 | 61 | used_versions_by_service: dict = {} 62 | 63 | if is_requirements or is_pipenv: 64 | used_versions_by_service = self.resolver.compare_package_versions(repo=repo, lines=file_dependencies) 65 | elif is_poetry: 66 | used_versions_by_service = self.resolver.extract_package_versions(repo=repo, content=file_dependencies) 67 | 68 | return used_versions_by_service 69 | 70 | def render(self) -> None: 71 | """Render the view""" 72 | 73 | with ThreadPoolExecutor(max_workers=5) as executor: 74 | used_versions_by_services: list[dict] = list(executor.map(self._retrieve, GITHUB_REPOSITORIES)) 75 | 76 | for used_versions_by_service in used_versions_by_services: 77 | if used_versions_by_service: 78 | self.used_versions_by_service = self.used_versions_by_service | used_versions_by_service 79 | 80 | if self.used_versions_by_service: 81 | for service_name, dependencies in self.used_versions_by_service.items(): 82 | table = self.render_service_dependencies( 83 | service_name=service_name, 84 | dependencies=dependencies, 85 | ) 86 | self.console.print(table, justify="center") 87 | else: 88 | self.console.print("No dependencies found") 89 | 90 | if DEPS_EXPORT_TO_SVG: 91 | self.console.save_svg("dependencies.svg", title="dependencies.py") 92 | 93 | webbrowser.open(f"file://{os.path.abspath('dependencies.svg')}") 94 | 95 | def render_service_dependencies(self, service_name: str, dependencies: list[dict[Any, Any]]) -> Table: 96 | """Render the service dependencies""" 97 | table: Table = Table(title=service_name, title_style="bold white", title_justify="center") 98 | 99 | table.add_column("Dependency Name", style="white", no_wrap=True, min_width=40) 100 | table.add_column("Current Version", justify="right", style="red", min_width=30) 101 | table.add_column("Available Version", justify="right", min_width=30) 102 | 103 | for dependency in dependencies: 104 | current_version: str = dependency["current_version"] 105 | available_version: str = dependency["available_version"] 106 | release_url: str = dependency["release_url"] 107 | 108 | # filter out versions that are identical 109 | if current_version != available_version: 110 | table.add_row( 111 | dependency["dependency_name"], 112 | current_version, 113 | f"[link={release_url}][green]{available_version}[/green][/]", 114 | ) 115 | 116 | return table 117 | -------------------------------------------------------------------------------- /deps/storage.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from http import HTTPStatus 3 | 4 | import requests 5 | from requests.exceptions import ConnectionError as HTTPConnectionError, HTTPError 6 | from retrying import retry 7 | 8 | from deps.config import EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS, EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS 9 | 10 | 11 | def retry_if_connection_or_http_error(exception: Exception) -> bool: 12 | """Retries on connection and HTTP errors""" 13 | if isinstance(exception, (HTTPConnectionError, HTTPError)): 14 | return True 15 | 16 | return False 17 | 18 | 19 | @dataclass 20 | class PackageCache: 21 | """A cache of the latest versions of packages""" 22 | 23 | cache: dict[str, dict[str, str]] 24 | 25 | def __init__(self) -> None: 26 | """Initialize the cache""" 27 | self.cache: dict[str, str] = {} 28 | 29 | @retry( 30 | retry_on_exception=retry_if_connection_or_http_error, 31 | stop_max_attempt_number=EXTERNAL_SERVICE_MAXIMUM_REQUEST_ATTEMPTS, 32 | wait_exponential_multiplier=EXTERNAL_SERVICE_WAIT_IN_MS_BETWEEN_REQUESTS, 33 | ) 34 | def retrieve(self, name: str) -> None: 35 | """Retrieve the latest version of a package from PyPI""" 36 | response = requests.get(f"https://pypi.org/pypi/{name}/json", timeout=10) 37 | 38 | if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): 39 | response.raise_for_status() 40 | 41 | if response.status_code != HTTPStatus.NOT_FOUND: 42 | if info := response.json().get("info"): 43 | self.cache[name] = { 44 | "available_version": str(info["version"]), 45 | "release_url": str(info["release_url"]), 46 | } 47 | 48 | def get(self, name: str) -> dict[str, str] | None: 49 | """Get the latest version of a package from the cache""" 50 | if name not in self.cache: 51 | self.retrieve(name) 52 | 53 | if name not in self.cache: 54 | return None 55 | 56 | return self.cache[name] 57 | 58 | 59 | cache: PackageCache = PackageCache() 60 | -------------------------------------------------------------------------------- /deps/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.3" 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | cli: &cli 4 | build: . 5 | command: python -m deps check 6 | working_dir: /usr/src/app/ 7 | environment: 8 | - DEBUG=true 9 | - GITHUB_ORG=${GITHUB_ORG} 10 | - GITHUB_REPOSITORIES=${GITHUB_REPOSITORIES} 11 | - GITHUB_TOKEN=${GITHUB_TOKEN} 12 | - GITHUB_USER=${GITHUB_USER} 13 | volumes: 14 | - "./:/usr/src/app/" 15 | stdin_open: true 16 | tty: true 17 | 18 | test: 19 | <<: *cli 20 | environment: 21 | - GITHUB_ORG=${GITHUB_ORG-"org"} 22 | - GITHUB_REPOSITORIES=${GITHUB_REPOSITORIES-"repo1,repo2"} 23 | - GITHUB_TOKEN=${GITHUB_TOKEN-"token"} 24 | - GITHUB_USER=${GITHUB_USER-"user"} 25 | command: pytest -s -vvv 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.codespell] 2 | quiet-level = 3 3 | builtin = "clear,rare,informal,code,names" 4 | ignore-words-list = "pullrequest" 5 | 6 | [tool.isort] 7 | profile="black" 8 | combine_as_imports=true 9 | default_section="THIRDPARTY" 10 | force_grid_wrap=0 11 | include_trailing_comma=true 12 | known_first_party=["deps", "app", "lib"] 13 | line_length=119 14 | multi_line_output=3 15 | skip=[".git", "__pycache", ".venv", "settings", ".tox"] 16 | use_parentheses=true 17 | 18 | [tool.black] 19 | line_length=119 20 | 21 | [tool.mypy] 22 | ignore_missing_imports=true 23 | pretty=true 24 | 25 | [[tool.mypy.overrides]] 26 | module = "app.*" 27 | ignore_missing_imports=false 28 | disallow_untyped_defs=true 29 | 30 | [[tool.mypy.overrides]] 31 | module = "lib.*" 32 | ignore_missing_imports=false 33 | disallow_untyped_defs=true 34 | 35 | [[tool.mypy.overrides]] 36 | module = "tests.*" 37 | disallow_untyped_defs=true 38 | 39 | [tool.pytest.ini_options] 40 | addopts = "-vvv -s " 41 | testpaths = [ 42 | "app", 43 | "tests", 44 | ] 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.6 2 | humanize==4.7.0 3 | idna==3.4 4 | keyboard==0.13.5 5 | PyGithub==1.59.1 6 | python-decouple==3.8 7 | retrying==1.3.4 8 | rich==13.5.2 9 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | bandit==1.7.5 4 | black==23.7.0 5 | codespell==2.2.5 6 | flake8-unused-arguments==0.0.13 7 | flake8==6.1.0 8 | freezegun==1.2.2 9 | ipdb==0.13.13 10 | isort==5.12.0 11 | mypy==1.5.0 12 | pre-commit==3.6.0 13 | pylint==2.17.5 14 | pytest==7.4.2 15 | pyupgrade==3.10.1 16 | types-click==7.1.8 17 | types-freezegun==1.1.10 18 | types-requests==2.31.0.2 19 | types-toml==0.10.8.7 20 | vulture==2.8 21 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/scripts/__init__.py -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_namespace_packages, setup 2 | 3 | 4 | def _requires_from_file(filename): 5 | with open(filename, encoding="utf-8") as f: 6 | return f.read().splitlines() 7 | 8 | 9 | with open("README.md", encoding="utf-8") as fh: 10 | long_description = fh.read() 11 | 12 | 13 | setup( 14 | name="deps", 15 | packages=find_namespace_packages(include=["*"]), 16 | version="0.2.3", 17 | license="MIT", 18 | description=("A terminal UI dashboard to view python dependencies across Github repositories."), 19 | author="Kyle Harrison", 20 | author_email="kyle.harrison.dev@gmail.com", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | download_url="https://pypi.org/project/deps/", 24 | project_urls={ 25 | "Funding": "https://ko-fi.com/apoclyps", 26 | "Say Thanks!": "https://twitter.com/apoclyps", 27 | "Source": "https://github.com/apoclyps/deps", 28 | "Tracker": "https://github.com/apoclyps/deps/issues", 29 | }, 30 | keywords=["Deps"], 31 | install_requires=_requires_from_file("requirements.txt"), 32 | entry_points={"console_scripts": ["deps = deps.cli.main:main"]}, 33 | classifiers=[ 34 | "Development Status :: 3 - Alpha", 35 | "Environment :: Console", 36 | "Intended Audience :: Developers", 37 | "License :: OSI Approved :: MIT License", 38 | "Natural Language :: English", 39 | "Operating System :: MacOS", 40 | "Operating System :: Microsoft :: Windows :: Windows 10", 41 | "Operating System :: Unix", 42 | "Programming Language :: Python :: 3.10", 43 | "Programming Language :: Python :: 3.11", 44 | "Topic :: Terminals", 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apoclyps/deps/e69962721918049cc9ceaaba82bc99dc0ffc3536/tests/conftest.py -------------------------------------------------------------------------------- /tests/test_pass.py: -------------------------------------------------------------------------------- 1 | def test_passes(): 2 | assert True is True 3 | --------------------------------------------------------------------------------