anafpy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. anafpy-0.1.0/.gitattributes +2 -0
  2. anafpy-0.1.0/.github/workflows/ci.yml +33 -0
  3. anafpy-0.1.0/.github/workflows/release.yml +67 -0
  4. anafpy-0.1.0/.gitignore +42 -0
  5. anafpy-0.1.0/.pre-commit-config.yaml +13 -0
  6. anafpy-0.1.0/.python-version +1 -0
  7. anafpy-0.1.0/CLAUDE.md +390 -0
  8. anafpy-0.1.0/DESIGN.md +513 -0
  9. anafpy-0.1.0/INSTALL.md +297 -0
  10. anafpy-0.1.0/LICENSE +173 -0
  11. anafpy-0.1.0/NOTICE +17 -0
  12. anafpy-0.1.0/PKG-INFO +417 -0
  13. anafpy-0.1.0/README.md +384 -0
  14. anafpy-0.1.0/docs/anaf-reference/README.md +25 -0
  15. anafpy-0.1.0/docs/anaf-reference/_sources/Oauth_procedura_inregistrare_aplicatii_portal_ANAF.pdf +0 -0
  16. anafpy-0.1.0/docs/anaf-reference/_sources/README.md +83 -0
  17. anafpy-0.1.0/docs/anaf-reference/_sources/eTransport-validation_v.2.0.2_12082024.sch +588 -0
  18. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/descarcare.html +54 -0
  19. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/listamesaje.html +522 -0
  20. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/staremesaj.html +54 -0
  21. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/upload.html +570 -0
  22. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/validare.html +226 -0
  23. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/validaresemnatura.html +137 -0
  24. anafpy-0.1.0/docs/anaf-reference/_sources/efactura-swagger/xmltopdf.html +356 -0
  25. anafpy-0.1.0/docs/anaf-reference/_sources/efactura_prezentare_api.pdf +0 -0
  26. anafpy-0.1.0/docs/anaf-reference/_sources/etransport-swagger/info_transportatori.html +223 -0
  27. anafpy-0.1.0/docs/anaf-reference/_sources/etransport-swagger/lista.html +341 -0
  28. anafpy-0.1.0/docs/anaf-reference/_sources/etransport-swagger/stare.html +232 -0
  29. anafpy-0.1.0/docs/anaf-reference/_sources/etransport-swagger/upload_param.html +439 -0
  30. anafpy-0.1.0/docs/anaf-reference/_sources/etransport_29072024.pdf +0 -0
  31. anafpy-0.1.0/docs/anaf-reference/_sources/limiteApeluriAPI.txt +25 -0
  32. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/docV1.txt +63 -0
  33. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/doc_WS_Async_V8.txt +210 -0
  34. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/doc_WS_Bilant_V1.txt +196 -0
  35. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/doc_WS_V9.txt +156 -0
  36. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/documentatie_SWRARG_v2.txt +83 -0
  37. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/index_cult_v2.html +124 -0
  38. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/servicii_asincron.html +46 -0
  39. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/servicii_sincron.html +157 -0
  40. anafpy-0.1.0/docs/anaf-reference/_sources/servicii-web/servicii_web.html +316 -0
  41. anafpy-0.1.0/docs/anaf-reference/efactura/api.md +336 -0
  42. anafpy-0.1.0/docs/anaf-reference/etransport/api.md +223 -0
  43. anafpy-0.1.0/docs/anaf-reference/oauth/authentication.md +257 -0
  44. anafpy-0.1.0/docs/anaf-reference/public/api.md +224 -0
  45. anafpy-0.1.0/pyproject.toml +113 -0
  46. anafpy-0.1.0/schemas/README.md +23 -0
  47. anafpy-0.1.0/schemas/etransport/schema_ETR_v2_20230126.xsd +945 -0
  48. anafpy-0.1.0/schemas/ubl-2.1/common/CCTS_CCT_SchemaModule-2.1.xsd +731 -0
  49. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-CommonAggregateComponents-2.1.xsd +39799 -0
  50. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-CommonBasicComponents-2.1.xsd +5389 -0
  51. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-CommonExtensionComponents-2.1.xsd +223 -0
  52. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-CommonSignatureComponents-2.1.xsd +101 -0
  53. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-CoreComponentParameters-2.1.xsd +63 -0
  54. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-ExtensionContentDataType-2.1.xsd +89 -0
  55. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-QualifiedDataTypes-2.1.xsd +69 -0
  56. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-SignatureAggregateComponents-2.1.xsd +138 -0
  57. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-SignatureBasicComponents-2.1.xsd +78 -0
  58. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-UnqualifiedDataTypes-2.1.xsd +553 -0
  59. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-XAdESv132-2.1.xsd +476 -0
  60. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-XAdESv141-2.1.xsd +25 -0
  61. anafpy-0.1.0/schemas/ubl-2.1/common/UBL-xmldsig-core-schema-2.1.xsd +330 -0
  62. anafpy-0.1.0/schemas/ubl-2.1/maindoc/UBL-CreditNote-2.1.xsd +951 -0
  63. anafpy-0.1.0/schemas/ubl-2.1/maindoc/UBL-Invoice-2.1.xsd +1002 -0
  64. anafpy-0.1.0/scripts/generate_etransport.py +203 -0
  65. anafpy-0.1.0/scripts/generate_ubl.py +103 -0
  66. anafpy-0.1.0/skills/etransport-declare/SKILL.md +275 -0
  67. anafpy-0.1.0/src/anafpy/__init__.py +25 -0
  68. anafpy-0.1.0/src/anafpy/_transport/__init__.py +7 -0
  69. anafpy-0.1.0/src/anafpy/_transport/base.py +145 -0
  70. anafpy-0.1.0/src/anafpy/auth/__init__.py +25 -0
  71. anafpy-0.1.0/src/anafpy/auth/callback.py +215 -0
  72. anafpy-0.1.0/src/anafpy/auth/models.py +94 -0
  73. anafpy-0.1.0/src/anafpy/auth/oauth.py +119 -0
  74. anafpy-0.1.0/src/anafpy/auth/provider.py +132 -0
  75. anafpy-0.1.0/src/anafpy/auth/store.py +230 -0
  76. anafpy-0.1.0/src/anafpy/cli/__init__.py +1 -0
  77. anafpy-0.1.0/src/anafpy/cli/main.py +299 -0
  78. anafpy-0.1.0/src/anafpy/efactura/__init__.py +48 -0
  79. anafpy-0.1.0/src/anafpy/efactura/client.py +509 -0
  80. anafpy-0.1.0/src/anafpy/efactura/models.py +425 -0
  81. anafpy-0.1.0/src/anafpy/efactura/ubl/__init__.py +1 -0
  82. anafpy-0.1.0/src/anafpy/efactura/ubl/common/__init__.py +5852 -0
  83. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ccts_cct_schema_module_2_1.py +1127 -0
  84. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_common_aggregate_components_2_1.py +55291 -0
  85. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_common_basic_components_2_1.py +12592 -0
  86. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_common_extension_components_2_1.py +297 -0
  87. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_common_signature_components_2_1.py +60 -0
  88. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_extension_content_data_type_2_1.py +30 -0
  89. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_signature_aggregate_components_2_1.py +86 -0
  90. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_signature_basic_components_2_1.py +24 -0
  91. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_unqualified_data_types_2_1.py +577 -0
  92. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_xad_esv132_2_1.py +1613 -0
  93. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_xad_esv141_2_1.py +58 -0
  94. anafpy-0.1.0/src/anafpy/efactura/ubl/common/ubl_xmldsig_core_schema_2_1.py +881 -0
  95. anafpy-0.1.0/src/anafpy/efactura/ubl/maindoc/__init__.py +15 -0
  96. anafpy-0.1.0/src/anafpy/efactura/ubl/maindoc/ubl_credit_note_2_1.py +1151 -0
  97. anafpy-0.1.0/src/anafpy/efactura/ubl/maindoc/ubl_invoice_2_1.py +1229 -0
  98. anafpy-0.1.0/src/anafpy/etransport/__init__.py +61 -0
  99. anafpy-0.1.0/src/anafpy/etransport/client.py +376 -0
  100. anafpy-0.1.0/src/anafpy/etransport/models.py +1048 -0
  101. anafpy-0.1.0/src/anafpy/etransport/schema/__init__.py +49 -0
  102. anafpy-0.1.0/src/anafpy/etransport/schema/schema_etr_v2_20230126.py +1143 -0
  103. anafpy-0.1.0/src/anafpy/exceptions.py +67 -0
  104. anafpy-0.1.0/src/anafpy/mcp/__init__.py +13 -0
  105. anafpy-0.1.0/src/anafpy/mcp/__main__.py +8 -0
  106. anafpy-0.1.0/src/anafpy/mcp/config.py +140 -0
  107. anafpy-0.1.0/src/anafpy/mcp/context.py +144 -0
  108. anafpy-0.1.0/src/anafpy/mcp/documents.py +68 -0
  109. anafpy-0.1.0/src/anafpy/mcp/models.py +83 -0
  110. anafpy-0.1.0/src/anafpy/mcp/nomenclatures.py +119 -0
  111. anafpy-0.1.0/src/anafpy/mcp/server/__init__.py +39 -0
  112. anafpy-0.1.0/src/anafpy/mcp/server/_shared.py +15 -0
  113. anafpy-0.1.0/src/anafpy/mcp/server/app.py +107 -0
  114. anafpy-0.1.0/src/anafpy/mcp/server/efactura.py +221 -0
  115. anafpy-0.1.0/src/anafpy/mcp/server/etransport.py +375 -0
  116. anafpy-0.1.0/src/anafpy/mcp/server/prompts.py +54 -0
  117. anafpy-0.1.0/src/anafpy/mcp/server/public.py +91 -0
  118. anafpy-0.1.0/src/anafpy/mcp/server/resources.py +45 -0
  119. anafpy-0.1.0/src/anafpy/mcp/skills.py +71 -0
  120. anafpy-0.1.0/src/anafpy/mcp/tokens.py +124 -0
  121. anafpy-0.1.0/src/anafpy/mcp/unitcodes.py +127 -0
  122. anafpy-0.1.0/src/anafpy/public/__init__.py +57 -0
  123. anafpy-0.1.0/src/anafpy/public/client.py +445 -0
  124. anafpy-0.1.0/src/anafpy/public/models.py +368 -0
  125. anafpy-0.1.0/src/anafpy/py.typed +0 -0
  126. anafpy-0.1.0/tests/_wire.py +287 -0
  127. anafpy-0.1.0/tests/conftest.py +67 -0
  128. anafpy-0.1.0/tests/test_auth.py +521 -0
  129. anafpy-0.1.0/tests/test_cli.py +167 -0
  130. anafpy-0.1.0/tests/test_efactura_client.py +690 -0
  131. anafpy-0.1.0/tests/test_efactura_read.py +70 -0
  132. anafpy-0.1.0/tests/test_efactura_roundtrip_live.py +328 -0
  133. anafpy-0.1.0/tests/test_etransport_client.py +682 -0
  134. anafpy-0.1.0/tests/test_etransport_read.py +293 -0
  135. anafpy-0.1.0/tests/test_etransport_roundtrip_live.py +178 -0
  136. anafpy-0.1.0/tests/test_etransport_schema.py +135 -0
  137. anafpy-0.1.0/tests/test_mcp_server.py +942 -0
  138. anafpy-0.1.0/tests/test_mcp_tokens.py +83 -0
  139. anafpy-0.1.0/tests/test_oauth_live.py +107 -0
  140. anafpy-0.1.0/tests/test_public_client.py +564 -0
  141. anafpy-0.1.0/tests/test_public_live.py +69 -0
  142. anafpy-0.1.0/tests/test_ubl_roundtrip.py +63 -0
  143. anafpy-0.1.0/tests/test_version.py +15 -0
  144. anafpy-0.1.0/uv.lock +1654 -0
