paygent 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.
- paygent-0.1.0/.gitignore +42 -0
- paygent-0.1.0/CONTRIBUTING.md +427 -0
- paygent-0.1.0/LICENSE +21 -0
- paygent-0.1.0/PKG-INFO +440 -0
- paygent-0.1.0/README.md +400 -0
- paygent-0.1.0/pyproject.toml +58 -0
- paygent-0.1.0/src/paygent/__init__.py +52 -0
- paygent-0.1.0/src/paygent/api_client.py +423 -0
- paygent-0.1.0/src/paygent/context.py +209 -0
- paygent-0.1.0/src/paygent/core.py +1834 -0
- paygent-0.1.0/src/paygent/event_queue.py +251 -0
- paygent-0.1.0/src/paygent/guardrails.py +396 -0
- paygent-0.1.0/src/paygent/instrumentor.py +149 -0
- paygent-0.1.0/src/paygent/integrations/__init__.py +29 -0
- paygent-0.1.0/src/paygent/integrations/crewai.py +192 -0
- paygent-0.1.0/src/paygent/integrations/langchain.py +292 -0
- paygent-0.1.0/src/paygent/local_cache.py +343 -0
- paygent-0.1.0/src/paygent/metering.py +369 -0
- paygent-0.1.0/src/paygent/models.py +333 -0
- paygent-0.1.0/src/paygent/patcher.py +812 -0
- paygent-0.1.0/src/paygent/providers.py +278 -0
- paygent-0.1.0/src/paygent/storage/__init__.py +0 -0
- paygent-0.1.0/src/paygent/storage/base.py +55 -0
- paygent-0.1.0/src/paygent/storage/sqlite.py +522 -0
- paygent-0.1.0/src/paygent/stream_wrapper.py +307 -0
- paygent-0.1.0/tests/__init__.py +0 -0
- paygent-0.1.0/tests/conftest.py +79 -0
- paygent-0.1.0/tests/test_aggressive.py +1555 -0
- paygent-0.1.0/tests/test_api_client.py +728 -0
- paygent-0.1.0/tests/test_callbacks.py +489 -0
- paygent-0.1.0/tests/test_context.py +257 -0
- paygent-0.1.0/tests/test_core.py +1423 -0
- paygent-0.1.0/tests/test_core_backend_wiring.py +357 -0
- paygent-0.1.0/tests/test_crewai_integration.py +373 -0
- paygent-0.1.0/tests/test_event_queue.py +341 -0
- paygent-0.1.0/tests/test_guardrails.py +818 -0
- paygent-0.1.0/tests/test_instrumentor.py +353 -0
- paygent-0.1.0/tests/test_langchain_integration.py +422 -0
- paygent-0.1.0/tests/test_local_cache.py +732 -0
- paygent-0.1.0/tests/test_metering.py +716 -0
- paygent-0.1.0/tests/test_models.py +257 -0
- paygent-0.1.0/tests/test_multi_call.py +844 -0
- paygent-0.1.0/tests/test_patcher.py +866 -0
- paygent-0.1.0/tests/test_providers.py +276 -0
- paygent-0.1.0/tests/test_refresh_merge.py +408 -0
- paygent-0.1.0/tests/test_reservations.py +602 -0
- paygent-0.1.0/tests/test_sqlite_storage.py +532 -0
- paygent-0.1.0/tests/test_stream_wrapper.py +326 -0
- paygent-0.1.0/tests/test_wrap.py +628 -0
paygent-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
venv/
|
|
12
|
+
.venv/
|
|
13
|
+
|
|
14
|
+
# Environment variables
|
|
15
|
+
.env
|
|
16
|
+
.env.local
|
|
17
|
+
.env.test
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.sw?
|
|
23
|
+
*.suo
|
|
24
|
+
*.ntvs*
|
|
25
|
+
*.njsproj
|
|
26
|
+
*.sln
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
Thumbs.db
|
|
31
|
+
|
|
32
|
+
# Testing
|
|
33
|
+
.pytest_cache/
|
|
34
|
+
.coverage
|
|
35
|
+
htmlcov/
|
|
36
|
+
|
|
37
|
+
# SQLite
|
|
38
|
+
*.sqlite
|
|
39
|
+
*.db
|
|
40
|
+
|
|
41
|
+
# Logs
|
|
42
|
+
*.log
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# Contributing to Paygent
|
|
2
|
+
|
|
3
|
+
This doc is for people working on the Paygent SDK itself — setup, architecture,
|
|
4
|
+
running the full stack, tests, and publishing releases. For end-user docs see
|
|
5
|
+
the [README](./README.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Repository layout](#repository-layout)
|
|
12
|
+
- [Dev environment setup](#dev-environment-setup)
|
|
13
|
+
- [Running the tests](#running-the-tests)
|
|
14
|
+
- [Running the backend locally](#running-the-backend-locally)
|
|
15
|
+
- [Architecture](#architecture)
|
|
16
|
+
- [Call path](#call-path)
|
|
17
|
+
- [Two-phase reservation (concurrency safety)](#two-phase-reservation-concurrency-safety)
|
|
18
|
+
- [Event queue and background sync](#event-queue-and-background-sync)
|
|
19
|
+
- [Local storage (SQLite)](#local-storage-sqlite)
|
|
20
|
+
- [Schema](#schema)
|
|
21
|
+
- [Debugging with SQLite](#debugging-with-sqlite)
|
|
22
|
+
- [Coding standards](#coding-standards)
|
|
23
|
+
- [Known gotchas](#known-gotchas)
|
|
24
|
+
- [Release process](#release-process)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Repository layout
|
|
29
|
+
|
|
30
|
+
Paygent lives in one monorepo with three workspaces:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
code/
|
|
34
|
+
├── sdk/ # The Python SDK (published to PyPI)
|
|
35
|
+
│ ├── src/paygent/ # SDK source
|
|
36
|
+
│ ├── tests/ # SDK unit tests
|
|
37
|
+
│ └── pyproject.toml
|
|
38
|
+
├── backend/ # FastAPI backend (not published)
|
|
39
|
+
│ ├── src/paygent_api/
|
|
40
|
+
│ ├── tests/
|
|
41
|
+
│ ├── alembic/ # DB migrations
|
|
42
|
+
│ └── docker-compose.yml
|
|
43
|
+
└── tests/e2e/ # End-to-end tests that exercise SDK ↔ backend
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Dev environment setup
|
|
47
|
+
|
|
48
|
+
### Prerequisites
|
|
49
|
+
|
|
50
|
+
- Python 3.10+
|
|
51
|
+
- Docker (for PostgreSQL when working on the backend or running e2e tests)
|
|
52
|
+
- `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` if you want to run real-LLM e2e tests
|
|
53
|
+
|
|
54
|
+
### Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/paygent/paygent.git
|
|
58
|
+
cd paygent
|
|
59
|
+
|
|
60
|
+
# SDK dev install
|
|
61
|
+
cd sdk
|
|
62
|
+
python3 -m venv venv
|
|
63
|
+
venv/bin/pip install -e '.[dev]'
|
|
64
|
+
|
|
65
|
+
# Backend dev install (separate venv; the two codebases are independent)
|
|
66
|
+
cd ../backend
|
|
67
|
+
python3 -m venv venv
|
|
68
|
+
venv/bin/pip install -e '.[dev]'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Optional framework extras
|
|
72
|
+
|
|
73
|
+
Install these only if you need to run the LangChain/LangGraph/CrewAI test
|
|
74
|
+
paths locally:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sdk/venv/bin/pip install langchain-openai langchain-anthropic langgraph crewai
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Running the tests
|
|
81
|
+
|
|
82
|
+
The project has three test suites. Each has its own runner.
|
|
83
|
+
|
|
84
|
+
### SDK unit tests (no backend, no real LLM)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
cd sdk
|
|
88
|
+
venv/bin/python -m pytest tests/ -q
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Expect ~500 tests in under 10 seconds.
|
|
92
|
+
|
|
93
|
+
### Backend unit tests (no SDK, no real LLM, needs PostgreSQL)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd backend
|
|
97
|
+
docker compose up -d # start PostgreSQL
|
|
98
|
+
venv/bin/python -m pytest tests/ -q
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### End-to-end tests (full stack — backend + real LLM calls)
|
|
102
|
+
|
|
103
|
+
The e2e suite has its own runner that spins up a dedicated test backend on
|
|
104
|
+
port 8001 against the `paygent_test` database:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
./tests/e2e/run_e2e.sh # everything
|
|
108
|
+
./tests/e2e/run_e2e.sh tests/e2e/test_phase3_langchain.py
|
|
109
|
+
./tests/e2e/run_e2e.sh -k "hard_gate"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [`tests/README.md`](../tests/README.md) for the full e2e reference —
|
|
113
|
+
phases, fixtures, markers, env vars.
|
|
114
|
+
|
|
115
|
+
## Running the backend locally
|
|
116
|
+
|
|
117
|
+
If you're working on the SDK and want to point it at a local backend:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Start PostgreSQL
|
|
121
|
+
cd backend
|
|
122
|
+
docker compose up -d
|
|
123
|
+
|
|
124
|
+
# Run migrations
|
|
125
|
+
PYTHONPATH=src venv/bin/python -m alembic upgrade head
|
|
126
|
+
|
|
127
|
+
# Start the server on port 8000 (default)
|
|
128
|
+
PYTHONPATH=src venv/bin/uvicorn paygent_api.main:app --reload
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Health check: `curl http://localhost:8000/api/v1/health`
|
|
132
|
+
|
|
133
|
+
PgAdmin is available at [http://localhost:8080](http://localhost:8080) via
|
|
134
|
+
docker-compose.
|
|
135
|
+
|
|
136
|
+
## Architecture
|
|
137
|
+
|
|
138
|
+
### Call path
|
|
139
|
+
|
|
140
|
+
Every metered LLM call (via auto-instrumentation or `pg.wrap()`) goes
|
|
141
|
+
through the same four-step pipeline:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
User sends prompt
|
|
145
|
+
|
|
|
146
|
+
Paygent SDK intercepts LLM call
|
|
147
|
+
|
|
|
148
|
+
Guard check + reservation (under per-user lock, microseconds)
|
|
149
|
+
| |
|
|
150
|
+
| +--> hard_gate? raise PaygentLimitExceeded
|
|
151
|
+
| +--> soft_gate? fire callback, continue
|
|
152
|
+
| +--> ok? reserve estimated cost
|
|
153
|
+
|
|
|
154
|
+
Original LLM call executes (lock released, concurrent calls interleave)
|
|
155
|
+
|
|
|
156
|
+
Metering: extract tokens, finalize reservation (under lock, microseconds)
|
|
157
|
+
|
|
|
158
|
+
Event pushed to background queue (non-blocking)
|
|
159
|
+
|
|
|
160
|
+
Response returned IMMEDIATELY
|
|
161
|
+
|
|
162
|
+
[Background thread] Events batched and synced to the Paygent backend
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The per-user lock is held only for the two short windows around the LLM
|
|
166
|
+
call — never across the network I/O. Concurrent calls for the same user
|
|
167
|
+
run their HTTP round-trips in parallel.
|
|
168
|
+
|
|
169
|
+
### Two-phase reservation (concurrency safety)
|
|
170
|
+
|
|
171
|
+
Without a reservation, two concurrent calls at the boundary of a cap can
|
|
172
|
+
both pass the guard (both see the same stale `period_cost`) and both
|
|
173
|
+
execute — doubling the overrun.
|
|
174
|
+
|
|
175
|
+
The reservation pattern fixes this:
|
|
176
|
+
|
|
177
|
+
1. **Reserve** — after the guard passes, tentatively add the estimated
|
|
178
|
+
cost to a separate `reserved_cost` field. Subsequent concurrent guard
|
|
179
|
+
checks see `period_cost + reserved_cost` and can block.
|
|
180
|
+
2. **Execute** — lock released, LLM call runs.
|
|
181
|
+
3. **Finalize** — atomic swap under the lock: subtract the reservation,
|
|
182
|
+
add the actual cost from the response. Net recorded spend = actual.
|
|
183
|
+
|
|
184
|
+
The `reservation_safety_factor` (default 1.2) absorbs estimation drift.
|
|
185
|
+
A 20% multiplier on the reservation hold means 20% of bonus headroom is
|
|
186
|
+
held during the await — prevents small token-counting errors from
|
|
187
|
+
overshooting. Actual spend is never inflated.
|
|
188
|
+
|
|
189
|
+
Errors roll back the reservation:
|
|
190
|
+
- LLM call raises → `release_reservation` → re-raise
|
|
191
|
+
- Token extraction fails → `release_reservation` → swallow (fail-open)
|
|
192
|
+
- Event creation fails → `release_reservation` → swallow
|
|
193
|
+
|
|
194
|
+
### Event queue and background sync
|
|
195
|
+
|
|
196
|
+
A single `threading.Thread` (daemon) runs the flush loop:
|
|
197
|
+
|
|
198
|
+
- Every `flush_interval` seconds (default 5s), drain the in-memory queue
|
|
199
|
+
and push to the backend in batches.
|
|
200
|
+
- Every `sync_pending_interval` (default 30s), retry previously-unsynced
|
|
201
|
+
events stored in SQLite.
|
|
202
|
+
- Every `refresh_interval` (default 60s), fetch fresh user state from
|
|
203
|
+
the backend for every cached user.
|
|
204
|
+
|
|
205
|
+
If the backend is unreachable, events stay in SQLite marked `synced=0`.
|
|
206
|
+
They retry on the next `sync_pending` cycle when the backend returns.
|
|
207
|
+
|
|
208
|
+
## Local storage (SQLite)
|
|
209
|
+
|
|
210
|
+
Paygent maintains a local SQLite database at `~/.paygent/local.db` by
|
|
211
|
+
default (configurable via `db_path`). This database serves two purposes:
|
|
212
|
+
|
|
213
|
+
1. **Offline fallback** — events are written locally before the backend
|
|
214
|
+
sync attempt. If the backend is down, they queue here and sync on
|
|
215
|
+
reconnect.
|
|
216
|
+
2. **Local-only mode** — when no API key is provided, this is the sole
|
|
217
|
+
storage for usage data.
|
|
218
|
+
|
|
219
|
+
### Schema
|
|
220
|
+
|
|
221
|
+
```sql
|
|
222
|
+
-- Every metered event
|
|
223
|
+
CREATE TABLE usage_events (
|
|
224
|
+
id TEXT PRIMARY KEY, -- UUID (idempotency key)
|
|
225
|
+
user_id TEXT NOT NULL,
|
|
226
|
+
session_id TEXT, -- Optional
|
|
227
|
+
timestamp TEXT NOT NULL, -- ISO 8601
|
|
228
|
+
model TEXT, -- e.g. "gpt-4o"
|
|
229
|
+
input_tokens INTEGER DEFAULT 0,
|
|
230
|
+
output_tokens INTEGER DEFAULT 0,
|
|
231
|
+
total_tokens INTEGER DEFAULT 0,
|
|
232
|
+
tool_calls TEXT DEFAULT '[]', -- JSON array
|
|
233
|
+
cost_tokens REAL DEFAULT 0,
|
|
234
|
+
cost_tools REAL DEFAULT 0,
|
|
235
|
+
cost_total REAL DEFAULT 0,
|
|
236
|
+
metadata TEXT DEFAULT '{}', -- JSON blob
|
|
237
|
+
synced INTEGER DEFAULT 0, -- 0 = pending, 1 = synced
|
|
238
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE INDEX idx_events_user ON usage_events(user_id, timestamp);
|
|
242
|
+
CREATE INDEX idx_events_synced ON usage_events(synced);
|
|
243
|
+
|
|
244
|
+
-- Per-user cached state for offline recovery
|
|
245
|
+
CREATE TABLE user_snapshots (
|
|
246
|
+
user_id TEXT PRIMARY KEY,
|
|
247
|
+
plan TEXT NOT NULL,
|
|
248
|
+
plan_config TEXT NOT NULL, -- JSON (PlanConfig)
|
|
249
|
+
current_usage TEXT NOT NULL, -- JSON (CurrentUsage — Pydantic model_dump_json)
|
|
250
|
+
billing_period_start TEXT, -- ISO 8601 or NULL
|
|
251
|
+
billing_period_end TEXT,
|
|
252
|
+
snapshot_at TEXT DEFAULT (datetime('now'))
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The `current_usage` JSON carries `period_cost`, `session_cost`, per-model
|
|
257
|
+
breakdowns, and the in-flight `reserved_cost` / `reserved_tokens_by_model`
|
|
258
|
+
fields. `plan_config` uses the sentinel `"__INF__"` for `float('inf')`
|
|
259
|
+
limits (stdlib JSON can't encode infinity).
|
|
260
|
+
|
|
261
|
+
### Debugging with SQLite
|
|
262
|
+
|
|
263
|
+
Inspect the database directly for debugging:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
sqlite3 ~/.paygent/local.db
|
|
267
|
+
|
|
268
|
+
-- Total events + sync status
|
|
269
|
+
SELECT synced, COUNT(*) FROM usage_events GROUP BY synced;
|
|
270
|
+
|
|
271
|
+
-- Recent events for a user
|
|
272
|
+
SELECT id, model, total_tokens, cost_total, synced, created_at
|
|
273
|
+
FROM usage_events WHERE user_id = 'user_123'
|
|
274
|
+
ORDER BY timestamp DESC LIMIT 10;
|
|
275
|
+
|
|
276
|
+
-- Cost breakdown by model
|
|
277
|
+
SELECT model, SUM(total_tokens) AS tokens, ROUND(SUM(cost_total), 4) AS cost
|
|
278
|
+
FROM usage_events WHERE user_id = 'user_123' GROUP BY model;
|
|
279
|
+
|
|
280
|
+
-- Stuck unsynced events
|
|
281
|
+
SELECT COUNT(*) FROM usage_events WHERE synced = 0;
|
|
282
|
+
|
|
283
|
+
-- Snapshot for a user
|
|
284
|
+
SELECT user_id, plan, current_usage FROM user_snapshots WHERE user_id = 'user_123';
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Coding standards
|
|
288
|
+
|
|
289
|
+
- **Python 3.10+.** Use the newer union syntax (`str | None`, not
|
|
290
|
+
`Optional[str]`) everywhere.
|
|
291
|
+
- **Pydantic v2** for all data models. Use `PrivateAttr` for fields
|
|
292
|
+
that must not serialize (e.g. locks).
|
|
293
|
+
- **Logging**: use `logging.getLogger("paygent")` for SDK code,
|
|
294
|
+
`"paygent_api"` for backend code. No `print` statements.
|
|
295
|
+
- **Fail-open on every path that intercepts an LLM call.** Every try
|
|
296
|
+
block in `patcher.py`, `wrap()`, and `awrap()` should either return
|
|
297
|
+
the original response or re-raise `PaygentLimitExceeded` — never
|
|
298
|
+
propagate internal errors into the dev's call stack.
|
|
299
|
+
- **Every test verifies both the happy path AND the fail-open path**
|
|
300
|
+
when applicable.
|
|
301
|
+
- **UUIDs for event IDs** (`uuid4()`). These are idempotency keys on
|
|
302
|
+
the backend — duplicates are silently ignored.
|
|
303
|
+
- **Docstrings on every public method.**
|
|
304
|
+
- **pytest + pytest-asyncio** for testing. Mock external services in
|
|
305
|
+
unit tests; no real API calls below the e2e tier.
|
|
306
|
+
|
|
307
|
+
## Known gotchas
|
|
308
|
+
|
|
309
|
+
### Shutdown hang on some test files
|
|
310
|
+
|
|
311
|
+
A few SDK test files (`test_wrap.py`, `test_callbacks.py`,
|
|
312
|
+
`test_multi_call.py`, `test_sqlite_storage.py`) run to completion but
|
|
313
|
+
pytest hangs on interpreter shutdown, waiting for a background daemon
|
|
314
|
+
thread to join. If you see "N passed" and then the process stalls, the
|
|
315
|
+
tests passed — just `Ctrl+C`. Pre-existing issue; tracked as tech debt.
|
|
316
|
+
|
|
317
|
+
### Multi-process drift
|
|
318
|
+
|
|
319
|
+
The in-memory cache is per-process. Multi-worker deployments (Gunicorn
|
|
320
|
+
with `workers > 1`, multi-replica Kubernetes) drift between refreshes.
|
|
321
|
+
See the [README "Known Limitations"](./README.md#known-limitations)
|
|
322
|
+
section for the math and mitigations.
|
|
323
|
+
|
|
324
|
+
### `datetime.utcnow()` deprecation
|
|
325
|
+
|
|
326
|
+
The codebase uses `datetime.utcnow()` in several places. Python 3.12+
|
|
327
|
+
deprecates this in favor of `datetime.now(timezone.utc)`. Currently
|
|
328
|
+
fine on Python 3.10/3.11. Revisit when we bump the minimum supported
|
|
329
|
+
Python version.
|
|
330
|
+
|
|
331
|
+
### Thread safety of `_sessions` dict
|
|
332
|
+
|
|
333
|
+
The cache dict is protected from concurrent iteration via snapshotting
|
|
334
|
+
(`list(self._sessions.items())`). Per-user state is now protected by
|
|
335
|
+
the per-user lock introduced with the reservation pattern. The
|
|
336
|
+
remaining gap is pure dict mutation concurrency — single-op mutations
|
|
337
|
+
are safe under the GIL, but compound check-then-set patterns would need
|
|
338
|
+
`_user_locks_lock` (used only for lazy lock creation). Not a production
|
|
339
|
+
risk today.
|
|
340
|
+
|
|
341
|
+
## Release process
|
|
342
|
+
|
|
343
|
+
Paygent is published to PyPI as the `paygent` package.
|
|
344
|
+
|
|
345
|
+
### Before release
|
|
346
|
+
|
|
347
|
+
1. Run the full test suite and make sure it's green:
|
|
348
|
+
```bash
|
|
349
|
+
cd sdk && venv/bin/python -m pytest tests/ -q
|
|
350
|
+
cd ../backend && venv/bin/python -m pytest tests/ -q
|
|
351
|
+
./tests/e2e/run_e2e.sh
|
|
352
|
+
```
|
|
353
|
+
2. Bump `version` in `sdk/pyproject.toml` (semantic versioning).
|
|
354
|
+
3. Update any version references in docs.
|
|
355
|
+
4. Commit + tag: `git tag v<version> && git push --tags`.
|
|
356
|
+
|
|
357
|
+
### Publishing to TestPyPI first (recommended)
|
|
358
|
+
|
|
359
|
+
TestPyPI is a sandbox with the same API as PyPI. Always verify the
|
|
360
|
+
package installs and imports correctly before pushing to real PyPI —
|
|
361
|
+
once a version is published to real PyPI it cannot be re-uploaded
|
|
362
|
+
(even after yanking).
|
|
363
|
+
|
|
364
|
+
1. Create accounts on [TestPyPI](https://test.pypi.org) and [PyPI](https://pypi.org).
|
|
365
|
+
2. Generate API tokens on each, stored in `~/.pypirc`:
|
|
366
|
+
```ini
|
|
367
|
+
[distutils]
|
|
368
|
+
index-servers = pypi testpypi
|
|
369
|
+
|
|
370
|
+
[pypi]
|
|
371
|
+
username = __token__
|
|
372
|
+
password = pypi-...
|
|
373
|
+
|
|
374
|
+
[testpypi]
|
|
375
|
+
repository = https://test.pypi.org/legacy/
|
|
376
|
+
username = __token__
|
|
377
|
+
password = pypi-...
|
|
378
|
+
```
|
|
379
|
+
3. Install build tools:
|
|
380
|
+
```bash
|
|
381
|
+
cd sdk
|
|
382
|
+
venv/bin/pip install build twine
|
|
383
|
+
```
|
|
384
|
+
4. Build:
|
|
385
|
+
```bash
|
|
386
|
+
rm -rf dist/
|
|
387
|
+
venv/bin/python -m build
|
|
388
|
+
```
|
|
389
|
+
5. Verify the artifacts:
|
|
390
|
+
```bash
|
|
391
|
+
venv/bin/twine check dist/*
|
|
392
|
+
unzip -l dist/paygent-*.whl # spot-check the wheel contents
|
|
393
|
+
```
|
|
394
|
+
6. Upload to TestPyPI:
|
|
395
|
+
```bash
|
|
396
|
+
venv/bin/twine upload --repository testpypi dist/*
|
|
397
|
+
```
|
|
398
|
+
7. Test-install into a clean venv:
|
|
399
|
+
```bash
|
|
400
|
+
python -m venv /tmp/test-paygent
|
|
401
|
+
/tmp/test-paygent/bin/pip install \
|
|
402
|
+
--index-url https://test.pypi.org/simple/ \
|
|
403
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
404
|
+
paygent
|
|
405
|
+
/tmp/test-paygent/bin/python -c "
|
|
406
|
+
from paygent import Paygent, paygent_context, PlanConfig
|
|
407
|
+
pg = Paygent.init()
|
|
408
|
+
print('OK:', pg.is_initialized)
|
|
409
|
+
pg.shutdown()
|
|
410
|
+
"
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Publishing to PyPI
|
|
414
|
+
|
|
415
|
+
Once the TestPyPI smoke passes:
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
cd sdk
|
|
419
|
+
venv/bin/twine upload dist/*
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Post-publish
|
|
423
|
+
|
|
424
|
+
- Visit `https://pypi.org/project/paygent/` — verify the README renders
|
|
425
|
+
correctly.
|
|
426
|
+
- `pip install paygent` into a fresh venv and run a quick smoke.
|
|
427
|
+
- Announce the release (changelog, blog, etc.).
|
paygent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paygent
|
|
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.
|