opensolvency 0.1.1__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.
- opensolvency-0.1.1/.gitignore +15 -0
- opensolvency-0.1.1/PKG-INFO +33 -0
- opensolvency-0.1.1/README.md +23 -0
- opensolvency-0.1.1/opensolvency.py +92 -0
- opensolvency-0.1.1/pyproject.toml +17 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opensolvency
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python client for the OpenSolvency governance gate (REST over the HTTP ingress)
|
|
5
|
+
Author: General Liquidity
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: agentic-payments,governance,opensolvency
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# opensolvency (Python client)
|
|
12
|
+
|
|
13
|
+
A thin REST client for the [OpenSolvency](https://github.com/general-liquidity/opensolvency)
|
|
14
|
+
governance gate. Point it at a running `opensolvency serve`; every payment it submits
|
|
15
|
+
runs through the same gate (auto-execute inside a mandate, park for operator approval,
|
|
16
|
+
or block). A `blocked` outcome is a normal result, not an error. Standard library
|
|
17
|
+
only - no dependencies.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from opensolvency import OpenSolvencyClient
|
|
21
|
+
|
|
22
|
+
os = OpenSolvencyClient("http://127.0.0.1:8787", token="...")
|
|
23
|
+
res = os.pay(payee="tesco", payee_class="groceries", amount=8000, # minor-units
|
|
24
|
+
rationale="the weekly grocery shop")
|
|
25
|
+
print(res["outcome"]) # settled | pending | blocked | failed
|
|
26
|
+
print(os.status(), os.ready())
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Idempotency keys are generated per `pay()` (override with `idempotency_key=`). The
|
|
30
|
+
full-feature surface is the TypeScript SDK (`@general-liquidity/opensolvency`); this
|
|
31
|
+
is for Python hosts that talk to a running ingress.
|
|
32
|
+
|
|
33
|
+
License: MIT.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# opensolvency (Python client)
|
|
2
|
+
|
|
3
|
+
A thin REST client for the [OpenSolvency](https://github.com/general-liquidity/opensolvency)
|
|
4
|
+
governance gate. Point it at a running `opensolvency serve`; every payment it submits
|
|
5
|
+
runs through the same gate (auto-execute inside a mandate, park for operator approval,
|
|
6
|
+
or block). A `blocked` outcome is a normal result, not an error. Standard library
|
|
7
|
+
only - no dependencies.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from opensolvency import OpenSolvencyClient
|
|
11
|
+
|
|
12
|
+
os = OpenSolvencyClient("http://127.0.0.1:8787", token="...")
|
|
13
|
+
res = os.pay(payee="tesco", payee_class="groceries", amount=8000, # minor-units
|
|
14
|
+
rationale="the weekly grocery shop")
|
|
15
|
+
print(res["outcome"]) # settled | pending | blocked | failed
|
|
16
|
+
print(os.status(), os.ready())
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Idempotency keys are generated per `pay()` (override with `idempotency_key=`). The
|
|
20
|
+
full-feature surface is the TypeScript SDK (`@general-liquidity/opensolvency`); this
|
|
21
|
+
is for Python hosts that talk to a running ingress.
|
|
22
|
+
|
|
23
|
+
License: MIT.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""OpenSolvency Python client — a thin REST client over the HTTP ingress.
|
|
2
|
+
|
|
3
|
+
The ingress runs the SAME gate as everything else, so this client adds no
|
|
4
|
+
authority: a payment it submits is auto-executed inside a mandate, parked for
|
|
5
|
+
operator approval, or blocked. Standard-library only (no dependencies).
|
|
6
|
+
|
|
7
|
+
from opensolvency import OpenSolvencyClient
|
|
8
|
+
|
|
9
|
+
os = OpenSolvencyClient("http://127.0.0.1:8787", token="...")
|
|
10
|
+
result = os.pay(payee="tesco", payee_class="groceries", amount=8000,
|
|
11
|
+
rationale="the weekly grocery shop")
|
|
12
|
+
print(result["outcome"]) # "settled" | "pending" | "blocked" | "failed"
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
__all__ = ["OpenSolvencyClient", "OpenSolvencyError"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenSolvencyError(Exception):
|
|
27
|
+
"""Raised on a transport error (not on a gate 'blocked' — that's a normal result)."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OpenSolvencyClient:
|
|
31
|
+
def __init__(self, base_url: str = "http://127.0.0.1:8787", token: Optional[str] = None):
|
|
32
|
+
self.base_url = base_url.rstrip("/")
|
|
33
|
+
self.token = token
|
|
34
|
+
|
|
35
|
+
def _request(self, method: str, path: str, body: Any = None, headers: Optional[dict] = None):
|
|
36
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
37
|
+
h = {"content-type": "application/json"}
|
|
38
|
+
if self.token:
|
|
39
|
+
h["authorization"] = f"Bearer {self.token}"
|
|
40
|
+
if headers:
|
|
41
|
+
h.update(headers)
|
|
42
|
+
req = urllib.request.Request(self.base_url + path, data=data, method=method, headers=h)
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.urlopen(req) as resp:
|
|
45
|
+
payload = resp.read()
|
|
46
|
+
return resp.status, (json.loads(payload) if payload else None)
|
|
47
|
+
except urllib.error.HTTPError as e:
|
|
48
|
+
payload = e.read()
|
|
49
|
+
# 403/401/413/429 carry a JSON body too — return it, don't raise.
|
|
50
|
+
return e.code, (json.loads(payload) if payload else None)
|
|
51
|
+
except urllib.error.URLError as e:
|
|
52
|
+
raise OpenSolvencyError(f"transport error: {e}") from e
|
|
53
|
+
|
|
54
|
+
def pay(
|
|
55
|
+
self,
|
|
56
|
+
payee: str,
|
|
57
|
+
payee_class: str,
|
|
58
|
+
amount: int,
|
|
59
|
+
currency: str = "GBP",
|
|
60
|
+
rail: str = "card",
|
|
61
|
+
rationale: str = "",
|
|
62
|
+
idempotency_key: Optional[str] = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Submit a payment intent. Returns the gate result
|
|
65
|
+
{intentId, outcome, reasons, receiptId, verified}. `amount` is minor-units."""
|
|
66
|
+
headers = {"idempotency-key": idempotency_key or str(uuid.uuid4())}
|
|
67
|
+
_, body = self._request(
|
|
68
|
+
"POST",
|
|
69
|
+
"/payment-intent",
|
|
70
|
+
{
|
|
71
|
+
"payee": payee,
|
|
72
|
+
"payeeClass": payee_class,
|
|
73
|
+
"amount": amount,
|
|
74
|
+
"currency": currency,
|
|
75
|
+
"rail": rail,
|
|
76
|
+
"rationale": rationale,
|
|
77
|
+
},
|
|
78
|
+
headers,
|
|
79
|
+
)
|
|
80
|
+
return body or {}
|
|
81
|
+
|
|
82
|
+
def status(self) -> dict:
|
|
83
|
+
"""Kill-switch / circuit-breaker state."""
|
|
84
|
+
return self._request("GET", "/status")[1] or {}
|
|
85
|
+
|
|
86
|
+
def ready(self) -> dict:
|
|
87
|
+
"""Readiness probe."""
|
|
88
|
+
return self._request("GET", "/ready")[1] or {}
|
|
89
|
+
|
|
90
|
+
def openapi(self) -> dict:
|
|
91
|
+
"""The served OpenAPI 3.1 document."""
|
|
92
|
+
return self._request("GET", "/openapi.json")[1] or {}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "opensolvency"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Python client for the OpenSolvency governance gate (REST over the HTTP ingress)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "General Liquidity" }]
|
|
9
|
+
keywords = ["agentic-payments", "governance", "opensolvency"]
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
only-include = ["opensolvency.py"]
|