tx-verify 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 (34) hide show
  1. tx_verify-0.1.0/.github/workflows/ci.yml +52 -0
  2. tx_verify-0.1.0/.github/workflows/publish.yml +50 -0
  3. tx_verify-0.1.0/.gitignore +44 -0
  4. tx_verify-0.1.0/.python-version +1 -0
  5. tx_verify-0.1.0/PKG-INFO +381 -0
  6. tx_verify-0.1.0/README.md +339 -0
  7. tx_verify-0.1.0/examples/abyssinia.py +36 -0
  8. tx_verify-0.1.0/examples/cbe.py +36 -0
  9. tx_verify-0.1.0/examples/cbe_birr.py +44 -0
  10. tx_verify-0.1.0/examples/dashen.py +43 -0
  11. tx_verify-0.1.0/examples/error_handling.py +45 -0
  12. tx_verify-0.1.0/examples/image.py +54 -0
  13. tx_verify-0.1.0/examples/mpesa.py +38 -0
  14. tx_verify-0.1.0/examples/telebirr.py +37 -0
  15. tx_verify-0.1.0/examples/universal.py +62 -0
  16. tx_verify-0.1.0/pyproject.toml +124 -0
  17. tx_verify-0.1.0/scripts/test_proxy_scenarios.py +312 -0
  18. tx_verify-0.1.0/tests/conftest.py +7 -0
  19. tx_verify-0.1.0/tests/test_verify_mpesa.py +80 -0
  20. tx_verify-0.1.0/tx_verify/__init__.py +39 -0
  21. tx_verify-0.1.0/tx_verify/services/__init__.py +0 -0
  22. tx_verify-0.1.0/tx_verify/services/verify_abyssinia.py +156 -0
  23. tx_verify-0.1.0/tx_verify/services/verify_cbe.py +177 -0
  24. tx_verify-0.1.0/tx_verify/services/verify_cbe_birr.py +318 -0
  25. tx_verify-0.1.0/tx_verify/services/verify_dashen.py +253 -0
  26. tx_verify-0.1.0/tx_verify/services/verify_image.py +156 -0
  27. tx_verify-0.1.0/tx_verify/services/verify_mpesa.py +275 -0
  28. tx_verify-0.1.0/tx_verify/services/verify_telebirr.py +305 -0
  29. tx_verify-0.1.0/tx_verify/services/verify_universal.py +138 -0
  30. tx_verify-0.1.0/tx_verify/utils/__init__.py +15 -0
  31. tx_verify-0.1.0/tx_verify/utils/error_handler.py +59 -0
  32. tx_verify-0.1.0/tx_verify/utils/http_client.py +355 -0
  33. tx_verify-0.1.0/tx_verify/utils/logger.py +74 -0
  34. tx_verify-0.1.0/uv.lock +1767 -0
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ name: Lint & Type Check
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - uses: astral-sh/ruff-action@v3
21
+ with:
22
+ args: "check"
23
+
24
+ - name: Check formatting
25
+ run: ruff format --check .
26
+
27
+ - name: Install mypy
28
+ run: pip install mypy
29
+
30
+ - name: Type check
31
+ run: mypy tx_verify/
32
+
33
+ test:
34
+ name: Test (Python ${{ matrix.python-version }})
35
+ runs-on: ubuntu-latest
36
+ needs: [lint]
37
+ strategy:
38
+ fail-fast: false
39
+ matrix:
40
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+
44
+ - uses: actions/setup-python@v5
45
+ with:
46
+ python-version: ${{ matrix.python-version }}
47
+
48
+ - name: Install
49
+ run: pip install ".[dev]"
50
+
51
+ - name: Test
52
+ run: pytest -v
@@ -0,0 +1,50 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*" # e.g. v3.0.1
7
+ - "[0-9]+.*" # bare version e.g. 3.0.1
8
+
9
+ jobs:
10
+ build-and-publish:
11
+ name: Build & publish to PyPI
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ id-token: write # required for trusted publishing
15
+ contents: read
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Install build tools
25
+ run: pip install build
26
+
27
+ - name: Extract version
28
+ id: version
29
+ run: |
30
+ TAG="${{ github.ref_name }}"
31
+ # Strip leading 'v' if present
32
+ VERSION="${TAG#v}"
33
+ echo "tag=${TAG}" >> $GITHUB_OUTPUT
34
+ echo "version=${VERSION}" >> $GITHUB_OUTPUT
35
+ echo "Releasing version ${VERSION} from tag ${TAG}"
36
+
37
+ - name: Validate version matches tag
38
+ run: |
39
+ PKG_VERSION=$(python -c "import re; print(re.search(r'__version__ = \\\"([^\\\"]+)\\\"', open('tx_verify/__init__.py').read()).group(1))")
40
+ if [ "$PKG_VERSION" != "${{ steps.version.outputs.version }}" ]; then
41
+ echo "ERROR: pyproject.toml version ($PKG_VERSION) != tag version (${{ steps.version.outputs.version }})"
42
+ exit 1
43
+ fi
44
+ echo "Version check passed: $PKG_VERSION"
45
+
46
+ - name: Build
47
+ run: python -m build
48
+
49
+ - name: Publish to PyPI
50
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,44 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[co]
4
+ *.pyc
5
+ build/
6
+ dist/
7
+ wheels/
8
+ *.egg-info
9
+ *.egg
10
+
11
+ # Virtual environments
12
+ .venv
13
+ venv/
14
+ env/
15
+
16
+ # IDE
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ *~
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # Logs
28
+ logs/
29
+
30
+ # Environment
31
+ .env
32
+ .env.*
33
+ !.env.example
34
+
35
+ # Test
36
+ .pytest_cache/
37
+ .coverage
38
+ htmlcov/
39
+
40
+ # MyPy
41
+ .mypy_cache/
42
+
43
+ # Ruff
44
+ .ruff_cache/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,381 @@
1
+ Metadata-Version: 2.4
2
+ Name: tx-verify
3
+ Version: 0.1.0
4
+ Summary: Transaction Verification API — Python library for verifying Ethiopian payment transactions (CBE, Telebirr, Dashen, Abyssinia, CBE Birr, M-Pesa).
5
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/tx-verify
6
+ Project-URL: Repository, https://github.com/YOUR_USERNAME/tx-verify
7
+ Project-URL: Issues, https://github.com/YOUR_USERNAME/tx-verify/issues
8
+ Author: Nahom d
9
+ License: ISC
10
+ Keywords: abyssinia,cbe,cbe-birr,dashen,ethiopia,m-pesa,payment,telebirr,transaction,verification
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: aiosqlite>=0.20.0
22
+ Requires-Dist: alembic>=1.13.0
23
+ Requires-Dist: beautifulsoup4>=4.12.0
24
+ Requires-Dist: httpx[socks]>=0.27.0
25
+ Requires-Dist: mistralai>=1.0.0
26
+ Requires-Dist: pillow>=10.0.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: pypdf>=4.0.0
29
+ Requires-Dist: python-dotenv>=1.0.0
30
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: black>=24.0.0; extra == 'dev'
33
+ Requires-Dist: build>=1.2.0; extra == 'dev'
34
+ Requires-Dist: mypy>=1.10.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
36
+ Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
37
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
38
+ Requires-Dist: respx>=0.21.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.8.0; extra == 'dev'
40
+ Requires-Dist: twine>=5.0.0; extra == 'dev'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # tx-verify
44
+
45
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
46
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
47
+
48
+ > Python library for verifying Ethiopian payment transactions across multiple
49
+ > providers: **CBE**, **Telebirr**, **Dashen Bank**, **Bank of Abyssinia**,
50
+ > **CBE Birr**, and **M-Pesa**.
51
+
52
+ Each verifier fetches the official receipt from the provider (PDF or HTML),
53
+ parses it, and returns typed result objects. No headless browser is bundled —
54
+ PDFs are parsed with `pypdf` and HTML with `BeautifulSoup` — so it runs
55
+ anywhere Python does.
56
+
57
+ ---
58
+
59
+ ## Supported Providers
60
+
61
+ | Provider | Function | Input (example) |
62
+ | ------------------ | -------------------- | --------------------------------------- |
63
+ | CBE | `verify_cbe()` | `reference="FT…"`, `account_suffix="…"` |
64
+ | Telebirr | `verify_telebirr()` | `reference="CE12345678"` |
65
+ | Dashen Bank | `verify_dashen()` | `transaction_reference="123…"` (16 dig) |
66
+ | Bank of Abyssinia | `verify_abyssinia()` | `reference="FT…"`, `suffix="…"` (5 dig) |
67
+ | CBE Birr | `verify_cbe_birr()` | `receipt="…"`, `phone="251…"` |
68
+ | M-Pesa | `verify_mpesa()` | `transaction_id="UE20VG1GS8"` |
69
+ | Image (Mistral AI) | `verify_image()` | `image_bytes`, auto-detects provider |
70
+ | Universal | `verify_universal()` | `reference` — auto-routes by format |
71
+
72
+ ---
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install tx-verify
78
+ ```
79
+
80
+ Or with `uv`:
81
+
82
+ ```bash
83
+ uv pip install tx-verify
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ ```python
91
+ import asyncio
92
+ from tx_verify import verify_telebirr, verify_cbe
93
+
94
+ async def main():
95
+ # --- Telebirr ---
96
+ receipt = await verify_telebirr("CE12345678")
97
+ if receipt:
98
+ print(receipt.payer_name, receipt.settled_amount)
99
+
100
+ # --- CBE ---
101
+ result = await verify_cbe("FT23062669JJ", account_suffix="12345678")
102
+ if result.success:
103
+ print(f"Paid {result.amount} ETB to {result.receiver}")
104
+
105
+ asyncio.run(main())
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Examples
111
+
112
+ See the [`examples/`](examples/) directory for a runnable example per
113
+ provider:
114
+
115
+ | File | What it shows |
116
+ | ------------------------------------------------- | ------------------------------------------------ |
117
+ | [`telebirr.py`](examples/telebirr.py) | Verify a Telebirr receipt by reference number |
118
+ | [`cbe.py`](examples/cbe.py) | Fetch and parse a CBE PDF receipt |
119
+ | [`cbe_birr.py`](examples/cbe_birr.py) | Verify a CBE Birr wallet transaction |
120
+ | [`dashen.py`](examples/dashen.py) | Verify a Dashen Bank receipt with retry logic |
121
+ | [`abyssinia.py`](examples/abyssinia.py) | Verify a Bank of Abyssinia transaction |
122
+ | [`mpesa.py`](examples/mpesa.py) | Verify an Ethiopian M-Pesa transaction |
123
+ | [`image.py`](examples/image.py) | Analyse a receipt image with Mistral Vision AI |
124
+ | [`universal.py`](examples/universal.py) | Let the library auto-route to the right provider |
125
+ | [`error_handling.py`](examples/error_handling.py) | Catch provider-specific errors gracefully |
126
+
127
+ ---
128
+
129
+ ## Provider Reference
130
+
131
+ ### CBE — Commercial Bank of Ethiopia
132
+
133
+ CBE references are **12 characters** starting with `FT`. You must supply the
134
+ last **8 digits** of the account number as a suffix. The bank returns a PDF
135
+ that is fetched and parsed automatically.
136
+
137
+ ```python
138
+ from tx_verify import verify_cbe
139
+
140
+ result = await verify_cbe("FT23062669JJ", "12345678")
141
+ # result.success → bool
142
+ # result.payer → str | None
143
+ # result.receiver → str | None
144
+ # result.amount → float | None
145
+ # result.date → datetime | None
146
+ # result.reference → str | None
147
+ # result.reason → str | None
148
+ # result.error → str | None
149
+ ```
150
+
151
+ ### Telebirr
152
+
153
+ Telebirr references are **10-character alphanumeric** codes. The library scrapes
154
+ the public Ethio Telecom receipt page. It tries the primary source first,
155
+ then any fallback proxies configured via the `FALLBACK_PROXIES` environment
156
+ variable.
157
+
158
+ ```python
159
+ from tx_verify import verify_telebirr
160
+
161
+ receipt = await verify_telebirr("CE12345678")
162
+ # receipt.payer_name, receipt.settled_amount, receipt.total_paid_amount, …
163
+ ```
164
+
165
+ ### Dashen Bank
166
+
167
+ Dashen references are **16-digit numbers** starting with 3 digits (e.g.
168
+ `1234567890123456`). The verifier fetches a PDF with built-in retry logic
169
+ (up to 5 attempts).
170
+
171
+ ```python
172
+ from tx_verify import verify_dashen
173
+
174
+ result = await verify_dashen("1234567890123456")
175
+ # result.sender_name, result.transaction_amount, result.total, …
176
+ ```
177
+
178
+ ### Bank of Abyssinia
179
+
180
+ Abyssinia references are also **12 characters** starting with `FT`, but the
181
+ suffix is the last **5 digits** of the account number. The bank returns JSON
182
+ rather than a PDF.
183
+
184
+ ```python
185
+ from tx_verify import verify_abyssinia
186
+
187
+ result = await verify_abyssinia("FT23062669JJ", "90172")
188
+ # result.payer, result.amount, result.date, …
189
+ ```
190
+
191
+ ### CBE Birr
192
+
193
+ CBE Birr receipts are **10-character alphanumeric** codes. You also need the
194
+ wallet phone number in international format (`251…`).
195
+
196
+ ```python
197
+ from tx_verify import verify_cbe_birr
198
+
199
+ result = await verify_cbe_birr("AB1234CD56", "251911234567")
200
+ # result.customer_name, result.amount, result.paid_amount, …
201
+ ```
202
+
203
+ ### M-Pesa
204
+
205
+ M-Pesa references are **10-character alphanumeric** codes. The verifier hits
206
+ the Safaricom primary API first, then falls back to a proxy if configured via
207
+ `MPESA_PROXY_KEY`.
208
+
209
+ ```python
210
+ from tx_verify import verify_mpesa
211
+
212
+ result = await verify_mpesa("UE20VG1GS8")
213
+ # result.payer_name, result.amount, result.service_fee, result.vat, …
214
+ ```
215
+
216
+ ### Image verification (Mistral Vision)
217
+
218
+ Upload a receipt image (JPEG/PNG) and Mistral Vision AI will detect whether it
219
+ is a CBE or Telebirr receipt, extract the reference, and optionally verify it
220
+ automatically.
221
+
222
+ ```python
223
+ from tx_verify import verify_image
224
+
225
+ with open("receipt.jpg", "rb") as f:
226
+ image_bytes = f.read()
227
+
228
+ # Detect only
229
+ info = await verify_image(image_bytes, auto_verify=False)
230
+ print(info.type, info.reference, info.forward_to)
231
+
232
+ # Auto-verify (account_suffix required for CBE)
233
+ info = await verify_image(
234
+ image_bytes,
235
+ auto_verify=True,
236
+ account_suffix="12345678",
237
+ )
238
+ print(info.verified, info.details)
239
+ ```
240
+
241
+ > Requires `MISTRAL_API_KEY` environment variable and the `mistralai` package
242
+ > (installed automatically).
243
+
244
+ ### Universal — auto-route by reference format
245
+
246
+ Hand any reference to `verify_universal()` and it routes to the correct provider
247
+ based on length and prefix:
248
+
249
+ | Reference format | Routed to |
250
+ | -------------------------------------------- | ----------------- |
251
+ | 16 digits starting with `3` | Dashen Bank |
252
+ | 12 chars starting with `FT` + 8-digit suffix | CBE |
253
+ | 12 chars starting with `FT` + 5-digit suffix | Bank of Abyssinia |
254
+ | 10 chars + `phone_number` | CBE Birr |
255
+ | 10 chars (no phone) | Telebirr |
256
+
257
+ ```python
258
+ from tx_verify import verify_universal
259
+
260
+ result = await verify_universal("CE12345678")
261
+ print(result.success, result.data, result.error)
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Proxy Support
267
+
268
+ All receipt verifiers accept an explicit ``proxies`` argument. **Environment
269
+ variables are never read automatically** — you must pass the proxy yourself.
270
+
271
+ Supported schemes:
272
+
273
+ | Scheme | Description |
274
+ | -------- | ---------------------------------------------- |
275
+ | `http` | Plain HTTP forward proxy |
276
+ | `https` | HTTPS proxy (CONNECT tunnel) |
277
+ | `socks4` | SOCKS4 proxy |
278
+ | `socks5` | SOCKS5 proxy (client resolves DNS) |
279
+ | `socks5h`| SOCKS5 proxy (proxy resolves DNS) |
280
+
281
+ Authentication is embedded in the URL:
282
+
283
+ ```python
284
+ # Single global proxy
285
+ proxies = "http://user:pass@proxy.example.com:8080"
286
+
287
+ # Per-scheme mapping
288
+ proxies = {
289
+ "http://": "http://proxy.example.com:8080",
290
+ "https://": "socks5://localhost:1080",
291
+ }
292
+ ```
293
+
294
+ Pass it to any verifier:
295
+
296
+ ```python
297
+ from tx_verify import verify_telebirr, verify_cbe, verify_mpesa
298
+
299
+ # Telebirr through an HTTP proxy
300
+ receipt = await verify_telebirr("CE12345678", proxies="http://proxy:8080")
301
+
302
+ # CBE through SOCKS5
303
+ result = await verify_cbe("FT23062669JJ", "12345678", proxies="socks5://127.0.0.1:1080")
304
+
305
+ # M-Pesa with per-scheme mapping
306
+ result = await verify_mpesa("UE20VG1GS8", proxies={
307
+ "http://": "http://proxy:8080",
308
+ "https://": "socks5h://proxy:1080",
309
+ })
310
+ ```
311
+
312
+ `verify_universal` and `verify_image` also forward ``proxies`` to the
313
+ underlying provider automatically.
314
+
315
+ > **SOCKS tip:** `socks5h://` tells the proxy server to resolve hostnames,
316
+ > which is useful when the client cannot reach DNS directly.
317
+
318
+ ---
319
+
320
+ ## Error Handling
321
+
322
+ All verifiers return **result objects** rather than raising for expected
323
+ failures (network errors, missing receipts, parsing failures). Inspect
324
+ `result.success` and `result.error`.
325
+
326
+ Telebirr may raise `TelebirrVerificationError` when a proxy returns an explicit
327
+ error message. Catch it if you want to show the user a friendly message:
328
+
329
+ ```python
330
+ from tx_verify import TelebirrVerificationError, verify_telebirr
331
+
332
+ try:
333
+ receipt = await verify_telebirr("INVALID_REF")
334
+ except TelebirrVerificationError as exc:
335
+ print(f"Telebirr error: {exc}")
336
+ if exc.details:
337
+ print(f"Details: {exc.details}")
338
+ ```
339
+
340
+ The library also provides a generic error handler for wrapping database or
341
+ internal errors:
342
+
343
+ ```python
344
+ from tx_verify.utils.error_handler import AppError, ErrorType
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Environment Variables
350
+
351
+ | Variable | Purpose |
352
+ | --------------------------- | -------------------------------------------------------- |
353
+ | `FALLBACK_PROXIES` | Comma-separated proxy URLs for Telebirr |
354
+ | `TELEBIRR_PROXY_KEY` | API key for Telebirr proxy endpoints |
355
+ | `SKIP_PRIMARY_VERIFICATION` | Set to `true` to skip primary source (Telebirr / M-Pesa) |
356
+ | `MPESA_PROXY_KEY` | API key for M-Pesa fallback proxy |
357
+ | `MISTRAL_API_KEY` | Required for `verify_image()` |
358
+ | `LOG_LEVEL` | `DEBUG` or `INFO` (default `INFO`) |
359
+
360
+ ---
361
+
362
+ ## Development
363
+
364
+ ```bash
365
+ # Clone
366
+ git clone https://github.com/YOUR_USERNAME/tx-verify.git
367
+ cd tx-verify
368
+
369
+ # Install with dev dependencies
370
+ pip install -e ".[dev]"
371
+
372
+ # Lint & format
373
+ ruff check .
374
+ ruff format .
375
+
376
+ # Type-check
377
+ mypy tx_verify/
378
+
379
+ # Run tests
380
+ pytest
381
+ ```