lipay-sdk 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.
- lipay_sdk-1.0.0/LICENSE +22 -0
- lipay_sdk-1.0.0/PKG-INFO +230 -0
- lipay_sdk-1.0.0/README.md +192 -0
- lipay_sdk-1.0.0/lipay_sdk/__init__.py +5 -0
- lipay_sdk-1.0.0/lipay_sdk/csw_session_guard.py +196 -0
- lipay_sdk-1.0.0/lipay_sdk.egg-info/PKG-INFO +230 -0
- lipay_sdk-1.0.0/lipay_sdk.egg-info/SOURCES.txt +11 -0
- lipay_sdk-1.0.0/lipay_sdk.egg-info/dependency_links.txt +1 -0
- lipay_sdk-1.0.0/lipay_sdk.egg-info/requires.txt +11 -0
- lipay_sdk-1.0.0/lipay_sdk.egg-info/top_level.txt +1 -0
- lipay_sdk-1.0.0/pyproject.toml +49 -0
- lipay_sdk-1.0.0/setup.cfg +4 -0
- lipay_sdk-1.0.0/tests/test_csw_session_guard.py +105 -0
lipay_sdk-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lipay
|
|
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
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
lipay_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lipay-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Lipay Messaging Platform
|
|
5
|
+
Author-email: Evans Musamia <evans@lipay.store>
|
|
6
|
+
Maintainer-email: Lipay Platform <dev@lipay.store>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://message.lipay.store
|
|
9
|
+
Project-URL: Documentation, https://github.com/Evans-musamia/LipayMessagingPlatform#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/Evans-musamia/LipayMessagingPlatform
|
|
11
|
+
Project-URL: Issues, https://github.com/Evans-musamia/LipayMessagingPlatform/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/Evans-musamia/LipayMessagingPlatform/releases
|
|
13
|
+
Keywords: lipay,whatsapp,messaging,africa,customer-service-window,api-sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Environment :: Web Environment
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Communications :: Telephony
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: httpx>=0.27.0
|
|
29
|
+
Requires-Dist: pydantic>=2.9.0
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.115.0; extra == "fastapi"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Lipay Python SDK
|
|
40
|
+
|
|
41
|
+
Official Python client for [Lipay](https://message.lipay.store) — the developer platform for WhatsApp messaging, routing, and Customer Service Window (CSW) management across Africa.
|
|
42
|
+
|
|
43
|
+
Use this library in your application (BrandFlow, Teacher, custom backends) to check whether the Meta **24-hour customer service window** is open before sending free-text WhatsApp messages.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
**From PyPI (recommended):**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install lipay-sdk
|
|
53
|
+
pip install "lipay-sdk[fastapi]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pin in `requirements.txt`:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
lipay-sdk==1.0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**From Git (tagged release):**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**TestPyPI (maintainers — sandbox):**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ lipay-sdk
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Editable (local development):**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/Evans-musamia/LipayMessagingPlatform.git
|
|
78
|
+
cd LipayMessagingPlatform
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Publishing guide: [PYPI.md](PYPI.md)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
|
|
91
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def main() -> None:
|
|
95
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
96
|
+
|
|
97
|
+
active = await guard.is_window_active(
|
|
98
|
+
customer_phone="+254700000000",
|
|
99
|
+
business_phone="+254711111111",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if active:
|
|
103
|
+
print("CSW open — safe to send free-text via Lipay /v1/whatsapp/send")
|
|
104
|
+
else:
|
|
105
|
+
print("CSW closed — send an approved template first")
|
|
106
|
+
|
|
107
|
+
await guard.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
asyncio.run(main())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Configuration object (Pydantic)
|
|
115
|
+
|
|
116
|
+
For explicit control over timeouts, cache TTL, and API paths:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from lipay_sdk.csw_session_guard import LipayCswSessionGuard, LipaySdkConfig
|
|
120
|
+
|
|
121
|
+
config = LipaySdkConfig(
|
|
122
|
+
gateway_url="https://message.lipay.store",
|
|
123
|
+
local_active_ttl_seconds=180,
|
|
124
|
+
http_timeout_seconds=10.0,
|
|
125
|
+
)
|
|
126
|
+
guard = LipayCswSessionGuard(config)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Full status payload
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
status = await guard.get_session_status(
|
|
133
|
+
customer_phone="254700000000",
|
|
134
|
+
business_phone="254711111111",
|
|
135
|
+
)
|
|
136
|
+
print(status.is_communication_window_active, status.window_expires_at)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Caching behavior
|
|
142
|
+
|
|
143
|
+
The SDK implements an **in-process memory guard** in front of Lipay's Redis-backed `GET /api/v1/switchboard/session-status` endpoint.
|
|
144
|
+
|
|
145
|
+
| Event | Behavior |
|
|
146
|
+
|--------|----------|
|
|
147
|
+
| Lipay returns `active=true` | Response cached in your app process RAM for **180 seconds** (configurable via `LipaySdkConfig.local_active_ttl_seconds`) |
|
|
148
|
+
| Repeat lookup within TTL | Served from local memory — **no HTTP call** to Lipay |
|
|
149
|
+
| Lipay returns `active=false` | **Never cached** — every lookup hits Lipay |
|
|
150
|
+
| Local TTL expires | Cache entry dropped; next lookup syncs from Lipay |
|
|
151
|
+
|
|
152
|
+
This mirrors how Twilio-style SDKs reduce read amplification: your container avoids redundant session-status traffic while the gateway remains the source of truth on Redis.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## FastAPI integration
|
|
157
|
+
|
|
158
|
+
Install the optional extra:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install "lipay-sdk[fastapi] @ git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Wire the guard on application startup:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from contextlib import asynccontextmanager
|
|
168
|
+
|
|
169
|
+
import httpx
|
|
170
|
+
from fastapi import Depends, FastAPI, Request
|
|
171
|
+
|
|
172
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
173
|
+
from lipay_sdk.csw_session_guard import LipaySdkConfig
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def lifespan(app: FastAPI):
|
|
178
|
+
http = httpx.AsyncClient(timeout=10.0)
|
|
179
|
+
config = LipaySdkConfig(gateway_url="https://message.lipay.store")
|
|
180
|
+
app.state.lipay_guard = LipayCswSessionGuard(config, http_client=http)
|
|
181
|
+
yield
|
|
182
|
+
await app.state.lipay_guard.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
app = FastAPI(lifespan=lifespan)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_guard(request: Request) -> LipayCswSessionGuard:
|
|
189
|
+
return request.app.state.lipay_guard
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.get("/check-window")
|
|
193
|
+
async def check_window(
|
|
194
|
+
customer_phone: str,
|
|
195
|
+
business_phone: str,
|
|
196
|
+
guard: LipayCswSessionGuard = Depends(get_guard),
|
|
197
|
+
):
|
|
198
|
+
return await guard.get_session_status(
|
|
199
|
+
customer_phone=customer_phone,
|
|
200
|
+
business_phone=business_phone,
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## API reference
|
|
207
|
+
|
|
208
|
+
### `LipayCswSessionGuard`
|
|
209
|
+
|
|
210
|
+
| Method | Description |
|
|
211
|
+
|--------|-------------|
|
|
212
|
+
| `is_window_active(customer_phone, business_phone)` | `bool` — CSW open for pair |
|
|
213
|
+
| `get_session_status(...)` | `SessionStatus` — full gateway payload |
|
|
214
|
+
| `fetch_from_lipay(...)` | Always calls Lipay (bypasses local cache) |
|
|
215
|
+
| `close()` | Close owned `httpx` client |
|
|
216
|
+
|
|
217
|
+
### `LipaySdkConfig` (Pydantic)
|
|
218
|
+
|
|
219
|
+
| Field | Default | Description |
|
|
220
|
+
|-------|---------|-------------|
|
|
221
|
+
| `gateway_url` | required | Lipay gateway base URL |
|
|
222
|
+
| `local_active_ttl_seconds` | `180` | Local RAM cache TTL |
|
|
223
|
+
| `http_timeout_seconds` | `5.0` | HTTP client timeout |
|
|
224
|
+
| `session_status_path` | `/api/v1/switchboard/session-status` | Status endpoint path |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Lipay Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for [Lipay](https://message.lipay.store) — the developer platform for WhatsApp messaging, routing, and Customer Service Window (CSW) management across Africa.
|
|
4
|
+
|
|
5
|
+
Use this library in your application (BrandFlow, Teacher, custom backends) to check whether the Meta **24-hour customer service window** is open before sending free-text WhatsApp messages.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
**From PyPI (recommended):**
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install lipay-sdk
|
|
15
|
+
pip install "lipay-sdk[fastapi]"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Pin in `requirements.txt`:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
lipay-sdk==1.0.0
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**From Git (tagged release):**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**TestPyPI (maintainers — sandbox):**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ lipay-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Editable (local development):**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/Evans-musamia/LipayMessagingPlatform.git
|
|
40
|
+
cd LipayMessagingPlatform
|
|
41
|
+
pip install -e ".[dev]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Publishing guide: [PYPI.md](PYPI.md)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def main() -> None:
|
|
57
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
58
|
+
|
|
59
|
+
active = await guard.is_window_active(
|
|
60
|
+
customer_phone="+254700000000",
|
|
61
|
+
business_phone="+254711111111",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if active:
|
|
65
|
+
print("CSW open — safe to send free-text via Lipay /v1/whatsapp/send")
|
|
66
|
+
else:
|
|
67
|
+
print("CSW closed — send an approved template first")
|
|
68
|
+
|
|
69
|
+
await guard.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Configuration object (Pydantic)
|
|
77
|
+
|
|
78
|
+
For explicit control over timeouts, cache TTL, and API paths:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from lipay_sdk.csw_session_guard import LipayCswSessionGuard, LipaySdkConfig
|
|
82
|
+
|
|
83
|
+
config = LipaySdkConfig(
|
|
84
|
+
gateway_url="https://message.lipay.store",
|
|
85
|
+
local_active_ttl_seconds=180,
|
|
86
|
+
http_timeout_seconds=10.0,
|
|
87
|
+
)
|
|
88
|
+
guard = LipayCswSessionGuard(config)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Full status payload
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
status = await guard.get_session_status(
|
|
95
|
+
customer_phone="254700000000",
|
|
96
|
+
business_phone="254711111111",
|
|
97
|
+
)
|
|
98
|
+
print(status.is_communication_window_active, status.window_expires_at)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Caching behavior
|
|
104
|
+
|
|
105
|
+
The SDK implements an **in-process memory guard** in front of Lipay's Redis-backed `GET /api/v1/switchboard/session-status` endpoint.
|
|
106
|
+
|
|
107
|
+
| Event | Behavior |
|
|
108
|
+
|--------|----------|
|
|
109
|
+
| Lipay returns `active=true` | Response cached in your app process RAM for **180 seconds** (configurable via `LipaySdkConfig.local_active_ttl_seconds`) |
|
|
110
|
+
| Repeat lookup within TTL | Served from local memory — **no HTTP call** to Lipay |
|
|
111
|
+
| Lipay returns `active=false` | **Never cached** — every lookup hits Lipay |
|
|
112
|
+
| Local TTL expires | Cache entry dropped; next lookup syncs from Lipay |
|
|
113
|
+
|
|
114
|
+
This mirrors how Twilio-style SDKs reduce read amplification: your container avoids redundant session-status traffic while the gateway remains the source of truth on Redis.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## FastAPI integration
|
|
119
|
+
|
|
120
|
+
Install the optional extra:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pip install "lipay-sdk[fastapi] @ git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Wire the guard on application startup:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from contextlib import asynccontextmanager
|
|
130
|
+
|
|
131
|
+
import httpx
|
|
132
|
+
from fastapi import Depends, FastAPI, Request
|
|
133
|
+
|
|
134
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
135
|
+
from lipay_sdk.csw_session_guard import LipaySdkConfig
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@asynccontextmanager
|
|
139
|
+
async def lifespan(app: FastAPI):
|
|
140
|
+
http = httpx.AsyncClient(timeout=10.0)
|
|
141
|
+
config = LipaySdkConfig(gateway_url="https://message.lipay.store")
|
|
142
|
+
app.state.lipay_guard = LipayCswSessionGuard(config, http_client=http)
|
|
143
|
+
yield
|
|
144
|
+
await app.state.lipay_guard.close()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
app = FastAPI(lifespan=lifespan)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_guard(request: Request) -> LipayCswSessionGuard:
|
|
151
|
+
return request.app.state.lipay_guard
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.get("/check-window")
|
|
155
|
+
async def check_window(
|
|
156
|
+
customer_phone: str,
|
|
157
|
+
business_phone: str,
|
|
158
|
+
guard: LipayCswSessionGuard = Depends(get_guard),
|
|
159
|
+
):
|
|
160
|
+
return await guard.get_session_status(
|
|
161
|
+
customer_phone=customer_phone,
|
|
162
|
+
business_phone=business_phone,
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## API reference
|
|
169
|
+
|
|
170
|
+
### `LipayCswSessionGuard`
|
|
171
|
+
|
|
172
|
+
| Method | Description |
|
|
173
|
+
|--------|-------------|
|
|
174
|
+
| `is_window_active(customer_phone, business_phone)` | `bool` — CSW open for pair |
|
|
175
|
+
| `get_session_status(...)` | `SessionStatus` — full gateway payload |
|
|
176
|
+
| `fetch_from_lipay(...)` | Always calls Lipay (bypasses local cache) |
|
|
177
|
+
| `close()` | Close owned `httpx` client |
|
|
178
|
+
|
|
179
|
+
### `LipaySdkConfig` (Pydantic)
|
|
180
|
+
|
|
181
|
+
| Field | Default | Description |
|
|
182
|
+
|-------|---------|-------------|
|
|
183
|
+
| `gateway_url` | required | Lipay gateway base URL |
|
|
184
|
+
| `local_active_ttl_seconds` | `180` | Local RAM cache TTL |
|
|
185
|
+
| `http_timeout_seconds` | `5.0` | HTTP client timeout |
|
|
186
|
+
| `session_status_path` | `/api/v1/switchboard/session-status` | Status endpoint path |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Customer Service Window (CSW) session guard for Lipay tenant applications.
|
|
3
|
+
|
|
4
|
+
Queries Lipay's Redis-backed session-status API with an in-process TTL cache
|
|
5
|
+
so active-window lookups avoid redundant network round-trips.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from pydantic import BaseModel, Field, field_validator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _normalize_phone(phone: str) -> str:
|
|
19
|
+
cleaned = phone.strip().replace(" ", "").replace("-", "")
|
|
20
|
+
if not cleaned:
|
|
21
|
+
return cleaned
|
|
22
|
+
if not cleaned.startswith("+"):
|
|
23
|
+
return f"+{cleaned.lstrip('+')}"
|
|
24
|
+
return cleaned
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LipaySdkConfig(BaseModel):
|
|
28
|
+
"""Runtime configuration for the Lipay HTTP client and local cache."""
|
|
29
|
+
|
|
30
|
+
gateway_url: str = Field(
|
|
31
|
+
...,
|
|
32
|
+
description="Lipay gateway base URL, e.g. https://message.lipay.store",
|
|
33
|
+
min_length=8,
|
|
34
|
+
)
|
|
35
|
+
local_active_ttl_seconds: int = Field(
|
|
36
|
+
default=180,
|
|
37
|
+
ge=0,
|
|
38
|
+
description="In-process cache TTL after a successful active=true response",
|
|
39
|
+
)
|
|
40
|
+
http_timeout_seconds: float = Field(default=5.0, gt=0)
|
|
41
|
+
session_status_path: str = Field(
|
|
42
|
+
default="/api/v1/switchboard/session-status",
|
|
43
|
+
description="Path appended to gateway_url for CSW lookups",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@field_validator("gateway_url", "session_status_path")
|
|
47
|
+
@classmethod
|
|
48
|
+
def strip_slashes(cls, value: str) -> str:
|
|
49
|
+
return value.strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SessionStatus(BaseModel):
|
|
53
|
+
"""Normalized response from Lipay session-status."""
|
|
54
|
+
|
|
55
|
+
customer_phone_number: str
|
|
56
|
+
business_phone_number: str
|
|
57
|
+
is_communication_window_active: bool
|
|
58
|
+
window_expires_at: str | None = None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_api_payload(cls, payload: dict[str, Any]) -> SessionStatus:
|
|
62
|
+
return cls.model_validate(payload)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LipayCswSessionGuard:
|
|
66
|
+
"""
|
|
67
|
+
In-process TTL cache + Lipay ``GET /api/v1/switchboard/session-status`` client.
|
|
68
|
+
|
|
69
|
+
Use before outbound free-text WhatsApp sends to confirm the 24-hour CSW is open.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
config: LipaySdkConfig | str,
|
|
75
|
+
*,
|
|
76
|
+
http_client: httpx.AsyncClient | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
if isinstance(config, str):
|
|
79
|
+
config = LipaySdkConfig(gateway_url=config)
|
|
80
|
+
self._config = config
|
|
81
|
+
self._base_url = config.gateway_url.rstrip("/")
|
|
82
|
+
self._status_path = (
|
|
83
|
+
config.session_status_path
|
|
84
|
+
if config.session_status_path.startswith("/")
|
|
85
|
+
else f"/{config.session_status_path}"
|
|
86
|
+
)
|
|
87
|
+
self._http = http_client
|
|
88
|
+
self._owns_client = http_client is None
|
|
89
|
+
self._local: dict[str, tuple[SessionStatus, float]] = {}
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def config(self) -> LipaySdkConfig:
|
|
93
|
+
return self._config
|
|
94
|
+
|
|
95
|
+
def _pair_key(self, business_phone: str, customer_phone: str) -> str:
|
|
96
|
+
return f"{_normalize_phone(business_phone)}:{_normalize_phone(customer_phone)}"
|
|
97
|
+
|
|
98
|
+
def _read_local(self, pair_key: str) -> SessionStatus | None:
|
|
99
|
+
entry = self._local.get(pair_key)
|
|
100
|
+
if not entry:
|
|
101
|
+
return None
|
|
102
|
+
payload, expires_at = entry
|
|
103
|
+
if time.monotonic() >= expires_at:
|
|
104
|
+
self._local.pop(pair_key, None)
|
|
105
|
+
return None
|
|
106
|
+
return payload
|
|
107
|
+
|
|
108
|
+
def _write_local_active(self, pair_key: str, status: SessionStatus) -> None:
|
|
109
|
+
ttl = self._config.local_active_ttl_seconds
|
|
110
|
+
self._local[pair_key] = (status, time.monotonic() + ttl)
|
|
111
|
+
|
|
112
|
+
def _clear_local(self, pair_key: str) -> None:
|
|
113
|
+
self._local.pop(pair_key, None)
|
|
114
|
+
|
|
115
|
+
async def _client(self) -> httpx.AsyncClient:
|
|
116
|
+
if self._http is None:
|
|
117
|
+
self._http = httpx.AsyncClient(timeout=self._config.http_timeout_seconds)
|
|
118
|
+
return self._http
|
|
119
|
+
|
|
120
|
+
async def close(self) -> None:
|
|
121
|
+
if self._owns_client and self._http is not None:
|
|
122
|
+
await self._http.aclose()
|
|
123
|
+
self._http = None
|
|
124
|
+
|
|
125
|
+
async def fetch_from_lipay(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
customer_phone: str,
|
|
129
|
+
business_phone: str,
|
|
130
|
+
) -> SessionStatus:
|
|
131
|
+
params = urlencode(
|
|
132
|
+
{
|
|
133
|
+
"customer_phone": customer_phone.lstrip("+"),
|
|
134
|
+
"business_phone": business_phone.lstrip("+"),
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
url = f"{self._base_url}{self._status_path}?{params}"
|
|
138
|
+
client = await self._client()
|
|
139
|
+
response = await client.get(url)
|
|
140
|
+
response.raise_for_status()
|
|
141
|
+
return SessionStatus.from_api_payload(response.json())
|
|
142
|
+
|
|
143
|
+
async def get_session_status(
|
|
144
|
+
self,
|
|
145
|
+
*,
|
|
146
|
+
customer_phone: str,
|
|
147
|
+
business_phone: str,
|
|
148
|
+
) -> SessionStatus:
|
|
149
|
+
"""Return CSW status — local memory first, then Lipay gateway."""
|
|
150
|
+
pair_key = self._pair_key(business_phone, customer_phone)
|
|
151
|
+
|
|
152
|
+
cached = self._read_local(pair_key)
|
|
153
|
+
if cached is not None:
|
|
154
|
+
return cached
|
|
155
|
+
|
|
156
|
+
status = await self.fetch_from_lipay(
|
|
157
|
+
customer_phone=customer_phone,
|
|
158
|
+
business_phone=business_phone,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if status.is_communication_window_active:
|
|
162
|
+
self._write_local_active(pair_key, status)
|
|
163
|
+
else:
|
|
164
|
+
self._clear_local(pair_key)
|
|
165
|
+
|
|
166
|
+
return status
|
|
167
|
+
|
|
168
|
+
async def is_window_active(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
customer_phone: str,
|
|
172
|
+
business_phone: str,
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""
|
|
175
|
+
Convenience API: ``True`` when the Meta 24-hour CSW is open for this pair.
|
|
176
|
+
|
|
177
|
+
Example::
|
|
178
|
+
|
|
179
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
180
|
+
if await guard.is_window_active(
|
|
181
|
+
customer_phone="+254700000000",
|
|
182
|
+
business_phone="+254711111111",
|
|
183
|
+
):
|
|
184
|
+
...
|
|
185
|
+
"""
|
|
186
|
+
status = await self.get_session_status(
|
|
187
|
+
customer_phone=customer_phone,
|
|
188
|
+
business_phone=business_phone,
|
|
189
|
+
)
|
|
190
|
+
return status.is_communication_window_active
|
|
191
|
+
|
|
192
|
+
def is_communication_window_active(self, status: SessionStatus | dict[str, Any]) -> bool:
|
|
193
|
+
"""Evaluate a status object returned by :meth:`get_session_status`."""
|
|
194
|
+
if isinstance(status, SessionStatus):
|
|
195
|
+
return status.is_communication_window_active
|
|
196
|
+
return bool(status.get("is_communication_window_active"))
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lipay-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Lipay Messaging Platform
|
|
5
|
+
Author-email: Evans Musamia <evans@lipay.store>
|
|
6
|
+
Maintainer-email: Lipay Platform <dev@lipay.store>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://message.lipay.store
|
|
9
|
+
Project-URL: Documentation, https://github.com/Evans-musamia/LipayMessagingPlatform#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/Evans-musamia/LipayMessagingPlatform
|
|
11
|
+
Project-URL: Issues, https://github.com/Evans-musamia/LipayMessagingPlatform/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/Evans-musamia/LipayMessagingPlatform/releases
|
|
13
|
+
Keywords: lipay,whatsapp,messaging,africa,customer-service-window,api-sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Environment :: Web Environment
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Communications :: Telephony
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: httpx>=0.27.0
|
|
29
|
+
Requires-Dist: pydantic>=2.9.0
|
|
30
|
+
Provides-Extra: fastapi
|
|
31
|
+
Requires-Dist: fastapi>=0.115.0; extra == "fastapi"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Lipay Python SDK
|
|
40
|
+
|
|
41
|
+
Official Python client for [Lipay](https://message.lipay.store) — the developer platform for WhatsApp messaging, routing, and Customer Service Window (CSW) management across Africa.
|
|
42
|
+
|
|
43
|
+
Use this library in your application (BrandFlow, Teacher, custom backends) to check whether the Meta **24-hour customer service window** is open before sending free-text WhatsApp messages.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
**From PyPI (recommended):**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install lipay-sdk
|
|
53
|
+
pip install "lipay-sdk[fastapi]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pin in `requirements.txt`:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
lipay-sdk==1.0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**From Git (tagged release):**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**TestPyPI (maintainers — sandbox):**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ lipay-sdk
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Editable (local development):**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/Evans-musamia/LipayMessagingPlatform.git
|
|
78
|
+
cd LipayMessagingPlatform
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Publishing guide: [PYPI.md](PYPI.md)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
|
|
91
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def main() -> None:
|
|
95
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
96
|
+
|
|
97
|
+
active = await guard.is_window_active(
|
|
98
|
+
customer_phone="+254700000000",
|
|
99
|
+
business_phone="+254711111111",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if active:
|
|
103
|
+
print("CSW open — safe to send free-text via Lipay /v1/whatsapp/send")
|
|
104
|
+
else:
|
|
105
|
+
print("CSW closed — send an approved template first")
|
|
106
|
+
|
|
107
|
+
await guard.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
asyncio.run(main())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Configuration object (Pydantic)
|
|
115
|
+
|
|
116
|
+
For explicit control over timeouts, cache TTL, and API paths:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from lipay_sdk.csw_session_guard import LipayCswSessionGuard, LipaySdkConfig
|
|
120
|
+
|
|
121
|
+
config = LipaySdkConfig(
|
|
122
|
+
gateway_url="https://message.lipay.store",
|
|
123
|
+
local_active_ttl_seconds=180,
|
|
124
|
+
http_timeout_seconds=10.0,
|
|
125
|
+
)
|
|
126
|
+
guard = LipayCswSessionGuard(config)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Full status payload
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
status = await guard.get_session_status(
|
|
133
|
+
customer_phone="254700000000",
|
|
134
|
+
business_phone="254711111111",
|
|
135
|
+
)
|
|
136
|
+
print(status.is_communication_window_active, status.window_expires_at)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Caching behavior
|
|
142
|
+
|
|
143
|
+
The SDK implements an **in-process memory guard** in front of Lipay's Redis-backed `GET /api/v1/switchboard/session-status` endpoint.
|
|
144
|
+
|
|
145
|
+
| Event | Behavior |
|
|
146
|
+
|--------|----------|
|
|
147
|
+
| Lipay returns `active=true` | Response cached in your app process RAM for **180 seconds** (configurable via `LipaySdkConfig.local_active_ttl_seconds`) |
|
|
148
|
+
| Repeat lookup within TTL | Served from local memory — **no HTTP call** to Lipay |
|
|
149
|
+
| Lipay returns `active=false` | **Never cached** — every lookup hits Lipay |
|
|
150
|
+
| Local TTL expires | Cache entry dropped; next lookup syncs from Lipay |
|
|
151
|
+
|
|
152
|
+
This mirrors how Twilio-style SDKs reduce read amplification: your container avoids redundant session-status traffic while the gateway remains the source of truth on Redis.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## FastAPI integration
|
|
157
|
+
|
|
158
|
+
Install the optional extra:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
pip install "lipay-sdk[fastapi] @ git+https://github.com/Evans-musamia/LipayMessagingPlatform.git@v1.0.0"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Wire the guard on application startup:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from contextlib import asynccontextmanager
|
|
168
|
+
|
|
169
|
+
import httpx
|
|
170
|
+
from fastapi import Depends, FastAPI, Request
|
|
171
|
+
|
|
172
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
173
|
+
from lipay_sdk.csw_session_guard import LipaySdkConfig
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@asynccontextmanager
|
|
177
|
+
async def lifespan(app: FastAPI):
|
|
178
|
+
http = httpx.AsyncClient(timeout=10.0)
|
|
179
|
+
config = LipaySdkConfig(gateway_url="https://message.lipay.store")
|
|
180
|
+
app.state.lipay_guard = LipayCswSessionGuard(config, http_client=http)
|
|
181
|
+
yield
|
|
182
|
+
await app.state.lipay_guard.close()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
app = FastAPI(lifespan=lifespan)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_guard(request: Request) -> LipayCswSessionGuard:
|
|
189
|
+
return request.app.state.lipay_guard
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.get("/check-window")
|
|
193
|
+
async def check_window(
|
|
194
|
+
customer_phone: str,
|
|
195
|
+
business_phone: str,
|
|
196
|
+
guard: LipayCswSessionGuard = Depends(get_guard),
|
|
197
|
+
):
|
|
198
|
+
return await guard.get_session_status(
|
|
199
|
+
customer_phone=customer_phone,
|
|
200
|
+
business_phone=business_phone,
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## API reference
|
|
207
|
+
|
|
208
|
+
### `LipayCswSessionGuard`
|
|
209
|
+
|
|
210
|
+
| Method | Description |
|
|
211
|
+
|--------|-------------|
|
|
212
|
+
| `is_window_active(customer_phone, business_phone)` | `bool` — CSW open for pair |
|
|
213
|
+
| `get_session_status(...)` | `SessionStatus` — full gateway payload |
|
|
214
|
+
| `fetch_from_lipay(...)` | Always calls Lipay (bypasses local cache) |
|
|
215
|
+
| `close()` | Close owned `httpx` client |
|
|
216
|
+
|
|
217
|
+
### `LipaySdkConfig` (Pydantic)
|
|
218
|
+
|
|
219
|
+
| Field | Default | Description |
|
|
220
|
+
|-------|---------|-------------|
|
|
221
|
+
| `gateway_url` | required | Lipay gateway base URL |
|
|
222
|
+
| `local_active_ttl_seconds` | `180` | Local RAM cache TTL |
|
|
223
|
+
| `http_timeout_seconds` | `5.0` | HTTP client timeout |
|
|
224
|
+
| `session_status_path` | `/api/v1/switchboard/session-status` | Status endpoint path |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
lipay_sdk/__init__.py
|
|
5
|
+
lipay_sdk/csw_session_guard.py
|
|
6
|
+
lipay_sdk.egg-info/PKG-INFO
|
|
7
|
+
lipay_sdk.egg-info/SOURCES.txt
|
|
8
|
+
lipay_sdk.egg-info/dependency_links.txt
|
|
9
|
+
lipay_sdk.egg-info/requires.txt
|
|
10
|
+
lipay_sdk.egg-info/top_level.txt
|
|
11
|
+
tests/test_csw_session_guard.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lipay_sdk
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lipay-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for Lipay Messaging Platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Evans Musamia", email = "evans@lipay.store" }]
|
|
13
|
+
maintainers = [{ name = "Lipay Platform", email = "dev@lipay.store" }]
|
|
14
|
+
keywords = ["lipay", "whatsapp", "messaging", "africa", "customer-service-window", "api-sdk"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Environment :: Web Environment",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Communications :: Telephony",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"httpx>=0.27.0",
|
|
30
|
+
"pydantic>=2.9.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
fastapi = ["fastapi>=0.115.0"]
|
|
35
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.24", "build>=1.0", "twine>=5.0"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://message.lipay.store"
|
|
39
|
+
Documentation = "https://github.com/Evans-musamia/LipayMessagingPlatform#readme"
|
|
40
|
+
Repository = "https://github.com/Evans-musamia/LipayMessagingPlatform"
|
|
41
|
+
Issues = "https://github.com/Evans-musamia/LipayMessagingPlatform/issues"
|
|
42
|
+
Changelog = "https://github.com/Evans-musamia/LipayMessagingPlatform/releases"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
include = ["lipay_sdk*"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Unit tests for lipay_sdk.csw_session_guard."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from lipay_sdk import LipayCswSessionGuard
|
|
9
|
+
from lipay_sdk.csw_session_guard import LipaySdkConfig, SessionStatus
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ACTIVE = SessionStatus(
|
|
13
|
+
customer_phone_number="+254700000000",
|
|
14
|
+
business_phone_number="+254711111111",
|
|
15
|
+
is_communication_window_active=True,
|
|
16
|
+
window_expires_at="2026-06-01T15:30:22Z",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
INACTIVE = SessionStatus(
|
|
20
|
+
customer_phone_number="+254700000000",
|
|
21
|
+
business_phone_number="+254711111111",
|
|
22
|
+
is_communication_window_active=False,
|
|
23
|
+
window_expires_at=None,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_config_from_string_gateway_url():
|
|
28
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
29
|
+
assert guard.config.gateway_url == "https://message.lipay.store"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_config_pydantic_overrides():
|
|
33
|
+
config = LipaySdkConfig(
|
|
34
|
+
gateway_url="https://message.lipay.store",
|
|
35
|
+
local_active_ttl_seconds=60,
|
|
36
|
+
session_status_path="/api/v1/switchboard/session-status",
|
|
37
|
+
)
|
|
38
|
+
guard = LipayCswSessionGuard(config)
|
|
39
|
+
assert guard.config.local_active_ttl_seconds == 60
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_is_window_active_true():
|
|
44
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
45
|
+
with patch.object(
|
|
46
|
+
guard, "fetch_from_lipay", new_callable=AsyncMock, return_value=ACTIVE
|
|
47
|
+
):
|
|
48
|
+
assert await guard.is_window_active(
|
|
49
|
+
customer_phone="254700000000",
|
|
50
|
+
business_phone="254711111111",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_local_cache_skips_second_fetch():
|
|
56
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
57
|
+
with patch.object(
|
|
58
|
+
guard, "fetch_from_lipay", new_callable=AsyncMock, return_value=ACTIVE
|
|
59
|
+
) as mock_fetch:
|
|
60
|
+
await guard.get_session_status(
|
|
61
|
+
customer_phone="254700000000",
|
|
62
|
+
business_phone="254711111111",
|
|
63
|
+
)
|
|
64
|
+
await guard.get_session_status(
|
|
65
|
+
customer_phone="254700000000",
|
|
66
|
+
business_phone="254711111111",
|
|
67
|
+
)
|
|
68
|
+
mock_fetch.assert_awaited_once()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_inactive_never_cached():
|
|
73
|
+
guard = LipayCswSessionGuard("https://message.lipay.store")
|
|
74
|
+
with patch.object(
|
|
75
|
+
guard, "fetch_from_lipay", new_callable=AsyncMock, return_value=INACTIVE
|
|
76
|
+
) as mock_fetch:
|
|
77
|
+
await guard.get_session_status(
|
|
78
|
+
customer_phone="254700000000",
|
|
79
|
+
business_phone="254711111111",
|
|
80
|
+
)
|
|
81
|
+
await guard.get_session_status(
|
|
82
|
+
customer_phone="254700000000",
|
|
83
|
+
business_phone="254711111111",
|
|
84
|
+
)
|
|
85
|
+
assert mock_fetch.await_count == 2
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_expired_local_cache_refetches():
|
|
90
|
+
config = LipaySdkConfig(
|
|
91
|
+
gateway_url="https://message.lipay.store",
|
|
92
|
+
local_active_ttl_seconds=1,
|
|
93
|
+
)
|
|
94
|
+
guard = LipayCswSessionGuard(config)
|
|
95
|
+
pair_key = guard._pair_key("254711111111", "254700000000")
|
|
96
|
+
guard._local[pair_key] = (ACTIVE, time.monotonic() - 1)
|
|
97
|
+
|
|
98
|
+
with patch.object(
|
|
99
|
+
guard, "fetch_from_lipay", new_callable=AsyncMock, return_value=ACTIVE
|
|
100
|
+
) as mock_fetch:
|
|
101
|
+
await guard.get_session_status(
|
|
102
|
+
customer_phone="254700000000",
|
|
103
|
+
business_phone="254711111111",
|
|
104
|
+
)
|
|
105
|
+
mock_fetch.assert_awaited_once()
|