bila 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.
bila-0.1.0/.gitignore ADDED
@@ -0,0 +1,47 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
42
+ .env.local
43
+ .env*.local
44
+
45
+ # Playwright MCP — local screenshots & dev logs
46
+ /.playwright-mcp/
47
+ /bila-*.png
bila-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: bila
3
+ Version: 0.1.0
4
+ Summary: Official Bila Python SDK — Stripe-style payment aggregator for Somalia.
5
+ Project-URL: Homepage, https://bila.so
6
+ Project-URL: Documentation, https://bila.so/docs/sdks
7
+ Project-URL: Source, https://github.com/Saakuut/bila
8
+ Author-email: Tabsamo Group <info@tabsamo.com>
9
+ License: MIT
10
+ Keywords: bila,edahab,payments,somalia,stripe,waafipay
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Office/Business :: Financial
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: requests>=2.31
24
+ Description-Content-Type: text/markdown
25
+
26
+ # bila — Python SDK
27
+
28
+ Official Python SDK for [Bila](https://bila.so), the Stripe-style payment aggregator for Somalia.
29
+
30
+ ```bash
31
+ pip install bila
32
+ ```
33
+
34
+ ```python
35
+ from bila import Bila
36
+
37
+ bila = Bila(api_key="sk_live_...")
38
+
39
+ charge = bila.charges.create(
40
+ amount=10000,
41
+ currency="USD",
42
+ rail="auto",
43
+ identifier="+252611234567",
44
+ description="Order #1234",
45
+ )
46
+
47
+ if charge["status"] == "requires_action":
48
+ redirect_url = charge["requires_action_payload"]["redirect_to"]
49
+ # send the payer to redirect_url
50
+ ```
51
+
52
+ Webhook verification:
53
+
54
+ ```python
55
+ from bila import verify_webhook_signature
56
+
57
+ signature = request.headers["Bila-Signature"]
58
+ body = request.get_data().decode()
59
+ if not verify_webhook_signature(body, signature, secret="whsec_..."):
60
+ return "invalid signature", 400
61
+ ```
62
+
63
+ Full docs at <https://bila.so/docs/sdks>.
bila-0.1.0/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # bila — Python SDK
2
+
3
+ Official Python SDK for [Bila](https://bila.so), the Stripe-style payment aggregator for Somalia.
4
+
5
+ ```bash
6
+ pip install bila
7
+ ```
8
+
9
+ ```python
10
+ from bila import Bila
11
+
12
+ bila = Bila(api_key="sk_live_...")
13
+
14
+ charge = bila.charges.create(
15
+ amount=10000,
16
+ currency="USD",
17
+ rail="auto",
18
+ identifier="+252611234567",
19
+ description="Order #1234",
20
+ )
21
+
22
+ if charge["status"] == "requires_action":
23
+ redirect_url = charge["requires_action_payload"]["redirect_to"]
24
+ # send the payer to redirect_url
25
+ ```
26
+
27
+ Webhook verification:
28
+
29
+ ```python
30
+ from bila import verify_webhook_signature
31
+
32
+ signature = request.headers["Bila-Signature"]
33
+ body = request.get_data().decode()
34
+ if not verify_webhook_signature(body, signature, secret="whsec_..."):
35
+ return "invalid signature", 400
36
+ ```
37
+
38
+ Full docs at <https://bila.so/docs/sdks>.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bila"
7
+ version = "0.1.0"
8
+ description = "Official Bila Python SDK — Stripe-style payment aggregator for Somalia."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "Tabsamo Group", email = "info@tabsamo.com" }]
12
+ requires-python = ">=3.9"
13
+ keywords = ["bila", "payments", "somalia", "waafipay", "edahab", "stripe"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Office/Business :: Financial",
25
+ "Topic :: Software Development :: Libraries",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.31",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://bila.so"
33
+ Documentation = "https://bila.so/docs/sdks"
34
+ Source = "https://github.com/Saakuut/bila"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/bila"]
38
+
39
+ [tool.hatch.build.targets.sdist]
40
+ include = ["src/bila", "README.md", "pyproject.toml"]
@@ -0,0 +1,229 @@
1
+ """Official Bila Python SDK.
2
+
3
+ Stripe-style: one client, resources hanging off the client, every method
4
+ returns a dict with the API response shape.
5
+
6
+ Usage:
7
+ from bila import Bila
8
+ bila = Bila(api_key="sk_live_...")
9
+ charge = bila.charges.create(amount=10000, currency="USD", rail="auto",
10
+ identifier="+252611234567")
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ import hashlib
16
+ import hmac
17
+ import json
18
+ import time
19
+ import uuid
20
+ from typing import Any, Dict, Optional
21
+
22
+ import requests
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = ["Bila", "BilaError", "verify_webhook_signature"]
26
+
27
+ DEFAULT_BASE_URL = "https://api.bila.so"
28
+ DEFAULT_VERSION = "2026-05-17"
29
+ DEFAULT_TIMEOUT = 60
30
+
31
+
32
+ class BilaError(Exception):
33
+ """Raised when the Bila API returns a non-2xx response."""
34
+
35
+ def __init__(self, status: int, body: Dict[str, Any]):
36
+ super().__init__(body.get("message") or f"Bila {status}")
37
+ self.status = status
38
+ self.type = body.get("type")
39
+ self.code = body.get("code")
40
+ self.param = body.get("param")
41
+ self.request_id = body.get("request_id")
42
+
43
+
44
+ class Bila:
45
+ def __init__(
46
+ self,
47
+ api_key: str,
48
+ *,
49
+ base_url: str = DEFAULT_BASE_URL,
50
+ bila_version: str = DEFAULT_VERSION,
51
+ timeout: int = DEFAULT_TIMEOUT,
52
+ ) -> None:
53
+ if not api_key:
54
+ raise ValueError("Bila: api_key is required.")
55
+ self._api_key = api_key
56
+ self._base_url = base_url.rstrip("/")
57
+ self._bila_version = bila_version
58
+ self._timeout = timeout
59
+ self._session = requests.Session()
60
+
61
+ # Resources
62
+ self.charges = _Charges(self)
63
+ self.refunds = _Refunds(self)
64
+ self.customers = _Customers(self)
65
+ self.payment_methods = _PaymentMethods(self)
66
+ self.webhook_endpoints = _WebhookEndpoints(self)
67
+ self.balance = _Balance(self)
68
+ self.events = _Events(self)
69
+
70
+ def request(
71
+ self,
72
+ method: str,
73
+ path: str,
74
+ json_body: Optional[Dict[str, Any]] = None,
75
+ *,
76
+ idempotency_key: Optional[str] = None,
77
+ bila_version: Optional[str] = None,
78
+ request_id: Optional[str] = None,
79
+ ) -> Dict[str, Any]:
80
+ url = f"{self._base_url}{path}"
81
+ basic = base64.b64encode(f"{self._api_key}:".encode()).decode()
82
+ headers = {
83
+ "Authorization": f"Basic {basic}",
84
+ "Bila-Version": bila_version or self._bila_version,
85
+ "Accept": "application/json",
86
+ }
87
+ if json_body is not None:
88
+ headers["Content-Type"] = "application/json"
89
+ if method.upper() not in ("GET", "HEAD"):
90
+ headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4())
91
+ if request_id:
92
+ headers["X-Request-Id"] = request_id
93
+
94
+ res = self._session.request(
95
+ method,
96
+ url,
97
+ data=json.dumps(json_body) if json_body is not None else None,
98
+ headers=headers,
99
+ timeout=self._timeout,
100
+ )
101
+ try:
102
+ data = res.json()
103
+ except ValueError:
104
+ data = {}
105
+ if not res.ok:
106
+ err = (data or {}).get("error") or {"type": "api_error", "message": f"HTTP {res.status_code}"}
107
+ raise BilaError(res.status_code, err)
108
+ return data
109
+
110
+
111
+ class _Resource:
112
+ def __init__(self, client: Bila) -> None:
113
+ self._client = client
114
+
115
+
116
+ def _qs(params: Dict[str, Any]) -> str:
117
+ items = [(k, str(v)) for k, v in params.items() if v is not None]
118
+ if not items:
119
+ return ""
120
+ from urllib.parse import urlencode
121
+ return "?" + urlencode(items)
122
+
123
+
124
+ class _Charges(_Resource):
125
+ def create(self, *, amount: int, currency: str, rail: str, **kw: Any) -> Dict[str, Any]:
126
+ body = {"amount": amount, "currency": currency, "rail": rail, **kw}
127
+ opts = {k: body.pop(k) for k in ("idempotency_key", "bila_version", "request_id") if k in body}
128
+ return self._client.request("POST", "/v1/charges", body, **opts)
129
+
130
+ def retrieve(self, charge_id: str, **opts: Any) -> Dict[str, Any]:
131
+ return self._client.request("GET", f"/v1/charges/{charge_id}", **opts)
132
+
133
+ def list(self, *, limit: Optional[int] = None, starting_after: Optional[str] = None, **opts: Any) -> Dict[str, Any]:
134
+ q = _qs({"limit": limit, "starting_after": starting_after})
135
+ return self._client.request("GET", f"/v1/charges{q}", **opts)
136
+
137
+ def cancel(self, charge_id: str, **opts: Any) -> Dict[str, Any]:
138
+ return self._client.request("POST", f"/v1/charges/{charge_id}/cancel", {}, **opts)
139
+
140
+ def confirm(self, charge_id: str, **opts: Any) -> Dict[str, Any]:
141
+ return self._client.request("POST", f"/v1/charges/{charge_id}/confirm", {}, **opts)
142
+
143
+
144
+ class _Refunds(_Resource):
145
+ def create(self, *, charge: str, amount: Optional[int] = None, reason: Optional[str] = None, **opts: Any) -> Dict[str, Any]:
146
+ body: Dict[str, Any] = {"charge": charge}
147
+ if amount is not None:
148
+ body["amount"] = amount
149
+ if reason is not None:
150
+ body["reason"] = reason
151
+ return self._client.request("POST", "/v1/refunds", body, **opts)
152
+
153
+ def retrieve(self, refund_id: str, **opts: Any) -> Dict[str, Any]:
154
+ return self._client.request("GET", f"/v1/refunds/{refund_id}", **opts)
155
+
156
+ def list(self, *, limit: Optional[int] = None, starting_after: Optional[str] = None, charge: Optional[str] = None, **opts: Any) -> Dict[str, Any]:
157
+ q = _qs({"limit": limit, "starting_after": starting_after, "charge": charge})
158
+ return self._client.request("GET", f"/v1/refunds{q}", **opts)
159
+
160
+
161
+ class _Customers(_Resource):
162
+ def create(self, **params: Any) -> Dict[str, Any]:
163
+ opts = {k: params.pop(k) for k in ("idempotency_key", "bila_version", "request_id") if k in params}
164
+ return self._client.request("POST", "/v1/customers", params, **opts)
165
+
166
+ def retrieve(self, customer_id: str, **opts: Any) -> Dict[str, Any]:
167
+ return self._client.request("GET", f"/v1/customers/{customer_id}", **opts)
168
+
169
+
170
+ class _PaymentMethods(_Resource):
171
+ def create(self, *, rail: str, kind: str, **params: Any) -> Dict[str, Any]:
172
+ body = {"rail": rail, "kind": kind, **params}
173
+ opts = {k: body.pop(k) for k in ("idempotency_key", "bila_version", "request_id") if k in body}
174
+ return self._client.request("POST", "/v1/payment_methods", body, **opts)
175
+
176
+ def retrieve(self, pm_id: str, **opts: Any) -> Dict[str, Any]:
177
+ return self._client.request("GET", f"/v1/payment_methods/{pm_id}", **opts)
178
+
179
+
180
+ class _WebhookEndpoints(_Resource):
181
+ def create(self, *, url: str, **params: Any) -> Dict[str, Any]:
182
+ body = {"url": url, **params}
183
+ opts = {k: body.pop(k) for k in ("idempotency_key", "bila_version", "request_id") if k in body}
184
+ return self._client.request("POST", "/v1/webhook_endpoints", body, **opts)
185
+
186
+ def retrieve(self, endpoint_id: str, **opts: Any) -> Dict[str, Any]:
187
+ return self._client.request("GET", f"/v1/webhook_endpoints/{endpoint_id}", **opts)
188
+
189
+ def list(self, **opts: Any) -> Dict[str, Any]:
190
+ return self._client.request("GET", "/v1/webhook_endpoints", **opts)
191
+
192
+
193
+ class _Balance(_Resource):
194
+ def retrieve(self, **opts: Any) -> Dict[str, Any]:
195
+ return self._client.request("GET", "/v1/balance", **opts)
196
+
197
+
198
+ class _Events(_Resource):
199
+ def list(self, *, limit: Optional[int] = None, starting_after: Optional[str] = None, type: Optional[str] = None, **opts: Any) -> Dict[str, Any]:
200
+ q = _qs({"limit": limit, "starting_after": starting_after, "type": type})
201
+ return self._client.request("GET", f"/v1/events{q}", **opts)
202
+
203
+
204
+ def verify_webhook_signature(
205
+ body: str,
206
+ header: str,
207
+ secret: str,
208
+ tolerance_seconds: int = 300,
209
+ ) -> bool:
210
+ """Verify a Bila-Signature header against the raw request body.
211
+
212
+ Header format: 't=<unix-timestamp>,v1=<hex-hmac-sha256>'
213
+ """
214
+ parts: Dict[str, str] = {}
215
+ for piece in header.split(","):
216
+ if "=" in piece:
217
+ k, v = piece.split("=", 1)
218
+ parts[k.strip()] = v.strip()
219
+ try:
220
+ t = int(parts.get("t", ""))
221
+ except ValueError:
222
+ return False
223
+ sig = parts.get("v1", "")
224
+ if not sig:
225
+ return False
226
+ if abs(int(time.time()) - t) > tolerance_seconds:
227
+ return False
228
+ expected = hmac.new(secret.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
229
+ return hmac.compare_digest(expected, sig)