plystra 1.0.0.dev2__tar.gz → 1.0.0.dev10__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.
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/PKG-INFO +43 -6
- plystra-1.0.0.dev10/README.md +124 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/plystra/__init__.py +3 -1
- plystra-1.0.0.dev10/plystra/_utils.py +53 -0
- plystra-1.0.0.dev10/plystra/async_resources.py +408 -0
- plystra-1.0.0.dev10/plystra/client.py +268 -0
- plystra-1.0.0.dev10/plystra/exceptions.py +37 -0
- plystra-1.0.0.dev10/plystra/resources.py +403 -0
- plystra-1.0.0.dev10/plystra/types.py +7 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/pyproject.toml +46 -47
- plystra-1.0.0.dev10/tests/__init__.py +1 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/tests/test_client.py +34 -4
- plystra-1.0.0.dev2/README.md +0 -86
- plystra-1.0.0.dev2/plystra/client.py +0 -980
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/.gitattributes +0 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/.gitignore +0 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/LICENSE +0 -0
- {plystra-1.0.0.dev2 → plystra-1.0.0.dev10}/plystra/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plystra
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev10
|
|
4
4
|
Summary: Official Python SDK for Plystra.
|
|
5
5
|
Project-URL: Homepage, https://plystra.com
|
|
6
6
|
Project-URL: Documentation, https://docs.plystra.com
|
|
@@ -13,12 +13,11 @@ Keywords: auth,authorization,identity,plystra,sdk
|
|
|
13
13
|
Classifier: Development Status :: 3 - Alpha
|
|
14
14
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.10
|
|
22
21
|
Requires-Dist: httpx>=0.28
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
|
|
@@ -27,6 +26,9 @@ Description-Content-Type: text/markdown
|
|
|
27
26
|
Official Python SDK for Plystra Core v1.0.
|
|
28
27
|
|
|
29
28
|
PyPI package name: `plystra`
|
|
29
|
+
Repository name: `plystra/python-sdk`
|
|
30
|
+
|
|
31
|
+
Requires Python 3.10 or newer.
|
|
30
32
|
|
|
31
33
|
## Install
|
|
32
34
|
|
|
@@ -36,12 +38,45 @@ pip install plystra
|
|
|
36
38
|
|
|
37
39
|
## Synchronous Usage
|
|
38
40
|
|
|
41
|
+
For production backend code, prefer passing a server-side access token or API key that was issued outside the request path. Keep password login available for admin tools and bootstrap flows, not for routine service-to-service checks.
|
|
42
|
+
|
|
43
|
+
Using an API key:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from plystra import Plystra
|
|
47
|
+
|
|
48
|
+
with Plystra("https://plystra.internal", api_key="ply_ak_...") as plystra:
|
|
49
|
+
decision = plystra.authz.check(
|
|
50
|
+
actor_user_id="user_alice",
|
|
51
|
+
actor_member_id="member_finance_reviewer",
|
|
52
|
+
actor_user_member_id="um_alice_finance_reviewer",
|
|
53
|
+
space_id="space_acme",
|
|
54
|
+
resource_type="invoice",
|
|
55
|
+
resource_id="invoice_001",
|
|
56
|
+
action="approve",
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Using an access token received from your frontend or gateway session:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from plystra import Plystra
|
|
64
|
+
|
|
65
|
+
with Plystra("https://plystra.internal", access_token=session["plystra_access_token"]) as plystra:
|
|
66
|
+
print(plystra.authz.check(action="approve", resource_type="invoice", resource_id="invoice_001"))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Password login is still supported:
|
|
70
|
+
|
|
39
71
|
```python
|
|
40
72
|
from plystra import Plystra
|
|
41
73
|
|
|
42
74
|
with Plystra("http://localhost:8080") as plystra:
|
|
43
75
|
plystra.auth.login("alice@example.com", "plystra-demo")
|
|
44
76
|
|
|
77
|
+
# Core rotates refresh tokens. Omitting the argument uses the stored one.
|
|
78
|
+
plystra.auth.refresh()
|
|
79
|
+
|
|
45
80
|
decision = plystra.authz.check(
|
|
46
81
|
actor={
|
|
47
82
|
"user_id": "user_alice",
|
|
@@ -65,8 +100,7 @@ from plystra import AsyncPlystra
|
|
|
65
100
|
|
|
66
101
|
|
|
67
102
|
async def main() -> None:
|
|
68
|
-
async with AsyncPlystra("
|
|
69
|
-
await plystra.auth.login("alice@example.com", "plystra-demo")
|
|
103
|
+
async with AsyncPlystra("https://plystra.internal", api_key="ply_ak_...") as plystra:
|
|
70
104
|
decision = await plystra.authz.check(
|
|
71
105
|
actor_user_id="user_alice",
|
|
72
106
|
actor_member_id="member_finance_reviewer",
|
|
@@ -90,6 +124,7 @@ The SDK exposes v1.0 Core modules as attributes:
|
|
|
90
124
|
- `auth`
|
|
91
125
|
- `actor`
|
|
92
126
|
- `admin`
|
|
127
|
+
- `api_keys`
|
|
93
128
|
- `users`
|
|
94
129
|
- `spaces`
|
|
95
130
|
- `groups`
|
|
@@ -107,4 +142,6 @@ The SDK exposes v1.0 Core modules as attributes:
|
|
|
107
142
|
- `plugins`
|
|
108
143
|
- `templates`
|
|
109
144
|
|
|
110
|
-
Non-public endpoints require a Bearer session whose user has an active admin grant.
|
|
145
|
+
Non-public endpoints require either a Bearer session whose user has an active admin grant or a scoped API key with matching permission keys.
|
|
146
|
+
|
|
147
|
+
Keep SDK tokens in a server-side encrypted session store. `login()` and `refresh()` update `access_token` and `refresh_token` on the client; call `plystra.tokens()` when you need to persist the rotated token pair.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# plystra
|
|
2
|
+
|
|
3
|
+
Official Python SDK for Plystra Core v1.0.
|
|
4
|
+
|
|
5
|
+
PyPI package name: `plystra`
|
|
6
|
+
Repository name: `plystra/python-sdk`
|
|
7
|
+
|
|
8
|
+
Requires Python 3.10 or newer.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install plystra
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Synchronous Usage
|
|
17
|
+
|
|
18
|
+
For production backend code, prefer passing a server-side access token or API key that was issued outside the request path. Keep password login available for admin tools and bootstrap flows, not for routine service-to-service checks.
|
|
19
|
+
|
|
20
|
+
Using an API key:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from plystra import Plystra
|
|
24
|
+
|
|
25
|
+
with Plystra("https://plystra.internal", api_key="ply_ak_...") as plystra:
|
|
26
|
+
decision = plystra.authz.check(
|
|
27
|
+
actor_user_id="user_alice",
|
|
28
|
+
actor_member_id="member_finance_reviewer",
|
|
29
|
+
actor_user_member_id="um_alice_finance_reviewer",
|
|
30
|
+
space_id="space_acme",
|
|
31
|
+
resource_type="invoice",
|
|
32
|
+
resource_id="invoice_001",
|
|
33
|
+
action="approve",
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Using an access token received from your frontend or gateway session:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from plystra import Plystra
|
|
41
|
+
|
|
42
|
+
with Plystra("https://plystra.internal", access_token=session["plystra_access_token"]) as plystra:
|
|
43
|
+
print(plystra.authz.check(action="approve", resource_type="invoice", resource_id="invoice_001"))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Password login is still supported:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from plystra import Plystra
|
|
50
|
+
|
|
51
|
+
with Plystra("http://localhost:8080") as plystra:
|
|
52
|
+
plystra.auth.login("alice@example.com", "plystra-demo")
|
|
53
|
+
|
|
54
|
+
# Core rotates refresh tokens. Omitting the argument uses the stored one.
|
|
55
|
+
plystra.auth.refresh()
|
|
56
|
+
|
|
57
|
+
decision = plystra.authz.check(
|
|
58
|
+
actor={
|
|
59
|
+
"user_id": "user_alice",
|
|
60
|
+
"space_id": "space_acme",
|
|
61
|
+
"member_id": "member_finance_reviewer",
|
|
62
|
+
"user_member_id": "um_alice_finance_reviewer",
|
|
63
|
+
},
|
|
64
|
+
resource_type="invoice",
|
|
65
|
+
resource_id="invoice_001",
|
|
66
|
+
action="approve",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
print(decision["decision"])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Asynchronous Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from plystra import AsyncPlystra
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def main() -> None:
|
|
80
|
+
async with AsyncPlystra("https://plystra.internal", api_key="ply_ak_...") as plystra:
|
|
81
|
+
decision = await plystra.authz.check(
|
|
82
|
+
actor_user_id="user_alice",
|
|
83
|
+
actor_member_id="member_finance_reviewer",
|
|
84
|
+
actor_user_member_id="um_alice_finance_reviewer",
|
|
85
|
+
space_id="space_acme",
|
|
86
|
+
resource_type="invoice",
|
|
87
|
+
resource_id="invoice_001",
|
|
88
|
+
action="approve",
|
|
89
|
+
)
|
|
90
|
+
print(decision["decision"])
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Modules
|
|
97
|
+
|
|
98
|
+
The SDK exposes v1.0 Core modules as attributes:
|
|
99
|
+
|
|
100
|
+
- `system`
|
|
101
|
+
- `auth`
|
|
102
|
+
- `actor`
|
|
103
|
+
- `admin`
|
|
104
|
+
- `api_keys`
|
|
105
|
+
- `users`
|
|
106
|
+
- `spaces`
|
|
107
|
+
- `groups`
|
|
108
|
+
- `members`
|
|
109
|
+
- `user_members`
|
|
110
|
+
- `roles`
|
|
111
|
+
- `member_roles`
|
|
112
|
+
- `permissions`
|
|
113
|
+
- `role_permissions`
|
|
114
|
+
- `resource_types`
|
|
115
|
+
- `resources`
|
|
116
|
+
- `authz`
|
|
117
|
+
- `audit`
|
|
118
|
+
- `data`
|
|
119
|
+
- `plugins`
|
|
120
|
+
- `templates`
|
|
121
|
+
|
|
122
|
+
Non-public endpoints require either a Bearer session whose user has an active admin grant or a scoped API key with matching permission keys.
|
|
123
|
+
|
|
124
|
+
Keep SDK tokens in a server-side encrypted session store. `login()` and `refresh()` update `access_token` and `refresh_token` on the client; call `plystra.tokens()` when you need to persist the rotated token pair.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .exceptions import PlystraAPIError
|
|
9
|
+
from .types import Query
|
|
10
|
+
|
|
11
|
+
def _data_or_raise(response: httpx.Response) -> Any:
|
|
12
|
+
try:
|
|
13
|
+
envelope = response.json()
|
|
14
|
+
except ValueError as exc:
|
|
15
|
+
raise PlystraAPIError(
|
|
16
|
+
"INVALID_JSON_RESPONSE",
|
|
17
|
+
f"Plystra returned non-JSON response with HTTP {response.status_code}",
|
|
18
|
+
status_code=response.status_code,
|
|
19
|
+
details=response.text,
|
|
20
|
+
) from exc
|
|
21
|
+
|
|
22
|
+
error = envelope.get("error") if isinstance(envelope, dict) else None
|
|
23
|
+
if response.status_code >= 400 or error:
|
|
24
|
+
error = error or {}
|
|
25
|
+
raise PlystraAPIError(
|
|
26
|
+
error.get("code", "HTTP_ERROR"),
|
|
27
|
+
error.get("message", response.reason_phrase),
|
|
28
|
+
status_code=response.status_code,
|
|
29
|
+
details=error.get("details"),
|
|
30
|
+
request_id=error.get("request_id") or envelope.get("request_id") or envelope.get("meta", {}).get("request_id"),
|
|
31
|
+
trace_id=error.get("trace_id"),
|
|
32
|
+
audit_log_id=error.get("audit_log_id"),
|
|
33
|
+
)
|
|
34
|
+
return envelope.get("data") if isinstance(envelope, dict) else envelope
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _clean_query(query: Query | None) -> dict[str, str]:
|
|
38
|
+
if not query:
|
|
39
|
+
return {}
|
|
40
|
+
return {key: str(value).lower() if isinstance(value, bool) else str(value) for key, value in query.items() if value is not None}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _e(value: str) -> str:
|
|
44
|
+
return quote(value, safe="")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _store_tokens(client: Plystra | AsyncPlystra, data: Mapping[str, Any]) -> None:
|
|
48
|
+
access_token = data.get("access_token")
|
|
49
|
+
refresh_token = data.get("refresh_token")
|
|
50
|
+
client.set_tokens(
|
|
51
|
+
access_token=access_token if isinstance(access_token, str) else client.access_token,
|
|
52
|
+
refresh_token=refresh_token if isinstance(refresh_token, str) else client.refresh_token,
|
|
53
|
+
)
|