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.
- pyefris-1.0.0/.github/workflows/publish.yml +41 -0
- pyefris-1.0.0/.gitignore +16 -0
- pyefris-1.0.0/AGENTS.md +34 -0
- pyefris-1.0.0/CHANGELOG.md +19 -0
- pyefris-1.0.0/LICENSE +21 -0
- pyefris-1.0.0/PKG-INFO +140 -0
- pyefris-1.0.0/README.md +110 -0
- pyefris-1.0.0/docs/How+to+use+the+System+to+System+API_V3.pdf +0 -0
- pyefris-1.0.0/docs/Interface+Design+for+EFRIS+v24.0.1URA.md +12714 -0
- pyefris-1.0.0/docs/Interface+Design+for+EFRIS+v24.0.1URA.pdf +0 -0
- pyefris-1.0.0/docs/Step+by+step+guide+on+how+to+integrate+System2system+on+EFRIS.pdf +0 -0
- pyefris-1.0.0/docs/howto.txt +159 -0
- pyefris-1.0.0/docs/stepguide.txt +708 -0
- pyefris-1.0.0/examples/01_generate_keys.py +22 -0
- pyefris-1.0.0/examples/02_server_time_and_login.py +21 -0
- pyefris-1.0.0/examples/03_submit_invoice.py +79 -0
- pyefris-1.0.0/examples/04_replay_queue.py +18 -0
- pyefris-1.0.0/pyproject.toml +64 -0
- pyefris-1.0.0/src/pyefris/__init__.py +88 -0
- pyefris-1.0.0/src/pyefris/__main__.py +77 -0
- pyefris-1.0.0/src/pyefris/_client_base.py +173 -0
- pyefris-1.0.0/src/pyefris/_runner.py +479 -0
- pyefris-1.0.0/src/pyefris/aclient.py +77 -0
- pyefris-1.0.0/src/pyefris/client.py +148 -0
- pyefris-1.0.0/src/pyefris/clock.py +81 -0
- pyefris-1.0.0/src/pyefris/config.py +134 -0
- pyefris-1.0.0/src/pyefris/constants.py +85 -0
- pyefris-1.0.0/src/pyefris/crypto/__init__.py +40 -0
- pyefris-1.0.0/src/pyefris/crypto/aes.py +76 -0
- pyefris-1.0.0/src/pyefris/crypto/keys.py +66 -0
- pyefris-1.0.0/src/pyefris/crypto/rsa.py +137 -0
- pyefris-1.0.0/src/pyefris/crypto/session.py +112 -0
- pyefris-1.0.0/src/pyefris/envelope.py +333 -0
- pyefris-1.0.0/src/pyefris/errors.py +177 -0
- pyefris-1.0.0/src/pyefris/interfaces.py +241 -0
- pyefris-1.0.0/src/pyefris/logging.py +38 -0
- pyefris-1.0.0/src/pyefris/models/__init__.py +12 -0
- pyefris-1.0.0/src/pyefris/models/base.py +118 -0
- pyefris-1.0.0/src/pyefris/models/batch_models.py +35 -0
- pyefris-1.0.0/src/pyefris/models/dictionary_models.py +144 -0
- pyefris-1.0.0/src/pyefris/models/goods_models.py +171 -0
- pyefris-1.0.0/src/pyefris/models/invoice_models.py +554 -0
- pyefris-1.0.0/src/pyefris/models/session_models.py +156 -0
- pyefris-1.0.0/src/pyefris/models/stock_models.py +153 -0
- pyefris-1.0.0/src/pyefris/models/taxpayer_models.py +119 -0
- pyefris-1.0.0/src/pyefris/outbox.py +126 -0
- pyefris-1.0.0/src/pyefris/serialization.py +57 -0
- pyefris-1.0.0/src/pyefris/services/__init__.py +17 -0
- pyefris-1.0.0/src/pyefris/services/base.py +53 -0
- pyefris-1.0.0/src/pyefris/services/dictionary.py +79 -0
- pyefris-1.0.0/src/pyefris/services/goods.py +61 -0
- pyefris-1.0.0/src/pyefris/services/invoicing.py +262 -0
- pyefris-1.0.0/src/pyefris/services/session.py +35 -0
- pyefris-1.0.0/src/pyefris/services/stock.py +115 -0
- pyefris-1.0.0/src/pyefris/services/taxpayer.py +43 -0
- pyefris-1.0.0/src/pyefris/store/__init__.py +29 -0
- pyefris-1.0.0/src/pyefris/store/base.py +132 -0
- pyefris-1.0.0/src/pyefris/store/memory.py +174 -0
- pyefris-1.0.0/src/pyefris/store/schema.sql +73 -0
- pyefris-1.0.0/src/pyefris/store/sqlite.py +307 -0
- pyefris-1.0.0/src/pyefris/transport.py +118 -0
- pyefris-1.0.0/tests/__init__.py +0 -0
- pyefris-1.0.0/tests/conftest.py +115 -0
- pyefris-1.0.0/tests/integration/__init__.py +0 -0
- pyefris-1.0.0/tests/integration/test_async_client.py +40 -0
- pyefris-1.0.0/tests/integration/test_client.py +140 -0
- pyefris-1.0.0/tests/unit/__init__.py +0 -0
- pyefris-1.0.0/tests/unit/test_crypto.py +103 -0
- pyefris-1.0.0/tests/unit/test_envelope.py +127 -0
- pyefris-1.0.0/tests/unit/test_models.py +89 -0
- pyefris-1.0.0/tests/unit/test_outbox.py +92 -0
- pyefris-1.0.0/tests/unit/test_runner.py +169 -0
- 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
|
pyefris-1.0.0/.gitignore
ADDED
pyefris-1.0.0/AGENTS.md
ADDED
|
@@ -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.
|
pyefris-1.0.0/README.md
ADDED
|
@@ -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.
|
|
Binary file
|