diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef0bc68 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Least privilege: nothing here needs to write to the repo. Declaring this at +# the workflow level also overrides a permissive repo/org default. +permissions: + contents: read + +env: + # Assert uv.lock is up to date with pyproject.toml and install exactly what + # it pins, rather than silently resolving something different from what + # developers run locally. (UV_LOCKED, not UV_FROZEN: --frozen skips + # re-locking without checking, --locked fails when the lock is stale.) + UV_LOCKED: "1" + +jobs: + lint: + name: Lint and type-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: ruff check + run: uv run ruff check . + + - name: ruff format --check + run: uv run ruff format --check . + + - name: mypy + run: uv run mypy src/myinvois + + test: + name: Test (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + with: + # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + # Live tests need real MyInvois sandbox credentials, so they are + # deselected here. They skip on their own if MYINVOIS_CLIENT_ID is + # unset, but deselecting keeps the CI summary honest about what ran. + - name: pytest + run: uv run pytest -m "not live" + + package: + name: Build and verify distributions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # Don't leave GITHUB_TOKEN in .git/config for later steps to reach. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Build sdist and wheel + run: uv build + + # Guards two things that are easy to break and painful to un-publish: + # 1. The PEP 561 marker and the JSON code tables must ship, or + # `from myinvois.codes import ...` fails at runtime in a wheel + # install and type-checkers ignore the package. + # 2. The test signing key/cert must NEVER ship. They are force-tracked + # in git for the byte-parity tests (see tests/fixtures/cert/README.md) + # which makes an accidental include plausible. + - name: Verify distribution contents + run: uv run --no-project python scripts/check_dist.py + + - uses: actions/upload-artifact@v4 + with: + name: distributions + path: dist/ diff --git a/AGENTS.md b/AGENTS.md index e2f9ac8..8f9eb18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,18 +116,19 @@ Steps (mirror PHP `AbstractDocumentBuilder::createSignature` exactly): - [x] Phase 4: digital signature (XmlSigner + JsonSigner, byte-for-byte PHP parity, commit `9ce6d48`, 232 tests) - [x] Phase 5: submit + state services (SubmissionsService + document-state mutations, commit `f4327a1`, 260 tests) - [x] Phase 6a: async mirror (`AsyncMyInvoisClient` + 5 async services, commit `80e7e2d` / PR #4, 284 tests) -- [ ] Phase 6b: polish + publish (CI workflow, PyPI Trusted Publishing, live sandbox verification) +- [~] Phase 6b: polish + publish — CI + packaging metadata DONE; PyPI Trusted Publishing, remaining 7 document types and live sandbox verification still TODO ## CURRENT_STATE -Phases 0-6a done and committed. **284 tests passing** (up from 260). `ruff check`, `ruff format --check`, `mypy src` all clean. Working tree clean; HEAD is `80e7e2d` on `master`. +Phases 0-6a done; Phase 6b partially done (CI + packaging). **284 tests passing** (up from 260). All four gates verified green *as of the Phase 6b commit* — `ruff check .`, `ruff format --check .`, `mypy src/myinvois`, `pytest -m "not live"`. The format gate only became green in that commit; see FORMATTING DRIFT. The full pipeline is implemented end-to-end and verified to run: build `Invoice` -> `JsonEnvelopeBuilder`/`XmlEnvelopeBuilder` -> `JsonSigner`/`XmlSigner` -> `build_submission_payload` -> `client.submissions.submit_documents`. Remaining before 1.0: -- CI workflow + PyPI release automation (see GITHUB section). +- PyPI release automation (CI itself is now in place — see GITHUB section). - Document types `02`/`03`/`04`/`11`-`14` (only `01` Invoice is modelled). - Live sandbox verification against LHDN preprod. +- Async test coverage gap: token cache-hit / proactive refresh and HTTP-status -> exception mapping are sync-side only. ## CODE_STATE - `src/myinvois/codes/__init__.py` implements curated `StrEnum(_EnumLookupMixin)` tables + `_CodeTable` loader instances; `src/myinvois/codes/_data/*.json` 8 tables = 3,637 rows. Re-extract via `uv run python scripts/extract_codes.py`. @@ -151,12 +152,20 @@ Phase 4 commit = `9ce6d48`. Phase 5 commit = `f4327a1`. Phase 6a commit = `80e7e Repo: **https://github.com/danieyal/myinvois-python** (public). Remote `origin` → `https://github.com/danieyal/myinvois-python.git`. Default branch is `master`. Topics: `myinvois`, `lhdn`, `e-invoice`, `malaysia`, `python`, `pydantic`, `ubl`, `xades`, `digital-signature`, `sdk`. -CI (TODO when added): `.github/workflows/ci.yml` running `uv run ruff check . && uv run ruff format --check . && uv run mypy src/myinvois && uv run pytest`. Release automation (TODO): GitHub Actions Trusted Publishing → PyPI on tag `v*`. +CI (DONE, Phase 6b): `.github/workflows/ci.yml`, three jobs — `lint` (ruff check + ruff format --check + mypy), `test` (pytest matrix 3.11/3.12/3.13, `-m "not live"`), `package` (uv build + `scripts/check_dist.py`, uploads the dists as an artifact). Runs on push-to-master, every PR, and `workflow_dispatch`. Workflow-level `permissions: contents: read` and `persist-credentials: false` on every checkout (least privilege; nothing in CI writes to the repo). `UV_LOCKED=1` so a stale `uv.lock` fails the build instead of silently resolving differently — **`UV_LOCKED`, not `UV_FROZEN`**: `--frozen` skips re-locking *without* checking, so it would not catch the drift. + +Release automation (TODO): GitHub Actions Trusted Publishing → PyPI on tag `v*`. Gate it on the `package` job. **Publishing under the `myinvois` name is outward-facing and irreversible per-version — confirm with the user before the first release.** Test-only signing fixtures `tests/fixtures/cert/dummy_signing_{cert,key}.pem` are intentionally force-tracked (see `tests/fixtures/cert/README.md`): the Phase 4 byte-parity tests pin against PHP-generated goldens that were signed with this exact self-signed dummy keypair. ## CHANGES - `pyproject.toml` has `[tool.uv.build-backend]` `data-includes` shipping `py.typed` + `_data/*.json` (PEP 561 marker). Codes symbols re-exported from top-level `myinvois/__init__.py`. +- `scripts/check_dist.py` (Phase 6b) verifies a built sdist+wheel: the 9 required data members are present, and no `*.pem|key|p12|pfx` / `tests/` / `fixtures/` path ships. Negative-tested (tamper a wheel → both failure classes are caught). Run after `uv build` via `uv run --no-project python scripts/check_dist.py`. + +## FORMATTING DRIFT — resolved, and why CI exists +Before Phase 6b, `ruff format --check .` **failed on 4 files** on `master`: `_async_client.py`, `services/async_document_types.py`, `services/models.py`, `tests/unit/test_async_client.py`. All four had been wrapped at ruff's default 88 columns instead of this project's configured `line-length = 100`, i.e. Phase 6a was committed without `ruff format` ever being run against the project config. The fix was pure line-rejoining (no semantic change) and is included in the Phase 6b CI commit. + +**Consequence for this file's own claims:** several CURRENT_STATE entries above asserted "`ruff check`, `ruff format --check`, `mypy src` all clean" while the format gate was in fact red. Do NOT copy a green-gates claim forward from a previous entry — re-run the four commands and report what they actually print. ## PENDING - [x] Phase 3b: UBL document models (Invoice-first) @@ -165,7 +174,7 @@ Test-only signing fixtures `tests/fixtures/cert/dummy_signing_{cert,key}.pem` ar - [x] Phase 4: digital signature - [x] Phase 5: submit + state services - [x] Phase 6a: async mirror (`AsyncMyInvoisClient` + 5 async services) -- [ ] Phase 6b: polish + publish (CI, PyPI, live sandbox verification, remaining 7 document types) +- [~] Phase 6b: polish + publish — CI + packaging metadata DONE; PyPI Trusted Publishing, remaining 7 document types and live sandbox verification still TODO ## PHASE 4 — Digital signature (TDD, in flight) @@ -595,9 +604,11 @@ for token cache-hit / proactive refresh (the `AsyncTokenManager` refresh-ahead path and its `asyncio.Lock` are untested), and no async test for HTTP-status -> exception mapping. Both are covered on the sync side only. -## PHASE 6b — Polish + publish (TODO) +## PHASE 6b — Polish + publish (partially DONE) -1. **CI** — `.github/workflows/ci.yml`: `uv run ruff check . && uv run ruff format --check . && uv run mypy src/myinvois && uv run pytest`. Matrix over Python 3.11/3.12/3.13. -2. **Release** — GitHub Actions Trusted Publishing to PyPI on tag `v*`. Confirm the wheel excludes `tests/fixtures/cert/*` (see `CERTIFY_BEFORE_PUBLIC` in the Phase 4 notes) and includes `py.typed` + `codes/_data/*.json`. -3. **Remaining document types** — `02` Credit Note, `03` Debit Note, `04` Refund Note, `11`-`14` self-billed variants. Per the Phase 3b design decision these reuse the Invoice models; self-billed swaps supplier/customer roles via a single builder function. The envelope builders already dispatch on the document tag (`ENVELOPE_DOCUMENT_TAGS`), so the work is model-side plus new golden fixtures from the PHP SDK. -4. **Live sandbox verification** — run the full build/sign/submit pipeline against `preprod-api.myinvois.hasil.gov.my` with a real LHDN cert. Everything so far is verified against PHP-SDK goldens, which is a proxy for (not proof of) LHDN validator acceptance. +1. **CI** — **DONE.** `.github/workflows/ci.yml` with `lint` / `test` (3.11-3.13 matrix) / `package` jobs. See the GITHUB section. Adding it surfaced 4 pre-existing `ruff format` failures — see the FORMATTING DRIFT section. +2. **Packaging metadata** — **DONE.** `pyproject.toml` `[project.urls]` had shipped placeholder `https://github.com/your-org/myinvois` URLs into the wheel METADATA; corrected to `danieyal/myinvois-python`. `Development Status` classifier bumped `3 - Alpha` -> `4 - Beta` to match the README. +3. **`CERTIFY_BEFORE_PUBLIC` — RESOLVED.** Verified empirically: neither the wheel nor the sdist ships `tests/` or `tests/fixtures/cert/*`. The wheel carries only `myinvois/**` + `dist-info`; the sdist carries `PKG-INFO`, `README.md`, `pyproject.toml`, `src/`. `scripts/check_dist.py` now enforces this in CI, so the Phase 4 worry is closed rather than merely observed. +4. **Release** — TODO. GitHub Actions Trusted Publishing to PyPI on tag `v*`, gated on the `package` job. **Outward-facing and irreversible per-version: confirm with the user before the first publish.** +5. **Remaining document types** — TODO. `02` Credit Note, `03` Debit Note, `04` Refund Note, `11`-`14` self-billed variants. Per the Phase 3b design decision these reuse the Invoice models; self-billed swaps supplier/customer roles via a single builder function. The envelope builders already dispatch on the document tag (`ENVELOPE_DOCUMENT_TAGS`), so the work is model-side plus new golden fixtures from the PHP SDK. +6. **Live sandbox verification** — TODO. Run the full build/sign/submit pipeline against `preprod-api.myinvois.hasil.gov.my` with a real LHDN cert. Everything so far is verified against PHP-SDK goldens, which is a proxy for (not proof of) LHDN validator acceptance. diff --git a/pyproject.toml b/pyproject.toml index 45939aa..b48203b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ license = "MIT" requires-python = ">=3.11" keywords = ["myinvois", "lhdn", "e-invoice", "malaysia", "ubl", "invoice"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", @@ -37,10 +37,10 @@ test = [ ] [project.urls] -Homepage = "https://github.com/your-org/myinvois" -Documentation = "https://github.com/your-org/myinvois#readme" -Repository = "https://github.com/your-org/myinvois" -Issues = "https://github.com/your-org/myinvois/issues" +Homepage = "https://github.com/danieyal/myinvois-python" +Documentation = "https://github.com/danieyal/myinvois-python#readme" +Repository = "https://github.com/danieyal/myinvois-python" +Issues = "https://github.com/danieyal/myinvois-python/issues" [build-system] requires = ["uv_build>=0.11.28,<0.12.0"] diff --git a/scripts/check_dist.py b/scripts/check_dist.py new file mode 100644 index 0000000..7d26205 --- /dev/null +++ b/scripts/check_dist.py @@ -0,0 +1,138 @@ +"""Verify the built sdist and wheel before they are published. + +Run from the repo root after `uv build`: + + uv run --no-project python scripts/check_dist.py + +Two classes of mistake are checked, both of which are unrecoverable once a +release is on PyPI (a version number can never be reused): + +1. **Missing runtime data.** `py.typed` and `codes/_data/*.json` are not + importable Python modules, so they only ship because of the explicit + `[tool.uv.build-backend] data-includes` entry. If that entry regresses, the + package still builds and imports, but `myinvois.codes` raises at runtime on + a wheel install and type-checkers silently ignore the package. + +2. **Leaked signing material.** `tests/fixtures/cert/` holds a dummy private + key and certificate that are deliberately force-tracked in git so the + byte-parity tests can pin against fixtures signed with them. They are + harmless (self-signed, test-only) but shipping a file named + `*_key.pem` inside a published distribution is the kind of thing that + triggers secret scanners and erodes trust, so it is a hard failure here. + +Exits non-zero with a description of every problem found. +""" + +from __future__ import annotations + +import re +import sys +import tarfile +import zipfile +from pathlib import Path + +DIST = Path(__file__).resolve().parents[1] / "dist" + +# Non-Python files that must ship, as paths relative to the package root. +REQUIRED_DATA_MEMBERS = ( + "myinvois/py.typed", + "myinvois/codes/_data/classification.json", + "myinvois/codes/_data/countries.json", + "myinvois/codes/_data/currencies.json", + "myinvois/codes/_data/msic.json", + "myinvois/codes/_data/payment_means.json", + "myinvois/codes/_data/states.json", + "myinvois/codes/_data/taxes.json", + "myinvois/codes/_data/units.json", +) + +# The same files live at different prefixes in each distribution: the wheel is +# already package-rooted, while the sdist keeps the repo's `src/` layout. Both +# are checked -- `pip install --no-binary` builds from the sdist, so a +# `data-includes` regression there breaks installs just as badly, and would go +# unnoticed if only the wheel were validated. +REQUIRED_MEMBER_PREFIXES = {"wheel": "", "sdist": "src/"} + +# Anything matching these must NOT appear in either distribution. Keyed on the +# path so a source module such as `ubl/signing/_cert.py` (legitimate) is not +# confused with an actual PEM payload. +FORBIDDEN_PATTERNS = ( + re.compile(r"\.(pem|key|p12|pfx)$", re.IGNORECASE), + re.compile(r"(^|/)tests?/", re.IGNORECASE), + re.compile(r"(^|/)fixtures?/", re.IGNORECASE), +) + + +def _wheel_and_sdist() -> tuple[Path, Path]: + wheels = sorted(DIST.glob("*.whl")) + sdists = sorted(DIST.glob("*.tar.gz")) + if len(wheels) != 1 or len(sdists) != 1: + sys.exit( + f"expected exactly one wheel and one sdist in {DIST}, " + f"found {len(wheels)} wheel(s) and {len(sdists)} sdist(s). " + "Remove stale builds and re-run `uv build`." + ) + return wheels[0], sdists[0] + + +def _wheel_members(wheel: Path) -> list[str]: + with zipfile.ZipFile(wheel) as zf: + return zf.namelist() + + +def _sdist_members(sdist: Path) -> list[str]: + with tarfile.open(sdist) as tf: + # Strip the leading `myinvois-/` directory so the paths line + # up with how they are written in the repo. + return [name.partition("/")[2] for name in tf.getnames()] + + +def main() -> int: + if not DIST.is_dir(): + sys.exit(f"{DIST} does not exist -- run `uv build` first.") + + wheel, sdist = _wheel_and_sdist() + wheel_members = _wheel_members(wheel) + sdist_members = _sdist_members(sdist) + + problems: list[str] = [] + + for kind, label, members in ( + ("wheel", wheel.name, wheel_members), + ("sdist", sdist.name, sdist_members), + ): + prefix = REQUIRED_MEMBER_PREFIXES[kind] + present = set(members) + for member in REQUIRED_DATA_MEMBERS: + if f"{prefix}{member}" not in present: + problems.append( + f"{label}: missing required member {prefix + member!r} -- check the " + "`[tool.uv.build-backend] data-includes` entry in pyproject.toml" + ) + + for label, members in ((wheel.name, wheel_members), (sdist.name, sdist_members)): + for member in members: + if not member or member.endswith("/"): + continue + for pattern in FORBIDDEN_PATTERNS: + if pattern.search(member): + problems.append( + f"{label}: must not ship {member!r} (matched {pattern.pattern})" + ) + break + + if problems: + print(f"Distribution check FAILED ({len(problems)} problem(s)):", file=sys.stderr) + for problem in problems: + print(f" - {problem}", file=sys.stderr) + return 1 + + scanned = len(wheel_members) + len(sdist_members) + print(f"Distribution check passed: {wheel.name}, {sdist.name}") + print(f" {len(REQUIRED_DATA_MEMBERS)} required members present in wheel and sdist") + print(f" no test fixtures or key material in {scanned} entries") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/myinvois/_async_client.py b/src/myinvois/_async_client.py index f2bfaaf..e1d54d4 100644 --- a/src/myinvois/_async_client.py +++ b/src/myinvois/_async_client.py @@ -58,9 +58,7 @@ def __init__( http_client: httpx.AsyncClient | None = None, ) -> None: self._owns_client = http_client is None - self._http = ( - http_client if http_client is not None else httpx.AsyncClient(timeout=30.0) - ) + self._http = http_client if http_client is not None else httpx.AsyncClient(timeout=30.0) self._environment = environment self._base_api_url = base_api_url(environment) self._base_portal_url = base_portal_url(environment) diff --git a/src/myinvois/services/async_document_types.py b/src/myinvois/services/async_document_types.py index 0e4414a..ade3119 100644 --- a/src/myinvois/services/async_document_types.py +++ b/src/myinvois/services/async_document_types.py @@ -38,9 +38,7 @@ async def get(self, id_: int | str) -> DocumentType: raise TypeError(f"Expected dict from API, got {type(raw).__name__}") return DocumentType.model_validate(raw) - async def get_version( - self, id_: int | str, version_id: int | str - ) -> DocumentTypeVersion: + async def get_version(self, id_: int | str, version_id: int | str) -> DocumentTypeVersion: raw = await self._client.request("GET", f"{self.BASE_PATH}/{id_}/versions/{version_id}") if not isinstance(raw, dict): raise TypeError(f"Expected dict from API, got {type(raw).__name__}") diff --git a/src/myinvois/services/models.py b/src/myinvois/services/models.py index 4552656..c99a65d 100644 --- a/src/myinvois/services/models.py +++ b/src/myinvois/services/models.py @@ -212,9 +212,7 @@ class GetSubmissionResponse(_Base): @model_validator(mode="after") def _require_uid_or_error(self) -> GetSubmissionResponse: if self.submission_uid is None and self.error is None: - raise ValueError( - "GetSubmissionResponse requires either submissionUid or error" - ) + raise ValueError("GetSubmissionResponse requires either submissionUid or error") return self diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index 1da5621..7085eaf 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -80,9 +80,7 @@ async def test_async_login_acquires_token(client: AsyncMyInvoisClient) -> None: assert client.access_token == "test-token" -async def test_async_login_with_on_behalf_of( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_login_with_on_behalf_of(client: AsyncMyInvoisClient, respx_mock: Any) -> None: captured: dict[str, Any] = {} def _capture(request: httpx.Request) -> httpx.Response: @@ -109,9 +107,7 @@ def _capture(request: httpx.Request) -> httpx.Response: } -async def test_async_submit_documents( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_submit_documents(client: AsyncMyInvoisClient, respx_mock: Any) -> None: captured: dict[str, Any] = {} def _capture(request: httpx.Request) -> httpx.Response: @@ -138,9 +134,7 @@ async def test_async_submit_documents_empty_raises(client: AsyncMyInvoisClient) await client.submissions.submit_documents([]) -async def test_async_get_submission( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_get_submission(client: AsyncMyInvoisClient, respx_mock: Any) -> None: body = { "submissionUid": "HJSD135P2S7D8IU", "documentCount": 1, @@ -215,12 +209,8 @@ async def test_async_get_submission_rejects_empty() -> None: # ===== documents (cancel/reject) ===== -async def test_async_cancel_document( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: - respx_mock.put( - f"{_API}/api/v1.0/documents/state/UUID-1/state" - ).mock( +async def test_async_cancel_document(client: AsyncMyInvoisClient, respx_mock: Any) -> None: + respx_mock.put(f"{_API}/api/v1.0/documents/state/UUID-1/state").mock( return_value=httpx.Response(200, json={"uuid": "UUID-1", "status": "Cancelled"}) ) resp = await client.documents.cancel_document("UUID-1", reason="wrong amount") @@ -229,12 +219,8 @@ async def test_async_cancel_document( assert resp.status == "Cancelled" -async def test_async_reject_document( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: - respx_mock.put( - f"{_API}/api/v1.0/documents/state/UUID-2/state" - ).mock( +async def test_async_reject_document(client: AsyncMyInvoisClient, respx_mock: Any) -> None: + respx_mock.put(f"{_API}/api/v1.0/documents/state/UUID-2/state").mock( return_value=httpx.Response( 200, json={"uuid": "UUID-2", "status": "Requested for Rejection"} ) @@ -269,9 +255,7 @@ def _cap(request: httpx.Request) -> httpx.Response: # ===== document types ===== -async def test_async_document_types_list( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_document_types_list(client: AsyncMyInvoisClient, respx_mock: Any) -> None: respx_mock.get(f"{_API}/api/v1.0/documenttypes").mock( return_value=httpx.Response( 200, @@ -290,9 +274,7 @@ async def test_async_document_types_list( assert types.result[0].name == "Invoice" -async def test_async_document_types_get( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_document_types_get(client: AsyncMyInvoisClient, respx_mock: Any) -> None: respx_mock.get(f"{_API}/api/v1.0/documenttypes/1").mock( return_value=httpx.Response( 200, json={"id": 1, "name": "Invoice", "description": "Invoice"} @@ -305,9 +287,7 @@ async def test_async_document_types_get( # ===== notifications ===== -async def test_async_get_notifications( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_get_notifications(client: AsyncMyInvoisClient, respx_mock: Any) -> None: respx_mock.get(f"{_API}/api/v1.0/notifications/taxpayer").mock( return_value=httpx.Response( 200, @@ -325,9 +305,7 @@ async def test_async_get_notifications( # ===== taxpayer ===== -async def test_async_validate_tin_valid( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_validate_tin_valid(client: AsyncMyInvoisClient, respx_mock: Any) -> None: respx_mock.get(url__regex=r".*/taxpayer/validate/.*").mock( return_value=httpx.Response(200, json={}) ) @@ -337,9 +315,7 @@ async def test_async_validate_tin_valid( assert ok is True -async def test_async_search_tin( - client: AsyncMyInvoisClient, respx_mock: Any -) -> None: +async def test_async_search_tin(client: AsyncMyInvoisClient, respx_mock: Any) -> None: respx_mock.get(url__regex=r".*/taxpayer/search/tin.*").mock( return_value=httpx.Response(200, json={"result": [{"tin": "C123"}]}) )