From be2038776171479a215b9ddbb20fdfa8cbdfa2f8 Mon Sep 17 00:00:00 2001 From: Petter Friberg Date: Thu, 14 Dec 2023 10:40:52 +0100 Subject: [PATCH] Prepare for a `0.0.1` (#2) * Use [hatch](https://hatch.pypa.io/latest/) as build system * Add `pyproject.toml` and `tox.ini` linters * Add GitHub actions workflows --- .github/dependabot.yaml | 6 ++ .github/workflows/ci.yaml | 69 ++++++++++++++ .github/workflows/release.yaml | 11 +++ .gitignore | 2 +- .pre-commit-config.yaml | 18 +++- flake.nix | 1 + pyproject.toml | 134 ++++++++++++++++++--------- src/bankid_sdk/__init__.py | 13 ++- src/bankid_sdk/_storage.py | 12 --- tests/conftest.py | 2 +- tests/contrib/django/test_request.py | 20 ++-- tests/contrib/django/test_rest.py | 4 +- tox.ini | 54 +++++++++++ 13 files changed, 263 insertions(+), 83 deletions(-) create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 tox.ini diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..1230149 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..84b627f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,69 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref | github.run_id }} + cancel-in-progress: true + +jobs: + test: + name: Test ${{ matrix.py }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + py: + - "3.12" + - "3.11" + - "3.10" + - "3.9" + steps: + - uses: actions/checkout@v4 + - name: Setup python for tox + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install tox + - uses: actions/checkout@v4 + - name: Setup python for test ${{ matrix.py }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.py }} + - name: Pick environment to test + shell: python + run: | + import codecs; import os; import sys + env = "TOXENV=py{}{}\n".format(*sys.version_info[:2]) + print(f"Picked:\n{env}for{sys.version}") + with codecs.open(os.environ["GITHUB_ENV"], "a", "utf-8") as file_handler: + file_handler.write(env) + - name: Setup test suite + run: tox -vv --notest + - name: Run test suite + run: tox --skip-pkg-install + + static-typing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.9' + - run: python -m pip install tox + - name: Run mypy + run: tox -e mypy + + lint: + name: Lint + uses: less-action/reusables/.github/workflows/pre-commit.yaml@main + with: + python-version: "3.12" + + check-build: + name: Check packaging + uses: less-action/reusables/.github/workflows/python-test-build.yaml@main diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..5df659d --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,11 @@ +name: Release + +on: + release: + types: [published] + +jobs: + build-and-publish: + uses: less-action/reusables/.github/workflows/python-publish.yaml@v8 + secrets: + pypi_api_token: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 2688601..2832e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST -src/bankid_sdk/__version__.py +src/bankid_sdk/_version.py # PyInstaller # Usually these files are written by a python script from a template diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b96544f..20b7359 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.9 + python: python3.12 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -12,6 +12,15 @@ repos: - id: trailing-whitespace - id: debug-statements - id: detect-private-key + - repo: https://github.com/tox-dev/tox-ini-fmt + rev: "1.3.1" + hooks: + - id: tox-ini-fmt + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "1.5.3" + hooks: + - id: pyproject-fmt + additional_dependencies: ["tox>=4.9"] - repo: https://github.com/psf/black rev: 23.11.0 hooks: @@ -20,6 +29,11 @@ repos: rev: 'v0.1.5' hooks: - id: ruff + - repo: https://github.com/sirosen/check-jsonschema + rev: 0.27.3 + hooks: + - id: check-github-workflows + args: ["--verbose"] - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.37.0 hooks: @@ -41,7 +55,7 @@ exclude: | | build | dist )/ - | ^src/bankid_api/__version__\.py$ + | ^src/bankid_api/_version\.py$ | ^tests/fixtures/fake_cert\.pem$ | ^tests/fixtures/fake_client\.key$ ) diff --git a/flake.nix b/flake.nix index af9f506..8fd02e5 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,7 @@ ${pkgs.python39}/bin/python -m \ venv --copies --upgrade-deps .venv > /dev/null source .venv/bin/activate + pip install --require-virtualenv --no-input tox ''; }; }); diff --git a/pyproject.toml b/pyproject.toml index 6c17eb6..3c3160f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,59 @@ [build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" +build-backend = "hatchling.build" +requires = [ + "hatch-vcs", + "hatchling", +] [project] name = "bankid-sdk" -version = "0.0.1" description = "A Python SDK for BankID" +readme = "README.md" +keywords = [ + "bankid", + "client", + "django", + "rest", +] +license = "BSD-3-Clause" +authors = [{ name = "Jonas Lundberg", email = "jonas@5monkeys.se" }] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 4 - Beta", + "Framework :: Django", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Typing :: Typed", +] +dynamic = [ + "version", +] dependencies = [ "abcattrs", "httpx", ] - -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ + "bankid-sdk[test]", + "mypy", + "pre-commit", +] +optional-dependencies.django = [ + "django", +] +optional-dependencies.test = [ + "bankid-sdk[django]", "dirty-equals", "django-stubs", "freezegun", - "mypy", - "pre-commit", "pytest", "pytest-asyncio", "pytest-cov", @@ -25,44 +61,13 @@ dev = [ "pytest-randomly", "respx", ] +urls.Homepage = "https://github.com/5monkeys/bankid-sdk" +urls.Source = "https://github.com/5monkeys/bankid-sdk" +urls.Tracker = "https://github.com/5monkeys/bankid-sdk/issues" -[tool.pytest.ini_options] -testpaths = ["tests", "src"] -addopts = "--cov --no-cov-on-fail" -asyncio_mode = "auto" - -[tool.coverage.report] -skip_covered = true -show_missing = true -exclude_also = [ - # ignore non-implementations - '^\s*\.\.\.', - "if TYPE_CHECKING:", - '^\s*(def\s)?assert_never(.*)$', -] -fail_under = 100 - -[tool.coverage.run] -branch = true -source = ["src/"] -omit = [ - "src/bankid_sdk/__version__.py", -] - -[tool.mypy] -python_version = "3.9" -pretty = true -show_error_codes = true -files = "src, tests" -exclude = '''(?x)( - ^src/bankid_sdk/__version__\.py$ -)''' - -no_implicit_optional = true -strict_optional = true -ignore_missing_imports = false -warn_unreachable = true -strict = true +[tool.hatch] +build.hooks.vcs.version-file = "src/bankid_sdk/_version.py" +version.source = "vcs" [tool.black] target-version = ["py39"] @@ -124,3 +129,42 @@ ignore = [ [tool.ruff.isort] known-first-party = ["bankid_sdk", "tests"] combine-as-imports = true + +[tool.pytest.ini_options] +testpaths = ["tests", "src"] +addopts = "--cov --no-cov-on-fail" +asyncio_mode = "auto" + +[tool.coverage.paths] +source = [ + "src/bankid_sdk", + ".tox/*/lib/*/site-packages/bankid_sdk", +] + +[tool.coverage.report] +skip_covered = true +show_missing = true +exclude_also = [ + # ignore non-implementations + '^\s*\.\.\.', + "if TYPE_CHECKING:", + '^\s*(def\s)?assert_never(.*)$', +] +fail_under = 100 + +[tool.coverage.run] +parallel = true +branch = true +source = ["bankid_sdk", "tests"] + +[tool.mypy] +python_version = "3.9" +pretty = true +show_error_codes = true +files = "src, tests" + +no_implicit_optional = true +strict_optional = true +ignore_missing_imports = false +warn_unreachable = true +strict = true diff --git a/src/bankid_sdk/__init__.py b/src/bankid_sdk/__init__.py index 31e83b0..5bab613 100644 --- a/src/bankid_sdk/__init__.py +++ b/src/bankid_sdk/__init__.py @@ -1,5 +1,3 @@ -from importlib.metadata import PackageNotFoundError, version - from ._actions import ( Action, AuthAction, @@ -30,7 +28,13 @@ from .errors import BankIDAPIError, BankIDHTTPError from .typing import OrderRef, PersonalNumber +try: + from ._version import __version__ +except ImportError: # pragma: no cover + __version__ = "unknown" + __all__ = [ + "__version__", "Action", "AuthAction", "AsyncV60", @@ -64,8 +68,3 @@ "generate_qr_code", "init_auth", ] - -try: - __version__ = version(__name__) -except PackageNotFoundError: - __version__ = "unknown" diff --git a/src/bankid_sdk/_storage.py b/src/bankid_sdk/_storage.py index b53e189..15e7a39 100644 --- a/src/bankid_sdk/_storage.py +++ b/src/bankid_sdk/_storage.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import Protocol -from uuid import uuid4 from bankid_sdk.typing import TransactionID @@ -17,14 +16,3 @@ def load(self, key: TransactionID, /) -> Transaction | None: def delete(self, key: TransactionID, /) -> None: ... - - -class NoStorage(Storage): - def save(self, obj: Transaction, /) -> TransactionID: - return TransactionID(str(uuid4())) - - def load(self, key: TransactionID, /) -> Transaction | None: - return None - - def delete(self, key: TransactionID, /) -> None: - ... diff --git a/tests/conftest.py b/tests/conftest.py index d3ece8f..f41f95d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ def configure_django() -> None: try: import django - except ImportError: + except ImportError: # pragma: no cover return from django.conf import settings diff --git a/tests/contrib/django/test_request.py b/tests/contrib/django/test_request.py index 3536cea..478a352 100644 --- a/tests/contrib/django/test_request.py +++ b/tests/contrib/django/test_request.py @@ -41,9 +41,7 @@ def test_returns_raises_error_if_body_is_read_twice(self) -> None: def test_passes_through_empty_body_when_content_length_is_missing(self) -> None: view = mock.MagicMock(return_value=JsonResponse({})) - request = RequestFactory().generic( - "POST", "/", headers={"Content-Type": "application/json"} - ) + request = RequestFactory(CONTENT_TYPE="application/json").generic("POST", "/") response = parse_json_body(view)(request) assert response.status_code == HTTPStatus.OK @@ -51,11 +49,9 @@ def test_passes_through_empty_body_when_content_length_is_missing(self) -> None: def test_passes_through_empty_body_when_content_length_is_zero(self) -> None: view = mock.MagicMock(return_value=JsonResponse({})) - request = RequestFactory().generic( - "POST", - "/", - headers={"Content-Type": "application/json", "Content-Length": "0"}, - ) + request = RequestFactory( + CONTENT_TYPE="application/json", CONTENT_LENGTH="0" + ).generic("POST", "/") response = parse_json_body(view)(request) assert response.status_code == HTTPStatus.OK @@ -65,11 +61,9 @@ def test_passes_through_empty_body_when_content_length_is_invalid_value( self, ) -> None: view = mock.MagicMock(return_value=JsonResponse({})) - request = RequestFactory().generic( - "POST", - "/", - headers={"Content-Type": "application/json", "Content-Length": "abc"}, - ) + request = RequestFactory( + CONTENT_TYPE="application/json", CONTENT_LENGTH="abc" + ).generic("POST", "/") response = parse_json_body(view)(request) assert response.status_code == HTTPStatus.OK diff --git a/tests/contrib/django/test_rest.py b/tests/contrib/django/test_rest.py index 4988bd0..e3c9735 100644 --- a/tests/contrib/django/test_rest.py +++ b/tests/contrib/django/test_rest.py @@ -58,7 +58,7 @@ def initialize( def finalize( self, response: bankid_sdk.CompleteCollect, request: Any, context: Any ) -> None: - return None + ... class FailsInitAction(bankid_sdk.AuthAction): @@ -72,7 +72,7 @@ def initialize( def finalize( self, response: bankid_sdk.CompleteCollect, request: Any, context: Any ) -> None: - return None + ... class FailsFinalizeAction(bankid_sdk.AuthAction): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1be61ac --- /dev/null +++ b/tox.ini @@ -0,0 +1,54 @@ +[tox] +requires = + tox>=4.2 +env_list = + py312-django{50, 42} + py311-django{50, 42, 41} + py310-django{50, 42, 41} + py39-django{42, 41} + mypy +skip_missing_interpreters = true + +[testenv] +description = Run tests with {basepython} +package = wheel +wheel_build_env = .pkg +deps = + django41: {[django]41} + django42: {[django]42} + django50: {[django]50} +extras = + test +pass_env = + CI_RUN + PYTEST_* + TERM +commands = + pytest --cov --no-cov-on-fail --cov-report xml --cov-report term-missing {posargs} + +[testenv:mypy] +description = Run type checking with Mypy +deps = + mypy +commands = + mypy --strict src/ tests/ + +[testenv:dev] +description = Generate a DEV environment +package = editable +extras = + dev +commands = + python -m pip list --format=columns + python -c 'import sys; print(sys.executable)' + +[pytest] +django_find_project = false + +[django] +50 = + Django~=5.0.0 +42 = + Django~=4.2.8 +41 = + Django~=4.1.3