pyefris 1.0.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 (73) hide show
  1. pyefris-1.0.0/.github/workflows/publish.yml +41 -0
  2. pyefris-1.0.0/.gitignore +16 -0
  3. pyefris-1.0.0/AGENTS.md +34 -0
  4. pyefris-1.0.0/CHANGELOG.md +19 -0
  5. pyefris-1.0.0/LICENSE +21 -0
  6. pyefris-1.0.0/PKG-INFO +140 -0
  7. pyefris-1.0.0/README.md +110 -0
  8. pyefris-1.0.0/docs/How+to+use+the+System+to+System+API_V3.pdf +0 -0
  9. pyefris-1.0.0/docs/Interface+Design+for+EFRIS+v24.0.1URA.md +12714 -0
  10. pyefris-1.0.0/docs/Interface+Design+for+EFRIS+v24.0.1URA.pdf +0 -0
  11. pyefris-1.0.0/docs/Step+by+step+guide+on+how+to+integrate+System2system+on+EFRIS.pdf +0 -0
  12. pyefris-1.0.0/docs/howto.txt +159 -0
  13. pyefris-1.0.0/docs/stepguide.txt +708 -0
  14. pyefris-1.0.0/examples/01_generate_keys.py +22 -0
  15. pyefris-1.0.0/examples/02_server_time_and_login.py +21 -0
  16. pyefris-1.0.0/examples/03_submit_invoice.py +79 -0
  17. pyefris-1.0.0/examples/04_replay_queue.py +18 -0
  18. pyefris-1.0.0/pyproject.toml +64 -0
  19. pyefris-1.0.0/src/pyefris/__init__.py +88 -0
  20. pyefris-1.0.0/src/pyefris/__main__.py +77 -0
  21. pyefris-1.0.0/src/pyefris/_client_base.py +173 -0
  22. pyefris-1.0.0/src/pyefris/_runner.py +479 -0
  23. pyefris-1.0.0/src/pyefris/aclient.py +77 -0
  24. pyefris-1.0.0/src/pyefris/client.py +148 -0
  25. pyefris-1.0.0/src/pyefris/clock.py +81 -0
  26. pyefris-1.0.0/src/pyefris/config.py +134 -0
  27. pyefris-1.0.0/src/pyefris/constants.py +85 -0
  28. pyefris-1.0.0/src/pyefris/crypto/__init__.py +40 -0
  29. pyefris-1.0.0/src/pyefris/crypto/aes.py +76 -0
  30. pyefris-1.0.0/src/pyefris/crypto/keys.py +66 -0
  31. pyefris-1.0.0/src/pyefris/crypto/rsa.py +137 -0
  32. pyefris-1.0.0/src/pyefris/crypto/session.py +112 -0
  33. pyefris-1.0.0/src/pyefris/envelope.py +333 -0
  34. pyefris-1.0.0/src/pyefris/errors.py +177 -0
  35. pyefris-1.0.0/src/pyefris/interfaces.py +241 -0
  36. pyefris-1.0.0/src/pyefris/logging.py +38 -0
  37. pyefris-1.0.0/src/pyefris/models/__init__.py +12 -0
  38. pyefris-1.0.0/src/pyefris/models/base.py +118 -0
  39. pyefris-1.0.0/src/pyefris/models/batch_models.py +35 -0
  40. pyefris-1.0.0/src/pyefris/models/dictionary_models.py +144 -0
  41. pyefris-1.0.0/src/pyefris/models/goods_models.py +171 -0
  42. pyefris-1.0.0/src/pyefris/models/invoice_models.py +554 -0
  43. pyefris-1.0.0/src/pyefris/models/session_models.py +156 -0
  44. pyefris-1.0.0/src/pyefris/models/stock_models.py +153 -0
  45. pyefris-1.0.0/src/pyefris/models/taxpayer_models.py +119 -0
  46. pyefris-1.0.0/src/pyefris/outbox.py +126 -0
  47. pyefris-1.0.0/src/pyefris/serialization.py +57 -0
  48. pyefris-1.0.0/src/pyefris/services/__init__.py +17 -0
  49. pyefris-1.0.0/src/pyefris/services/base.py +53 -0
  50. pyefris-1.0.0/src/pyefris/services/dictionary.py +79 -0
  51. pyefris-1.0.0/src/pyefris/services/goods.py +61 -0
  52. pyefris-1.0.0/src/pyefris/services/invoicing.py +262 -0
  53. pyefris-1.0.0/src/pyefris/services/session.py +35 -0
  54. pyefris-1.0.0/src/pyefris/services/stock.py +115 -0
  55. pyefris-1.0.0/src/pyefris/services/taxpayer.py +43 -0
  56. pyefris-1.0.0/src/pyefris/store/__init__.py +29 -0
  57. pyefris-1.0.0/src/pyefris/store/base.py +132 -0
  58. pyefris-1.0.0/src/pyefris/store/memory.py +174 -0
  59. pyefris-1.0.0/src/pyefris/store/schema.sql +73 -0
  60. pyefris-1.0.0/src/pyefris/store/sqlite.py +307 -0
  61. pyefris-1.0.0/src/pyefris/transport.py +118 -0
  62. pyefris-1.0.0/tests/__init__.py +0 -0
  63. pyefris-1.0.0/tests/conftest.py +115 -0
  64. pyefris-1.0.0/tests/integration/__init__.py +0 -0
  65. pyefris-1.0.0/tests/integration/test_async_client.py +40 -0
  66. pyefris-1.0.0/tests/integration/test_client.py +140 -0
  67. pyefris-1.0.0/tests/unit/__init__.py +0 -0
  68. pyefris-1.0.0/tests/unit/test_crypto.py +103 -0
  69. pyefris-1.0.0/tests/unit/test_envelope.py +127 -0
  70. pyefris-1.0.0/tests/unit/test_models.py +89 -0
  71. pyefris-1.0.0/tests/unit/test_outbox.py +92 -0
  72. pyefris-1.0.0/tests/unit/test_runner.py +169 -0
  73. pyefris-1.0.0/tests/unit/test_store.py +138 -0
