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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plystra
3
- Version: 1.0.0.dev2
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.9
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("http://localhost:8080") as plystra:
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.
@@ -1,8 +1,10 @@
1
1
  from .client import (
2
2
  AsyncPlystra,
3
3
  Plystra,
4
- PlystraAPIError,
5
4
  PlystraClient,
5
+ )
6
+ from .exceptions import (
7
+ PlystraAPIError,
6
8
  PlystraError,
7
9
  PlystraNetworkError,
8
10
  PlystraTimeoutError,
@@ -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
+ )