diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..423820e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# http://editorconfig.org + +# EditorConfig помогает поддерживать согласованные стили кодирования для нескольких разработчиков, +# работающих над одним проектом в различных редакторах и IDE. + +root = true + +[*] +charset = utf-8 # Кодировка файлов по умолчанию +# end_of_line = lf # Последовательность конца строки для файлов по умолчанию +indent_style = space # Стиль отступов в файлах по умолчанию +insert_final_newline = true # Автоматически добавлять пустую строку в конце файла +trim_trailing_whitespace = true # Автоматически убирать не используемые пробелы в конце строк + +[*.py] +profile = black +indent_size = 4 +max_line_length = 105 + +[*.{yml,yaml}] +indent_size = 2 + +[*.json] +indent_size = 4 +insert_final_newline = ignore + +[*.{html,md}] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..aae2038 --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,151 @@ +# For more information see: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + +name: Publish Python 🐍 distribution 📦 to PyPI + +# Build on every branch push, tag push, and pull request change +on: + push: + branches: + - main + tags: + - v* + pull_request: + +jobs: + test: + name: Test (💻 ${{ matrix.platform }}, 🐍 ${{ matrix.python-version }}) + strategy: + fail-fast: false + # max-parallel: 5 + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13'] + platform: [ + ubuntu-latest, + macos-latest, + windows-latest, + ] + + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + + - name: Install dependencies with optional + run: | + python -m pip install .[ci] + + - name: Run lint + run: | + python -m ruff check --fix + + - name: Run format + run: | + python -m ruff format + + - name: Run mypy + run: | + python -m mypy + + - name: Run pytest with coverage + run: | + python -m pytest --cov + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + build: + name: Build distribution 📦 + needs: + - test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + + - name: Build a binary wheel and a source tarball + run: python3 -m build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: >- + Publish Python 🐍 distribution 📦 to PyPI + + # only publish to PyPI on release with tag by repo owner + if: github.repository_owner == 'Friskes' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + + needs: + - build + runs-on: ubuntu-latest + + environment: + name: pypi + url: https://pypi.org/p/loadnsi + + permissions: + id-token: write + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + + # publish-to-testpypi: + # name: Publish Python 🐍 distribution 📦 to TestPyPI + + # if: github.repository_owner == 'Friskes' && github.event_name == 'push' + + # needs: + # - build + # runs-on: ubuntu-latest + + # environment: + # name: testpypi + # url: https://test.pypi.org/p/loadnsi + + # permissions: + # id-token: write + + # steps: + # - name: Download all the dists + # uses: actions/download-artifact@v4 + # with: + # name: python-package-distributions + # path: dist/ + + # - name: Publish distribution 📦 to TestPyPI + # uses: pypa/gh-action-pypi-publish@release/v1 + # with: + # repository-url: https://test.pypi.org/legacy/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d838001 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.pyc +*.db +*~ + +build/ +dist/ +venv/ +*.egg-info/ + +!.gitignore + +__pycache__ +.git +trash +.pytest_cache +.ruff_cache +.mypy_cache +.coverage +htmlcov +.vscode +.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..67eb24c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +# https://githooks.com/ +# https://verdantfox.com/blog/how-to-use-git-pre-commit-hooks-the-hard-way-and-the-easy-way +# https://docs.astral.sh/ruff/integrations/ +# https://pre-commit.com/ + +# How to use it? +# pre-commit run # Run the pre-commit check manually. +# pre-commit install # Create a file in the directory to run automatically on commit: .git/hooks/pre-commit +# pre-commit uninstall # Delete a file in the directory to disable automatic startup on commit. +# git commit --no-verify -m "msg" # Disable startup for this commit (--no-verify OR -n) + +repos: + - repo: /~https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-merge-conflict + - id: check-case-conflict + - id: check-toml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: /~https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli + + - repo: /~https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.8.0' + hooks: + - id: ruff + name: Check the codebase using the ruff linter. + args: [ + --fix, + --quiet, + # --silent, + ] + + - id: ruff-format + name: Check the codebase using the ruff formatter. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..570473b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contribution Guide 📖 + +> First off, thanks for taking the time to contribute! 😇 + +Contributions include but are not restricted to: +- Reporting bugs +- Contributing to code +- Writing tests +- Writing documentation + +## A recommended flow of contributing to an Open Source project + +1. First, [fork](/~https://github.com/Friskes/loadnsi/fork) this [project](/~https://github.com/Friskes/loadnsi) to your own namespace using the fork button at the top right of the repository page. + +2. Clone your fork repository to local: + ```bash + git clone /~https://github.com/YOURNAME/loadnsi.git + ``` + +3. Add the fork as a new remote: + ```bash + git remote add fork /~https://github.com/YOURNAME/loadnsi.git + git fetch fork + ``` + +## Local development + +> We recommend working in a [virtual environment](https://docs.python.org/3/tutorial/venv.html). Feel free to create a virtual environment with either the venv module or the virtualenv tool. For example: + +1. Create virtual environment + ```bash + python -m venv venv + . venv/bin/activate # linux + venv/Scripts/activate # windows + ``` + +2. Install development dependencies + ```bash + pip install ."[dev]" # linux + pip install .[dev] # windows + ``` + +3. Install [pre-commit](https://pre-commit.com/) + ```bash + pre-commit install + ``` + +4. (Optional) Run `pre-commit run --all-files` to run linters and formatters. + > This step is optional and will be executed automatically by git before you make a commit, but you may want to run it manually in order to apply fixes. + +#### Now, all dependencies are installed into the Python environment you chose, which will be used for development after this point 🎉. + +## Making changes to the project + +> Don't modify code on the main branch, the main branch should always keep track of origin/main 💡. + +1. (Optional) To update main branch to date: + ```bash + git pull origin main + # In rare cases that your local main branch diverges from the remote main: + git fetch origin && git reset --hard main + ``` + +2. Create a new branch based on the up-to-date main branch for new patches. + +> Code should be [Pythonic and zen](https://peps.python.org/pep-0020/) + +- All functions, methods, classes, and attributes should be documented with a docstring. We use the [Google docstring style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). + > If you come across a function or method that doesn't conform to this standard, please update it as you go + +- Writing and running tests + - Tests are contained within the `tests` directory, and follow the same directory structure as the `loadnsi` module. If you are adding a test case, it should be located within the correct submodule of `tests`. E.g., tests for `loadnsi/core.py` reside in `tests/test_core.py`. + +3. Make your changes + +4. Make sure that nothing is broken after making changes and all tests are checked. + ```bash + pytest + ``` + > The test suite is still simple and needs expansion! Please help write more test cases. + +5. Commit your changes to git 📝. We follow [conventional commits](https://www.conventionalcommits.org/) which are enforced using a pre-commit hook. + +6. Push the changes to your fork + +7. Open a [pull request](https://docs.github.com/en/pull-requests) 📥. *(using the `pull request` button at the top right of the repository page)*. Give the pull request a descriptive title indicating what it changes. The style of the PR title should also follow [conventional commits](https://www.conventionalcommits.org/). + +8. Go to the [Release Action](/~https://github.com/Friskes/loadnsi/actions/workflows/publish-to-pypi.yml) and make sure that the workflow was successful ✅. + +9. Wait until the pull request is accepted by the repository owner ⏳. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc0a173 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Friskes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..33743a7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,263 @@ +[project] +name = "loadnsi" +# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#version +dynamic = ["version"] +license = {file = "LICENSE"} +authors = [ + {name="Friskes", email="friskesx@gmail.com"}, +] +description = "Utility for downloading and updating NSI fixtures" +readme = "README.md" +requires-python = ">=3.8" +keywords = ["Django", "Fixture", "NSI"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Documentation", + "Topic :: Software Development :: Code Generators", +] +# pip install . +dependencies = [ + "click==8.1.8", + "bs4==0.0.2", + "httpx==0.28.0", + "aiofiles==24.1.0", + "tenacity==9.0.0", + "rich==13.9.4", + "python-dotenv==1.0.1", +] + + +# pip install .[dev] +# pip install .[ci] +[project.optional-dependencies] +dev = [ + "pre-commit>=3.5.0", + "ruff==0.8.0", + "pytest>=7.0.1", + "mypy>=1.10.0", + "pytest-cov>=5.0.0", +] +ci = [ + "ruff==0.8.0", + "pytest>=7.0.1", + "mypy>=1.10.0", + "pytest-cov>=5.0.0", +] + + +[project.urls] +Homepage = "/~https://github.com/Friskes/loadnsi" +Changelog = "/~https://github.com/Friskes/loadnsi/releases/" +Issues = "/~https://github.com/Friskes/loadnsi/issues" + + +[build-system] +requires = ["setuptools>=64", "setuptools_scm[toml]>=8"] +build-backend = "setuptools.build_meta" + + +# https://setuptools-scm.readthedocs.io/en/latest/ +# https://stackoverflow.com/a/76470558/19276507 +# Данная настройка позволяет PyPI получать версию пакета из GitHub release tag +[project.entry-points."setuptools.finalize_distribution_options"] +setuptools_scm = "setuptools_scm._integration.setuptools:infer_version" + +[tool.setuptools_scm] + + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +addopts = [ + "--strict-config", + "--strict-markers", +] +xfail_strict = true +filterwarnings = [ + "error", + "ignore::DeprecationWarning:typer", +] + + +[tool.mypy] +# for ignore rules: # type: ignore[error_code] +packages = ["src.loadnsi", "tests"] +python_version = "3.9" +strict = true # строгая проверка типов +ignore_missing_imports = true # [import-untyped] +# Проверять тело функции без типизации аргументов +check_untyped_defs = true # [annotation-unchecked] +# Не ругаться на нетипизированный родительский класс +disallow_subclassing_any = false # [misc] +# Не ругаться на вызов нетипизированной функции +disallow_untyped_calls = false # [no-untyped-call] +# Не ругаться на return -> Any +warn_return_any = false # [no-any-return] +# Не ругаться на определение функций без аннотаций типов или с неполными аннотациями типов +# disallow_untyped_defs = false +# Игнор ряда кодов, глобально по проекту +disable_error_code = [ + "operator", + "assignment", + "union-attr", + "comparison-overlap", +] + + +[tool.coverage.run] +concurrency = ["multiprocessing", "thread"] +omit = ["*/tests/*", "types.py", "apps.py"] # Не проверять директории и(или) файлы +parallel = true +branch = true # Включать в замер результаты вхождения в условия +source = ["src.loadnsi"] + +[tool.coverage.report] +fail_under = 90 # Тест упадёт если покрытие составит менее 90% +skip_covered = true # Не включать в отчет информацию о файлах со 100% покрытием +show_missing = true # Включать в отчет информацию о номерах не протестированных строк кода +sort = "-Cover" # Сортировать отчет по уровню покрытия + + +# https://docs.astral.sh/ruff/settings/ +# https://docs.astral.sh/ruff/configuration/#full-command-line-interface +# cmd: ruff format # Отформатирует все файлы в `.` текущем каталоге. +[tool.ruff] +required-version = "<=0.8.0" +target-version = "py312" # Версия python которую будет обслуживать ruff +line-length = 105 # Макс длина строки кода после которой будет выполнен автоперенос +indent-width = 4 +force-exclude = true # pre-commit файл будет использовать exclude список +src = ["src"] # Корень проекта в котором лежат приложения (необходимо для работы ruff.lint.isort) +extend-exclude = [ + "migrations", +] + + +[tool.ruff.format] +quote-style = "single" # Использовать одинарные кавычки при форматировании файла +indent-style = "space" # Использовать пробелы вместо табуляции при форматировании файла +# line-ending = "lf" # Последовательность конца строки для файлов по умолчанию + + +# https://docs.astral.sh/ruff/linter/ +# cmd: ruff check --fix # Проверить линтером все файлы в `.` текущем каталоге и исправить все безопасные проблемы. +[tool.ruff.lint] +# Настройки по умолчанию: https://docs.astral.sh/ruff/configuration/ +# https://docs.astral.sh/ruff/rules/ +# select по умолчанию: ["E4", "E7", "E9", "F"] +extend-select = [ + "E", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "I", # https://docs.astral.sh/ruff/rules/#isort-i + "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "YTT", # https://docs.astral.sh/ruff/rules/#flake8-2020-ytt + "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async + "BLE", # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "T10", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj + "FA", # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa + "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "ICN", # https://docs.astral.sh/ruff/rules/#flake8-import-conventions-icn + "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "PYI", # https://docs.astral.sh/ruff/rules/#flake8-pyi-pyi + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf + "SLOT", # https://docs.astral.sh/ruff/rules/#flake8-slots-slot + "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "TD", # https://docs.astral.sh/ruff/rules/#flake8-todos-td + "INT", # https://docs.astral.sh/ruff/rules/#flake8-gettext-int + "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly + "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf + "LOG", # https://docs.astral.sh/ruff/rules/#flake8-logging-log + "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + #! https://docs.astral.sh/ruff/rules/#refurb-furb + #! "FURB", # refurb (пока что не стабильно 29.03.2024) +] +ignore = [ + # https://docs.astral.sh/ruff/rules/#pyflakes-f + "F403", # (не ругаться на использование from ... import *) + # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "UP031", # (не ругаться на форматирование с помощью %s) + "UP035", # (не ругаться на импорт из typing_extensions) + "UP036", # (не ругаться на использование sys.version_info если текущая версия не подпадает под условие) + "UP038", # (не ругаться на использование кортежа в isinstance или issubclass) + # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "G004", # (не ругаться на использование f-строк для сообщения лога) + # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "RUF001", # (не ругаться на кириллицу в строках) + "RUF002", # (не ругаться на кириллицу в докстрингах) + "RUF003", # (не ругаться на кириллицу в комментариях) + "RUF012", # (не ругаться на мутабельность коллекций) + # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "PT009", # (не ругаться на unittest ассерты) + "PT027", # (не ругаться на unittest ассерты) + "PT001", # (не ругаться на отсутствие круглых скобок у декоратора fixture) + "PT023", # (не ругаться на отсутствие круглых скобок у декоратора mark.) + "PT004", # (не ругаться на написание фикстур которые ничего не возвращают без префикса "_") + # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "ISC001", # (конфликт с `COM812`) + # https://docs.astral.sh/ruff/rules/#flake8-commas-com + "COM812", # (конфликт с `ISC001`) + # https://docs.astral.sh/ruff/rules/#flake8-self-slf + "SLF001", # (не ругаться на приватные названия объектов) + # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "SIM105", # (не ругаться на использование try except pass блока) + # https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp/ + "SIM108", # (не ругаться на использование классического if-else блока вместо тернарного) + # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "RET503", # (не ругаться на отсутствие return None в конце функций) + # https://docs.astral.sh/ruff/rules/#flake8-blind-except-ble + "BLE001", # (не ругаться на обработку обычного Exception) + # https://docs.astral.sh/ruff/rules/#flake8-django-dj + # https://docs.djangoproject.com/en/4.2/ref/models/fields/#null + # https://sentry.io/answers/django-difference-between-null-and-blank/ + # https://www.django-rest-framework.org/api-guide/fields/#charfield + "DJ001", # (не ругаться на использование null в моделях для текстовых полей) + # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + # {} VS dict() # https://switowski.com/blog/dict-function-vs-literal-syntax/ + "C408", # (не ругаться на использование классов коллекций вместо их литералов) + "TD001", # (не ругаться на использование FIXME и XXX) + "TD003", # (не ругаться на отсутствие ссылки на issues) +] +# Не давать исправлять эти ошибки в тултипе, и в том числе автоматически при линте через команду +unfixable = [ +] + + +# https://docs.astral.sh/ruff/settings/#lintisort +[tool.ruff.lint.isort] +force-wrap-aliases = true # Записывать импорты с несколькими `as` в кортеже +combine-as-imports = true # Записывать импорты с несколькими `as` в одной строке +# force-sort-within-sections = true # Всегда сортировать импорты по алфавиту, не обращая внимание на порядок import или from +# from-first = true # Записывать from импорты перед import +# length-sort = true # Сортировать импорты по длине строки а не по алфавиту +# lines-after-imports = 2 # Количество пустых строк после последнего импорта +# order-by-type = true # Сортировать по регистру в дополнение к сортировке по алфавиту +# required-imports = ["from __future__ import annotations"] # Добавить импорт во все файлы + + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" # Не ругаться на одинарные кавычки +docstring-quotes = "double" # Ругаться на одинарные кавычки diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ea69417 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[metadata] +url = /~https://github.com/Friskes/loadnsi +author = Friskes +author_email = friskesx@gmail.com