@@ -0,0 +1,2 @@
1
+ # Raw ANAF originals under _sources/ are preserved verbatim - no EOL normalization.
2
+ docs/anaf-reference/_sources/** -text
@@ -0,0 +1,33 @@
1
+ # Credential-free gates on every push/PR: the respx-mocked test suite, ruff
2
+ # lint + format, and mypy --strict, across the supported Python versions.
3
+ # The live-marked tests stay opt-in (ANAFPY_LIVE=1) and never run here.
4
+ name: CI
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v6
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ enable-cache: true
24
+ - name: Sync dependencies
25
+ run: uv sync --all-extras
26
+ - name: Tests
27
+ run: uv run pytest -q
28
+ - name: Ruff lint
29
+ run: uv run ruff check .
30
+ - name: Ruff format
31
+ run: uv run ruff format --check .
32
+ - name: Mypy (strict)
33
+ run: uv run mypy
@@ -0,0 +1,67 @@
1
+ # Publish to PyPI on a version tag (v*) via trusted publishing (OIDC) — no
2
+ # API token is stored in the repo. PyPI side: the project's trusted publisher
3
+ # must name this repo, this filename (release.yml), and the `pypi` environment.
4
+ # The full CI gates re-run here so a tag can never ship an artifact that a
5
+ # broken branch produced.
6
+ name: Release
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ jobs:
13
+ gates:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.12", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v6
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ enable-cache: true
25
+ - name: Sync dependencies
26
+ run: uv sync --all-extras
27
+ - name: Tests
28
+ run: uv run pytest -q
29
+ - name: Ruff lint
30
+ run: uv run ruff check . && uv run ruff format --check .
31
+ - name: Mypy (strict)
32
+ run: uv run mypy
33
+
34
+ build:
35
+ needs: gates
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: astral-sh/setup-uv@v6
40
+ - name: Tag matches the package version
41
+ run: |
42
+ version="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
43
+ if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
44
+ echo "pyproject version ${version} does not match tag ${GITHUB_REF_NAME}" >&2
45
+ exit 1
46
+ fi
47
+ - name: Build sdist + wheel
48
+ run: uv build
49
+ - uses: actions/upload-artifact@v4
50
+ with:
51
+ name: dist
52
+ path: dist/
53
+
54
+ publish:
55
+ needs: build
56
+ runs-on: ubuntu-latest
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/p/anafpy
60
+ permissions:
61
+ id-token: write # OIDC for PyPI trusted publishing
62
+ steps:
63
+ - uses: actions/download-artifact@v4
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,42 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ *.so
9
+
10
+ # Environments / tooling
11
+ .venv/
12
+ venv/
13
+ .uv/
14
+ uv.lock.bak
15
+
16
+ # Caches
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ .pytest_cache/
20
+ .coverage
21
+ htmlcov/
22
+
23
+ # OS / editor
24
+ .DS_Store
25
+ .idea/
26
+ .vscode/
27
+ .claude/
28
+
29
+ # Local secrets / tokens (never commit ANAF credentials or token stores)
30
+ *.token.json
31
+ .anafpy/
32
+ secrets.toml
33
+ .env
34
+
35
+ # TLS certs / keys (mkcert localhost certs for `anafpy auth login --tls-cert/--tls-key`)
36
+ *.pem
37
+ *.key
38
+ *.crt
39
+ *.cert
40
+ *.p12
41
+ *.pfx
42
+ rootCA*.pem
@@ -0,0 +1,13 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.5.7
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: https://github.com/pre-commit/mirrors-mypy
9
+ rev: v1.11.1
10
+ hooks:
11
+ - id: mypy
12
+ additional_dependencies: [pydantic, httpx, tenacity]
13
+ exclude: '(^|/)(ubl|schema)/'
@@ -0,0 +1 @@
1
+ 3.13
anafpy-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,390 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for working in this repository. See [DESIGN.md](DESIGN.md) for the full
4
+ design rationale and [docs/anaf-reference/](docs/anaf-reference/) for a compiled local
5
+ reference of ANAF's APIs.
6
+
7
+ ## What this is
8
+
9
+ `anafpy` — typed Python clients for Romania's **ANAF** tax-authority web services,
10
+ **e-Factura** (electronic invoicing), **e-Transport** (goods transport), and the
11
+ **public no-auth services** (`anafpy.public`: registry lookups + financial
12
+ statements). It is a
13
+ **thin, stateless transport client, not invoicing software**. **e-Factura outbound is
14
+ XML pass-through** (no invoice composition): callers bring complete invoice XML their
15
+ own system produced, and anafpy validates, files, tracks, and downloads; received UBL
16
+ is wrapped in a friendly **flat read view** (`FlatInvoice`, UBL→flat only) backing
17
+ the e-Factura inbox. **e-Transport is fully
18
+ translated** (decided 2026-07-03): there is usually no upstream software producing
19
+ declaration XML and ANAF's XSD is small and fully enumerated, so the e-Transport flat
20
+ models are **bidirectional** — the same models author a filing
21
+ (`build_etransport`/`render_etransport`, `upload_document`) and view a parsed one
22
+ (`read_flat_transport`), covering all four operations (declaration/correction,
23
+ deletion, confirmation, vehicle change). Phase 1 is the typed async clients; phase 2 is
24
+ the **MCP server** (`anafpy.mcp`, extra `anafpy[mcp]`) exposing the operations as
25
+ Claude Cowork skills. The client methods map 1:1 onto MCP tools — discrete operations,
26
+ serializable typed inputs/outputs, good docstrings. Distribution is **free and
27
+ as-is**: the library is for anyone to use; the MCP server is **best-effort**, and
28
+ configuring it — including provisioning the OAuth application on ANAF's portal —
29
+ is the user's responsibility (DESIGN.md §11).
30
+
31
+ Python **3.12+** (`requires-python`; the repo `.python-version` dev pin stays 3.13).
32
+ Built on **httpx** and **Pydantic v2**.
33
+
34
+ ## Commands
35
+
36
+ ```bash
37
+ uv sync --all-extras # set up env with all dev dependency groups
38
+ uv run pytest -q # tests (respx-mocked, credential-free)
39
+ uv run pytest tests/test_auth.py # one file
40
+ uv run ruff check . && uv run ruff format --check .
41
+ uv run mypy # strict
42
+ ANAFPY_LIVE=1 uv run pytest -m live # opt-in live smoke: public services + authenticated TEST (needs .env + auth login)
43
+ ```
44
+
45
+ Run the MCP server (host-side, where the `anafpy auth login` token store lives):
46
+
47
+ ```bash
48
+ ANAFPY_CLIENT_ID=... ANAFPY_CLIENT_SECRET=... ANAFPY_CIF=... \
49
+ uv run python -m anafpy.mcp # stdio; or the `anafpy-mcp` console script
50
+ ```
51
+
52
+ Config is env-only — `anafpy.mcp.config.ServerConfig` is a `pydantic-settings`
53
+ `BaseSettings` (use `ServerConfig.from_env()` for a friendly `AnafConfigError`):
54
+ `ANAFPY_CLIENT_ID`,
55
+ `ANAFPY_CLIENT_SECRET` (optional — without them the server still starts and serves
56
+ the public `anaf_*` lookups; the authenticated tools raise a how-to-enable
57
+ `AnafConfigError`), `ANAFPY_TOKEN_STORE` (default `~/.anafpy/tokens.json`),
58
+ `ANAFPY_TOKEN_STORE_BACKEND` (`keyring`/`file`, default `keyring` — tokens live in
59
+ the OS credential store via `KeyringTokenStore` (`keyring` is a core dependency);
60
+ `file` is the opt-out for Docker/headless hosts without a credential store; the
61
+ CLI honours the same variable and `--store-backend`),
62
+ `ANAFPY_ENV` (`test`/`prod`, default `prod`), `ANAFPY_CIF` (default fiscal code), `ANAFPY_DOCS_DIR`
63
+ (reference resources, defaults to the repo `docs/anaf-reference/`),
64
+ `ANAFPY_SKILLS_DIR` (workflow skills re-served as MCP prompts, defaults to the repo
65
+ `skills/`).
66
+
67
+ Codegen (only when re-vendoring XSDs — see below):
68
+
69
+ ```bash
70
+ uv run python scripts/generate_ubl.py
71
+ uv run python scripts/generate_etransport.py
72
+ ```
73
+
74
+ All three gates (pytest / ruff / mypy --strict) are currently green and must stay green.
75
+
76
+ ## Layout
77
+
78
+ ```
79
+ src/anafpy/
80
+ exceptions.py # AnafError hierarchy (see "Error model")
81
+ _transport/base.py # Environment, Service, service_base_url + shared error raising
82
+ auth/ # OAuth2 layer: models, store, oauth, provider, callback
83
+ cli/main.py # `anafpy auth login|status|logout`
84
+ efactura/
85
+ ubl/ # GENERATED UBL 2.1 models (xsdata-pydantic) — do not hand-edit
86
+ client.py # EFacturaClient (async)
87
+ models.py # value types (UploadResult, MessageStatus, ...) + FlatInvoice read view + UBL→flat reader
88
+ __init__.py # re-exports Invoice, CreditNote from ubl.maindoc
89
+ etransport/
90
+ schema/ # GENERATED e-Transport XSD models — do not hand-edit
91
+ client.py # ETransportClient (async) — incl. upload_document (flat -> XML -> upload)
92
+ models.py # value types + BIDIRECTIONAL flat models (4 ops) + read/build/render
93
+ public/
94
+ client.py # PublicClient (async, no auth) — webservicesp.anaf.ro lookups
95
+ # + the stateless e-Factura document services (validare/transformare)
96
+ models.py # lookup value types (TaxpayerRecord, RegistryLookup[...], ...)
97
+ # + TransformStandard, RemoteValidationResult
98
+ mcp/ # MCP server (extra: anafpy[mcp]) — phase 2
99
+ config.py # ServerConfig.from_env (creds, store path, env, default CIF)
100
+ context.py # AppContext: TokenProvider + lazy clients + token ledger; auth_status
101
+ models.py # UBL XML pass-through inputs + prepared-submission gate
102
+ documents.py # resolve XML input -> bytes; parse bytes -> client flat models
103
+ nomenclatures.py # e-Transport code lists (from the XSD enums) for the model
104
+ skills.py # skills/*/SKILL.md loader (frontmatter + body) for MCP prompts
105
+ tokens.py # HMAC confirmation tokens for two-step gated mutations
106
+ server/ # FastMCP server package: app.py (`create_server`, `main`,
107
+ # auth_status + instructions), tool modules efactura.py /
108
+ # etransport.py / public.py, resources.py (ANAF reference),
109
+ # prompts.py (skills), _shared.py (tool annotations)
110
+ __main__.py # `python -m anafpy.mcp` (stdio)
111
+ skills/ # workflow skills, served by the MCP server as same-name
112
+ # prompts (etransport-declare: source data -> FlatTransport
113
+ # -> prepare -> approval -> submit -> status)
114
+ schemas/ # vendored XSDs (git-tracked, NOT shipped in the wheel)
115
+ scripts/ # codegen scripts
116
+ docs/anaf-reference/ # compiled ANAF API reference (oauth/efactura/etransport/public)
117
+ tests/ # respx-mocked unit tests (+ opt-in live: test_public_live.py, test_oauth_live.py read-only; test_{efactura,etransport}_roundtrip_live.py file to TEST)
118
+ ```
119
+
120
+ ## Architecture & conventions
121
+
122
+ - **Both OAuth services share one host** `api.anaf.ro`, differing only by path prefix
123
+ (`FCTEL/rest` vs `ETRANSPORT/ws/v1`) and `test`/`prod` segment. All of that lives in
124
+ [_transport/base.py](src/anafpy/_transport/base.py); clients take an `environment`.
125
+ - **`PublicClient` is the odd one out**: the unauthenticated registries/bilanț live on
126
+ `PUBLIC_HOST` (`webservicesp.anaf.ro`) — no `TokenProvider`, no `environment`
127
+ (production only). Unlike the OAuth clients' no-auto-backoff stance, it **paces its
128
+ own requests** (`min_request_interval`, default 1 req/s) because ANAF states that
129
+ limit as a usage *rule*, not via 429s. Registry membership is read from the
130
+ `registered` booleans, never from presence in `found` (RegAgric/RegCult return
131
+ unknown CUIs in `found` with empty fields). The e-Factura register's HTTP 404 with a
132
+ `found`/`notFound` body is a business "not found" (returned), not raised.
133
+ `PublicClient` also carries the **stateless e-Factura document services**
134
+ `validate_invoice` (`validare`) and `render_invoice_pdf` (`transformare`) — they
135
+ live on the same host, need no auth, and exist only under the `prod` segment
136
+ (moved from `EFacturaClient` 2026-07-04 so validation works with no OAuth
137
+ credentials configured); only the MF signature check (`validate_signature`, on
138
+ `api.anaf.ro`) stays on `EFacturaClient`.
139
+ - **Auth is a separate layer.** Clients receive a `TokenProvider` and drive httpx via
140
+ the `AnafAuth` (`httpx.Auth`) class, which handles transparent token refresh. The
141
+ qualified-certificate step happens only in the interactive `anafpy auth login` browser
142
+ flow; code-exchange and refresh are headless. Don't add cert/mTLS handling to clients.
143
+ ANAF's portal only registers `https://` callback URLs (an `http://` one 400s —
144
+ verified 2026-07-02); the login captures the code via `--paste` (no listener, the
145
+ baseline), a TLS listener (`--tls-cert/--tls-key`), or plain HTTP behind an external
146
+ TLS terminator — the listener binds *before* the browser opens (a fast redirect must
147
+ not outrun it) and the CLI falls back to paste if it can't start or times out.
148
+ Every login binds a random OAuth `state` (login-CSRF protection, 2026-07-04):
149
+ the listener 400s (and keeps waiting on) redirects that don't echo it, and the
150
+ paste parser rejects a mismatching URL — a pasted bare code is exempt.
151
+ `anafpy auth logout` is **purely local** — it clears the token store and makes
152
+ no network call: ANAF's documented `/revoke` is not reachable headlessly
153
+ (live-probed 2026-07-05: 302 to the F5 APM login wall, identical to a
154
+ nonexistent path — see the oauth reference §3), so tokens end only by expiry
155
+ or the portal's Renunțare Oauth. Don't (re)add a revoke call unless ANAF
156
+ actually routes the endpoint.
157
+ Token persistence is the `TokenStore` protocol (`load`/`save`/`clear`):
158
+ `KeyringTokenStore` (OS credential store — the **default** backend since
159
+ 2026-07-05, `keyring` is a core dependency; splits the set across vault
160
+ entries on Windows, whose 2560-byte blob cap is smaller than one ANAF JWT) or
161
+ `FileTokenStore` (JSON file, the opt-out for Docker/headless hosts); selected
162
+ by `ANAFPY_TOKEN_STORE_BACKEND` / `--store-backend`. The test suite installs
163
+ an in-memory fake keyring **autouse** so no test can touch the real OS store.
164
+ - **Clients are async**, own their `httpx.AsyncClient` (unless one is injected), and are
165
+ async context managers (`async with EFacturaClient(...) as c:`).
166
+ - **Discrete methods do NO transport retry** — one call, one result-or-raise — so the
167
+ non-idempotent `upload` POST is never silently repeated. Consumers bring their own
168
+ retry. `tenacity` is used in exactly one place: the `upload_and_wait` poll loop, which
169
+ retries on the *business* processing state, not on transport errors.
170
+ - **Module style**: `from __future__ import annotations`, explicit `__all__`, module +
171
+ class docstrings, Google-style docstring sections. Line length 88. Keep new code in the
172
+ voice of the surrounding files.
173
+ - **English identifiers everywhere in hand-written code**: variables, parameters, and
174
+ model field names are descriptive English; ANAF's Romanian wire names survive only
175
+ as Pydantic validation aliases (`data_creare` -> `created_at`), string literals
176
+ (dict keys, query params, URL segments), and the generated schema models. Domain
177
+ acronyms with no sensible translation (`cui`, `cif`, `uit`, `caen`) and enum
178
+ members named after ANAF's own codes stay as-is.
179
+
180
+ ## MCP server (`anafpy.mcp`)
181
+
182
+ - **Local stdio connector built on the phase-1 clients.** `create_server(config)`
183
+ returns a `FastMCP`; `AppContext` owns one `TokenProvider` + lazily-built clients and
184
+ closes them in the server lifespan. The server reads the existing token store and
185
+ refreshes headlessly — it never drives the cert/browser step (that stays the CLI).
186
+ - **Workflow skills are served as MCP prompts** (2026-07-03). Each
187
+ `skills/*/SKILL.md` is served as a same-name prompt (`anafpy.mcp.skills` reads
188
+ frontmatter + body; optional `source` argument seeds the workflow), giving
189
+ prompt-capable clients (Claude Desktop, `claude mcp add`) the playbooks as a
190
+ user-invoked entry point. The SKILL.md files are the single source of
191
+ truth — never duplicate their content into the server; parsing is
192
+ `python-frontmatter`'s (the `mcp` extra), with `skills.py` only enforcing that
193
+ `name`/`description` are present (missing fields fail loudly at server start).
194
+ - **No e-Factura filing tools** (removed 2026-07-03): outbound invoices come from
195
+ third-party invoicing software, which files with ANAF directly — there is no MCP
196
+ use case, so the e-Factura surface is **read-only** (inbox, download,
197
+ `efactura_validate`); `efactura_get_status` went with the filing tools — an
198
+ e-Factura upload id was only ever produced by them. `EFacturaClient.upload` /
199
+ `get_status` stay for library users. If filing
200
+ tools ever return, the pass-through rule still applies: the input must be the
201
+ complete UBL XML the caller's software exported (`UblXmlInput` in
202
+ `mcp/models.py`, now feeding only `efactura_validate`) — never composed, never
203
+ the generated UBL schema models as tool input, no flat→UBL write mapping.
204
+ - **Binary artifacts go to disk (or a resource), never into context.** The model
205
+ works from the flat `invoice` view; `efactura_download` optionally writes the
206
+ signed archive ZIP (`save_zip_as`) and ANAF's `transformare` PDF rendering
207
+ (`save_pdf_as`, best-effort — failures surface in `pdf_error`, never fail the
208
+ download; rendered with `validate=False` since the message already passed ANAF
209
+ validation at filing) to caller-given paths — the server is local stdio, so its
210
+ filesystem is the user's (this is what enables batch flows like "save last
211
+ month's invoices as `<date> - <partner>.pdf`", where the agent names the files).
212
+ An **existing file is never silently replaced** (added 2026-07-04): a name
213
+ collision is refused and reported in `pdf_error`/`zip_error`, and only an
214
+ explicit `overwrite=true` replaces the file — so a batch flow can't lose an
215
+ invoice to two identical names, while a deliberate re-export stays one flag away.
216
+ The PDF is also the resource template `anafmsg://{message_id}/pdf`
217
+ (fetch+convert on read); there is deliberately **no ZIP resource** — a base64
218
+ ZIP serves neither the model nor any host UI. Don't return base64 blobs from
219
+ tools. (decided 2026-07-03; the
220
+ pass-through rule is e-Factura-only). `etransport_prepare_declaration` /
221
+ `_deletion` / `_confirmation` / `_vehicle_change` take the client-layer flat models
222
+ or scalars, render the XML via `render_etransport`, and return it in
223
+ `PreparedSubmission.xml` alongside the preview and token; the caller passes that
224
+ XML back to `etransport_submit` verbatim (the token is bound to the rendered
225
+ bytes, so any mangling fails closed). `etransport_prepare` (`EtransportXmlInput`)
226
+ remains for callers with ready-made XML. `etransport_nomenclature` lists the XSD
227
+ code lists (names accepted anywhere an enum-coded field is) plus the code-only
228
+ `unit_codes` — the UN/ECE Rec 20/21 list ANAF's Schematron enforces for goods
229
+ lines, carried in [mcp/unitcodes.py](src/anafpy/mcp/unitcodes.py) — see
230
+ [mcp/nomenclatures.py](src/anafpy/mcp/nomenclatures.py).
231
+ - **`FlatInvoice` is a read view; the e-Transport flat models are bidirectional.**
232
+ All are defined at the **client layer**
233
+ ([efactura/models.py](src/anafpy/efactura/models.py),
234
+ [etransport/models.py](src/anafpy/etransport/models.py)). `FlatInvoice` is produced
235
+ *from* UBL by `read_flat_invoice` (UBL→flat only), backs `DownloadedMessage.view`
236
+ (`download` tier 3) and the e-Factura inbox, is lossy by
237
+ design — raw bytes + full UBL stay authoritative — and carries `complete` /
238
+ `dropped_fields` when it can't represent something. There is no flat→UBL path; do
239
+ not add one. The e-Transport `FlatTransport` / `FlatDeletion` / `FlatConfirmation`
240
+ / `FlatVehicleChange` (union `FlatSubmission`) are a **full translation** of the
241
+ XSD: `read_flat_transport` views, `build_etransport` / `render_etransport` author
242
+ (only the schema's unused `xs:any` hooks are not carried); enum-coded fields are
243
+ typed with the generated XSD enums, accept name or code, and serialize as names.
244
+ - **Tool display names**: every tool has an English MCP `title` following
245
+ `Service: operation` — services are `e-Factura`, `e-Transport`, `ANAF Info`
246
+ (public no-auth lookups), plus bare `ANAF` for `auth_status`. Titles are
247
+ UI-only (the model sees `name` + `description`); keep them single-language.
248
+ - **Branded service names in prose**: in strings, messages, and docs the services
249
+ are written exactly `e-Factura` and `e-Transport` — even at the start of a
250
+ sentence or title. This is the branding ANAF itself uses on its website
251
+ (decided 2026-07-03). Exceptions: identifiers stay English-convention
252
+ (`EFacturaClient`, `efactura_*`), ANAF wire facts stay verbatim (the
253
+ `eTransport` XML root/namespace, endpoint names, URLs), and quotes of ANAF's
254
+ own material in `docs/anaf-reference/` keep ANAF's spelling.
255
+ - **Read-first, two-step gated mutations.** Read-only tools (`*_list*`, `*_status`,
256
+ `*_lookup`, `etransport_nomenclature`, `auth_status`, and — over the no-auth
257
+ `PublicClient`, so usable even with no OAuth credentials configured —
258
+ `efactura_validate` and the `anaf_*` public lookups, registries + financial
259
+ statements) are annotated `readOnlyHint` and freely
260
+ callable. `efactura_download` is also freely callable but carries honest
261
+ annotations (`readOnlyHint=False`, idempotent, non-destructive) because it may
262
+ write files at caller-given paths; the two-step gate is for ANAF filings only. Filing (e-Transport only) is split `etransport_prepare*` →
263
+ `etransport_submit`: prepare
264
+ parses (or composes) the XML for a preview and returns an HMAC **confirmation token**
265
+ (`mcp/tokens.py`) bound to the exact XML bytes and the CIF;
266
+ submit requires that token (same document, same CIF) **and** `confirm=True`,
267
+ and each token is **single-use** (`TokenLedger`) so a non-idempotent upload is never
268
+ repeated on one approval. Don't collapse this into a `dry_run` bool.
269
+ - **Validation is ANAF's, not local.** `efactura_validate` calls the server-side
270
+ `validare` endpoint via `PublicClient.validate_invoice` (authoritative by
271
+ definition). `validare`/`transformare` are **public, no-auth, prod-only** (their
272
+ TEST paths 404), which is why they live on `PublicClient`
273
+ (`webservicesp.anaf.ro/prod`) — validation works on test configs too, needs no
274
+ OAuth credentials, and files
275
+ nothing; e-Transport has no standalone validator — ANAF validates on upload.
276
+ There is deliberately **no local rule engine** (a Schematron/saxonche extra existed
277
+ and was removed 2026-07-02); prepare never blocks on validation — the human review +
278
+ ANAF's verdict are the gates. Don't reintroduce local validation. Distinct from
279
+ that: the e-Transport flat models carry **field-level shape checks** — the XSD
280
+ constraints tightened by the *unconditional* rules of ANAF's e-Transport
281
+ Schematron (UIT check digits, gross ≥ net, `ALTELE` needs a note, ...; the list
282
+ is in DESIGN.md §5) — which fail at model construction as data hygiene. The
283
+ Schematron's operation-type *conditional* rules stay ANAF's and appear only as
284
+ field descriptions.
285
+
286
+ ## Error model (important)
287
+
288
+ Hybrid, per design — do not collapse it:
289
+
290
+ - **Exceptions** (`AnafError` → `AnafAuthError`, `AnafTransportError`/`AnafResponseError`,
291
+ `AnafRateLimitError`, `AnafConfigError`) are for transport / auth / programming errors.
292
+ HTTP 429 raises `AnafRateLimitError` exposing `retry_after`; the client does **not**
293
+ auto-back-off.
294
+ - **Business outcomes** (e-Factura `nok`/`REJECTED`, upload rejections with BR-RO
295
+ findings) are returned as **typed values** (e.g. `UploadResult.accepted is False`,
296
+ `MessageStatus.state`), never raised.
297
+ - **Listing and `info`** (`list_messages` / `list_notifications` / e-Transport `info`) are
298
+ where a 200-with-error-note is split: ANAF overloads the note (e-Factura: `eroare`;
299
+ e-Transport: `Errors[].errorMessage`, `info` also a top-level `error` string) for both
300
+ "no results" and real errors, so a no-results note yields an **empty iterator** (`info`:
301
+ an empty `InfoList` with the note in `.error`) while a genuine error **raises
302
+ `AnafResponseError`** (`status_code=200`). The classifier is
303
+ `_transport.base.is_empty_result_message`.
304
+
305
+ ## Generated code — do not hand-edit
306
+
307
+ `src/anafpy/efactura/ubl/` and `src/anafpy/etransport/schema/` are generated by the
308
+ `scripts/generate_*.py` scripts from vendored XSDs in `schemas/`. They are committed as
309
+ source but excluded from ruff, mypy, and pyright/Pylance (see `extend-exclude` /
310
+ `exclude` in [pyproject.toml](pyproject.toml)). To change them, edit the script /
311
+ re-vendor the XSD and regenerate; never edit the output by hand. Note: `xsdata[cli]`
312
+ is pinned `<25` — the `xsdata-pydantic` plugin targets the 24.x line and newer core
313
+ emits invalid fields. The e-Transport script post-processes the output: nomenclature
314
+ enum members get descriptive names derived from the XSD's own `xs:documentation`
315
+ annotations (`CodJudetType.CLUJ`, `CodTaraType.ROMANIA`; operation types use ANAF's
316
+ sigla with the full label as a trailing comment: `CodTipOperatiuneType.TTN`, ...)
317
+ instead of `VALUE_<code>`.
318
+
319
+ Public UBL entry points: `from anafpy.efactura import Invoice, CreditNote`.
320
+
321
+ ## ANAF response formats
322
+
323
+ Response schemas come from ANAF's official per-endpoint **swagger presentations**
324
+ (vendored 2026-07-02 under `docs/anaf-reference/_sources/{efactura,etransport}-swagger/`
325
+ and folded into `docs/anaf-reference/*/api.md`) — the API PDFs cover URLs/params only.
326
+ First live TEST confirmations 2026-07-02: the e-Factura paginated list's no-results
327
+ shape (200 + `eroare` note) and the e-Transport `lista` no-results shape
328
+ (`Errors[].errorMessage`, `ExecutionStatus: 1`) both matched the docs exactly. A full
329
+ e-Transport TEST **roundtrip** 2026-07-02 (upload → `stareMesaj` `in prelucrare`→`ok`
330
+ → `lista` → `info`) confirmed the upload/status/lista-with-results shapes and surfaced
331
+ one doc gap: `info`'s no-results case rides a **top-level singular `error` string**
332
+ (not `Errors[]`) — now handled by `_InfoEnvelope` / `_parse_info`. A full **e-Factura
333
+ TEST roundtrip** the same day (upload → `stareMesaj` `in prelucrare`→`ok` →
334
+ `descarcare` ZIP → paginated list with results) confirmed the e-Factura
335
+ upload/status/download shapes, and established that **`validare` and `transformare`
336
+ are prod-only** (the `test` paths answer HTTP 404) — since they are also public and
337
+ no-auth, they live on `PublicClient` (`validate_invoice` / `render_invoice_pdf`),
338
+ which always calls them on `webservicesp.anaf.ro/prod`. A **production** message-list
339
+ pull (2026-07-06, 522 messages) established that the list's `cif_emitent` /
340
+ `cif_beneficiar` are **never emitted** despite ANAF's API PDF listing them as
341
+ response fields — the swagger `Mesaj` schema (the response-schema authority) omits
342
+ them, and the CIFs ride only inside the free-text `detalii` (see the e-Factura
343
+ reference §3). The
344
+ `live`-marked `tests/test_oauth_live.py` re-confirms the authenticated TEST shapes on
345
+ demand (needs `.env` credentials + `anafpy auth login`). The **public services** have no swagger —
346
+ their reference (`docs/anaf-reference/public/api.md`) is compiled from ANAF's
347
+ instruction files and **was live-confirmed in production** (2026-07-02); the `live`
348
+ test marker re-confirms those shapes on demand. When touching parsing code, treat the
349
+ doc as the source of truth and prefer being explicit over silently returning empty
350
+ results.
351
+
352
+ ## Conventions for changes
353
+
354
+ - Keep `pytest`, `ruff`, and `mypy --strict` green; add/extend respx tests for client
355
+ behavior changes (upload→poll→download, `nok` path, 401-refresh, 429 surfacing).
356
+ The respx suite is the gate; the `live`-marked smoke tests
357
+ ([tests/test_public_live.py](tests/test_public_live.py) — public services;
358
+ [tests/test_oauth_live.py](tests/test_oauth_live.py) — authenticated TEST, read-only,
359
+ credentials from the gitignored repo-root `.env` loaded by `tests/conftest.py`) exist
360
+ only to re-confirm wire shapes on demand (`ANAFPY_LIVE=1`) and are skipped by
361
+ default — don't move behavioural assertions there, and keep them read-only. The **two
362
+ deliberate exceptions** are the roundtrip files —
363
+ [tests/test_etransport_roundtrip_live.py](tests/test_etransport_roundtrip_live.py)
364
+ **files** a domestic declaration composed via the flat authoring models — also
365
+ keeping anafpy's own rendered XML honest — (upload → `stareMesaj` → `lista` →
366
+ `info`) and
367
+ [tests/test_efactura_roundtrip_live.py](tests/test_efactura_roundtrip_live.py)
368
+ **files** a minimal CIUS-RO invoice (upload → `stareMesaj` → `descarcare` → list) —
369
+ **TEST only, never prod** — to keep the filing wire shapes honest; don't add uploads
370
+ to any other live file.
371
+ - **Keep the docs in sync with the change.** When a change alters the public surface,
372
+ status, layout, or conventions, update the affected docs in the same change:
373
+ [README.md](README.md) (what works / usage / install), [INSTALL.md](INSTALL.md)
374
+ (the end-user setup walkthrough — accountant audience: ANAF app registration,
375
+ cert login, Claude/Cowork config), this `CLAUDE.md` (layout,
376
+ commands, conventions), [DESIGN.md](DESIGN.md) (design decisions), and
377
+ `docs/anaf-reference/` (only when ANAF API facts change — keep its provenance
378
+ frontmatter intact). Don't let docs drift behind the code.
379
+ - **Repo boundary**: this repo is the whole project (typed clients + local stdio MCP
380
+ server + skills + docs) and is intended to be publishable. Never add hosted-service
381
+ code here — token custody, multi-tenancy, and an OAuth-provider surface toward
382
+ Claude are out of scope (DESIGN.md §11, decided 2026-07-04).
383
+ - Don't commit, push, or create branches/PRs unless asked.
384
+ - The remote is `github.com/robert-malai/anafpy`. CI is GitHub Actions:
385
+ `.github/workflows/ci.yml` runs the three gates on 3.12 + 3.13 for every
386
+ push/PR; `release.yml` re-runs them on a `v*` tag, checks the tag against
387
+ `pyproject.toml`'s version, builds, and publishes to PyPI via trusted
388
+ publishing (OIDC, environment `pypi` — no stored token). The version is
389
+ declared in `pyproject.toml` **and** `anafpy.__version__`;
390
+ `tests/test_version.py` keeps them agreeing.