@@ -0,0 +1,41 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.11"
16
+ - name: Install package with dev deps
17
+ run: pip install -e ".[dev]"
18
+ - name: Lint
19
+ run: ruff check src tests
20
+ - name: Test
21
+ run: pytest tests/
22
+
23
+ pypi:
24
+ name: Publish to PyPI
25
+ needs: test
26
+ runs-on: ubuntu-latest
27
+ environment: pypi
28
+ permissions:
29
+ id-token: write # OIDC trusted publishing — no API token needed
30
+ contents: read # required for actions/checkout (id-token alone strips defaults)
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.11"
36
+ - name: Install build
37
+ run: pip install build
38
+ - name: Build sdist and wheel
39
+ run: python -m build --sdist --wheel
40
+ - name: Publish to PyPI
41
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .pytest_cache/
12
+ *.db
13
+ *.db-wal
14
+ *.db-shm
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,34 @@
1
+ # pyefris — agent notes
2
+
3
+ ## What this is
4
+ Python client for URA EFRIS S2S API (spec: `docs/Interface+Design+for+EFRIS+v24.0.1URA.md`).
5
+ Greenfield build; no external service access required for dev/test.
6
+
7
+ ## Layout
8
+ - `src/pyefris/constants.py` — endpoints, codes, flags (single source of truth)
9
+ - `src/pyefris/interfaces.py` — 74-code registry w/ per-interface encrypt flags
10
+ - `src/pyefris/crypto/` — AES-ECB, RSA(SHA1withRSA/PKCS1), SessionKeyManager
11
+ - `src/pyefris/envelope.py` — envelope build/parse/decode
12
+ - `src/pyefris/_runner.py` — submit pipeline (record→seal→send→finalize, 402 refresh)
13
+ - `src/pyefris/outbox.py` — relay_pending + RelayWorker
14
+ - `src/pyefris/store/` — Storage protocol, SQLiteStorage (WAL), InMemoryStorage
15
+ - `src/pyefris/models/` — pydantic v2 wire models (alias_generator=to_camel;
16
+ exceptions use explicit validation/serialization alias pairs)
17
+ - `src/pyefris/services/` — typed service facades; work for sync AND async
18
+ (methods return whatever runner.submit returns)
19
+ - `src/pyefris/{client,aclient,_client_base}.py` — facades + wiring
20
+ - `tests/` — pytest; StubTransport scripts responses; no network
21
+
22
+ ## Commands
23
+ - Test: `python -m pytest tests/` (pythonpath=src is set in pyproject)
24
+ - Lint: `python -m ruff check src tests`
25
+ - Types: `python -m mypy src/pyefris` (strict)
26
+
27
+ ## Conventions
28
+ - Wire names live in model serialization aliases — constructor kwargs are
29
+ snake_case; `to_payload()` emits spec names (`mode="json"` → StrDecimal->str).
30
+ - All EFRIS leaf values are strings — use `StrDecimal`, never float for money.
31
+ - New interface: add/adjust `InterfaceSpec` in `interfaces.py`, add models,
32
+ add a service method. Bare-list responses: set `LIST_FIELD` on the model.
33
+ - Every submission must flow through OperationRunner (audit + idempotency).
34
+ - Never rebuild an envelope on replay — resend the stored one verbatim.
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 (2026-10-27)
4
+
5
+ Initial release.
6
+
7
+ - EFRIS v24.0.1 S2S protocol: envelope build/parse, per-interface encryption
8
+ flags for all 74 registered interface codes.
9
+ - Online crypto: AES-ECB/PKCS7 content, SHA1withRSA request signatures,
10
+ RSA/PKCS1 T104 session-key unwrap, auto refresh on 401/402.
11
+ - Offline payload mode (base64 content, empty signature).
12
+ - ACID outbox: SQLite WAL store (`synchronous=FULL`), atomic seal+claim+
13
+ finalize transactions, CAS claims, append-only audit log, unique
14
+ reference-number idempotency, replayable queue with backoff + worker thread.
15
+ - Typed pydantic v2 models for ~40 core interfaces (session, taxpayer,
16
+ dictionary, goods, stock, invoicing/credit-notes, batch); generic
17
+ `client.call()` for the rest.
18
+ - Sync `EfrisClient` + `AsyncEfrisClient` (httpx), `python -m pyefris` CLI,
19
+ runnable examples.
pyefris-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pyefris contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyefris-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyefris
3
+ Version: 1.0.0
4
+ Summary: Python client for URA EFRIS system-to-system e-invoicing integration
5
+ Project-URL: Homepage, https://efris.ura.go.ug/
6
+ Author: pyefris contributors
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: e-invoicing,efris,fiscal,uganda,ura
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: cryptography>=42
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.7
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.11; extra == 'dev'
25
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: respx>=0.21; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pyefris
32
+
33
+ Python client for the **URA EFRIS** (Electronic Fiscal Receipting and Invoicing
34
+ Solution) system-to-system API, per *Interface Design for EFRIS v24.0.1*.
35
+
36
+ Features:
37
+
38
+ - Full wire protocol: `{data, globalInfo, returnStateInfo}` envelope, one POST
39
+ endpoint, `dataDescription` encryption flags per interface.
40
+ - Online-mode crypto exactly as URA requires:
41
+ `content = base64(AES-ECB/PKCS7(json))`, `signature = base64(SHA1withRSA(content))`,
42
+ T104 session-key unwrap (RSA/PKCS1), auto-refresh on returnCode `401`/`402`.
43
+ - Offline-mode payload profile (base64 content, empty signature).
44
+ - **ACID outbox**: every submission is recorded atomically in a SQLite
45
+ (WAL, `synchronous=FULL`) store before it goes on the wire — idempotent
46
+ retries, crash-safe replay, append-only audit log.
47
+ - Typed pydantic v2 models for the core ~40 interfaces; `client.call(code, ...)`
48
+ generic escape hatch for the rest of the 74-code registry.
49
+ - Sync `EfrisClient` and `AsyncEfrisClient` (httpx).
50
+ - Python ≥ 3.10. Dependencies: `httpx`, `pydantic>=2`, `cryptography`.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install -e . # local
56
+ pip install -e ".[dev]" # with test/lint tooling
57
+ ```
58
+
59
+ ## Quickstart
60
+
61
+ ```python
62
+ from pyefris import EfrisClient, EfrisConfig
63
+
64
+ cfg = EfrisConfig(
65
+ tin="1000029771",
66
+ device_no="TCS9e0df01728335239",
67
+ private_key_path="keys/taxpayer_private.pem", # RSA-2048, registered w/ URA
68
+ environment="test", # or "prod"
69
+ store_path="pyefris.db",
70
+ )
71
+
72
+ with EfrisClient(cfg) as client:
73
+ client.sync_clock() # T101: requestTime within ±10min
74
+ result = client.invoicing.upload_invoice(invoice, reference_no="INV-0001")
75
+ doc = result.model.exist_invoice_list[0]
76
+ print(doc.invoice_no, doc.antifake_code, doc.qr_code)
77
+ ```
78
+
79
+ Domain services: `client.session` (T101–T105), `.taxpayer` (T119/T137/T138/T180),
80
+ `.dictionary` (T115/T123–T126/T134/T146), `.goods` (T127/T128/T130/T144),
81
+ `.stock` (T131/T139/T145/T147–T149/T160), `.invoicing`
82
+ (T106–T114/T116–T118/T120–T122/T129). Unmodeled interfaces:
83
+ `client.call("T167", {...})`.
84
+
85
+ ## ACID guarantees
86
+
87
+ | Property | Mechanism |
88
+ |---|---|
89
+ | **Atomicity** | intent (PENDING) + sealed envelope (SEALED) written in one `BEGIN IMMEDIATE` tx before network I/O; outcome + audit row in a second tx |
90
+ | **Consistency** | `UNIQUE(tin, device_no, interface_code, reference_no)` blocks double-fiscalization; `CHECK` on status enum |
91
+ | **Isolation** | WAL + busy_timeout; claims are compare-and-swap `UPDATE ... WHERE status=?` — threads/processes can't double-send |
92
+ | **Durability** | `synchronous=FULL`; sealed envelope persisted before send; append-only `audit_log` (triggers forbid UPDATE/DELETE) |
93
+
94
+ Recovery: `client.relay_pending()` re-sends the **byte-identical** stored
95
+ envelope (same `dataExchangeId`, signature) so URA can dedupe. Run it on a
96
+ schedule or `client.start_worker()` for a background thread. Retries use
97
+ exponential backoff (`RetryPolicy`) for transport failures; business
98
+ rejections (`EfrisReturnCodeError`) are terminal by design.
99
+
100
+ ```python
101
+ result = client.invoicing.upload_invoice(inv, reference_no="INV-1") # raises on dup
102
+ result = client.invoicing.upload_invoice(inv, reference_no="INV-1",
103
+ on_duplicate="return") # returns stored record
104
+ ```
105
+
106
+ ## Config reference
107
+
108
+ `EfrisConfig(tin, device_no, private_key_pem|_path, environment="test"|"prod",
109
+ mode="online"|"offline", endpoint=..., store_path=..., timeout_seconds=30,
110
+ retry=RetryPolicy(...), server_public_key_pem=...)`. Also `EfrisConfig.from_env()`
111
+ reads `PYEFRIS_*` variables. CLI: `python -m pyefris server-time|login|
112
+ sync-dictionaries|relay|call`.
113
+
114
+ ## Security notes
115
+
116
+ - SHA1withRSA signatures and AES-ECB are **required by the URA spec**, not a
117
+ design choice of this package.
118
+ - The decrypted AES session key is cached in the SQLite `kv_state` table —
119
+ protect the store file with filesystem permissions.
120
+ - `RedactingFilter` (pyefris.logging) scrubs keys/blobs from log records.
121
+ - T102's white-box `clientPriKey`/`keyTable` decryption is AP01-only and not
122
+ implemented (S2S/AP04 uses your own registered keypair).
123
+
124
+ ## Spec caveats captured in code
125
+
126
+ - `passowrdDes`, `buyerPlaceOfBusi`, `VehichleNo` keep the spec's spellings.
127
+ - T113/T114/T105/T120/T122 return unencrypted responses even though the
128
+ request is encrypted.
129
+ - `InvoiceUpload` allows spec additions via `extra="allow"` — unknown fields
130
+ round-trip instead of being dropped.
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ python -m pytest tests/ # 50 tests, no network needed
136
+ python -m ruff check src tests
137
+ python -m mypy src/pyefris # strict mode
138
+ ```
139
+
140
+ See `AGENTS.md` for architecture notes.
@@ -0,0 +1,110 @@
1
+ # pyefris
2
+
3
+ Python client for the **URA EFRIS** (Electronic Fiscal Receipting and Invoicing
4
+ Solution) system-to-system API, per *Interface Design for EFRIS v24.0.1*.
5
+
6
+ Features:
7
+
8
+ - Full wire protocol: `{data, globalInfo, returnStateInfo}` envelope, one POST
9
+ endpoint, `dataDescription` encryption flags per interface.
10
+ - Online-mode crypto exactly as URA requires:
11
+ `content = base64(AES-ECB/PKCS7(json))`, `signature = base64(SHA1withRSA(content))`,
12
+ T104 session-key unwrap (RSA/PKCS1), auto-refresh on returnCode `401`/`402`.
13
+ - Offline-mode payload profile (base64 content, empty signature).
14
+ - **ACID outbox**: every submission is recorded atomically in a SQLite
15
+ (WAL, `synchronous=FULL`) store before it goes on the wire — idempotent
16
+ retries, crash-safe replay, append-only audit log.
17
+ - Typed pydantic v2 models for the core ~40 interfaces; `client.call(code, ...)`
18
+ generic escape hatch for the rest of the 74-code registry.
19
+ - Sync `EfrisClient` and `AsyncEfrisClient` (httpx).
20
+ - Python ≥ 3.10. Dependencies: `httpx`, `pydantic>=2`, `cryptography`.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install -e . # local
26
+ pip install -e ".[dev]" # with test/lint tooling
27
+ ```
28
+
29
+ ## Quickstart
30
+
31
+ ```python
32
+ from pyefris import EfrisClient, EfrisConfig
33
+
34
+ cfg = EfrisConfig(
35
+ tin="1000029771",
36
+ device_no="TCS9e0df01728335239",
37
+ private_key_path="keys/taxpayer_private.pem", # RSA-2048, registered w/ URA
38
+ environment="test", # or "prod"
39
+ store_path="pyefris.db",
40
+ )
41
+
42
+ with EfrisClient(cfg) as client:
43
+ client.sync_clock() # T101: requestTime within ±10min
44
+ result = client.invoicing.upload_invoice(invoice, reference_no="INV-0001")
45
+ doc = result.model.exist_invoice_list[0]
46
+ print(doc.invoice_no, doc.antifake_code, doc.qr_code)
47
+ ```
48
+
49
+ Domain services: `client.session` (T101–T105), `.taxpayer` (T119/T137/T138/T180),
50
+ `.dictionary` (T115/T123–T126/T134/T146), `.goods` (T127/T128/T130/T144),
51
+ `.stock` (T131/T139/T145/T147–T149/T160), `.invoicing`
52
+ (T106–T114/T116–T118/T120–T122/T129). Unmodeled interfaces:
53
+ `client.call("T167", {...})`.
54
+
55
+ ## ACID guarantees
56
+
57
+ | Property | Mechanism |
58
+ |---|---|
59
+ | **Atomicity** | intent (PENDING) + sealed envelope (SEALED) written in one `BEGIN IMMEDIATE` tx before network I/O; outcome + audit row in a second tx |
60
+ | **Consistency** | `UNIQUE(tin, device_no, interface_code, reference_no)` blocks double-fiscalization; `CHECK` on status enum |
61
+ | **Isolation** | WAL + busy_timeout; claims are compare-and-swap `UPDATE ... WHERE status=?` — threads/processes can't double-send |
62
+ | **Durability** | `synchronous=FULL`; sealed envelope persisted before send; append-only `audit_log` (triggers forbid UPDATE/DELETE) |
63
+
64
+ Recovery: `client.relay_pending()` re-sends the **byte-identical** stored
65
+ envelope (same `dataExchangeId`, signature) so URA can dedupe. Run it on a
66
+ schedule or `client.start_worker()` for a background thread. Retries use
67
+ exponential backoff (`RetryPolicy`) for transport failures; business
68
+ rejections (`EfrisReturnCodeError`) are terminal by design.
69
+
70
+ ```python
71
+ result = client.invoicing.upload_invoice(inv, reference_no="INV-1") # raises on dup
72
+ result = client.invoicing.upload_invoice(inv, reference_no="INV-1",
73
+ on_duplicate="return") # returns stored record
74
+ ```
75
+
76
+ ## Config reference
77
+
78
+ `EfrisConfig(tin, device_no, private_key_pem|_path, environment="test"|"prod",
79
+ mode="online"|"offline", endpoint=..., store_path=..., timeout_seconds=30,
80
+ retry=RetryPolicy(...), server_public_key_pem=...)`. Also `EfrisConfig.from_env()`
81
+ reads `PYEFRIS_*` variables. CLI: `python -m pyefris server-time|login|
82
+ sync-dictionaries|relay|call`.
83
+
84
+ ## Security notes
85
+
86
+ - SHA1withRSA signatures and AES-ECB are **required by the URA spec**, not a
87
+ design choice of this package.
88
+ - The decrypted AES session key is cached in the SQLite `kv_state` table —
89
+ protect the store file with filesystem permissions.
90
+ - `RedactingFilter` (pyefris.logging) scrubs keys/blobs from log records.
91
+ - T102's white-box `clientPriKey`/`keyTable` decryption is AP01-only and not
92
+ implemented (S2S/AP04 uses your own registered keypair).
93
+
94
+ ## Spec caveats captured in code
95
+
96
+ - `passowrdDes`, `buyerPlaceOfBusi`, `VehichleNo` keep the spec's spellings.
97
+ - T113/T114/T105/T120/T122 return unencrypted responses even though the
98
+ request is encrypted.
99
+ - `InvoiceUpload` allows spec additions via `extra="allow"` — unknown fields
100
+ round-trip instead of being dropped.
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ python -m pytest tests/ # 50 tests, no network needed
106
+ python -m ruff check src tests
107
+ python -m mypy src/pyefris # strict mode
108
+ ```
109
+
110
+ See `AGENTS.md` for architecture notes.