atlas-backend 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.
- atlas_backend-0.1.0/.gitignore +9 -0
- atlas_backend-0.1.0/PKG-INFO +130 -0
- atlas_backend-0.1.0/README.md +113 -0
- atlas_backend-0.1.0/atlas_backend/__init__.py +55 -0
- atlas_backend-0.1.0/atlas_backend/_client.py +139 -0
- atlas_backend-0.1.0/atlas_backend/_errors.py +62 -0
- atlas_backend-0.1.0/atlas_backend/_handshake.py +330 -0
- atlas_backend-0.1.0/atlas_backend/_pagination.py +81 -0
- atlas_backend-0.1.0/atlas_backend/_transport.py +183 -0
- atlas_backend-0.1.0/atlas_backend/_types.py +51 -0
- atlas_backend-0.1.0/atlas_backend/aresources.py +792 -0
- atlas_backend-0.1.0/atlas_backend/models.py +653 -0
- atlas_backend-0.1.0/atlas_backend/py.typed +0 -0
- atlas_backend-0.1.0/atlas_backend/resources.py +765 -0
- atlas_backend-0.1.0/pyproject.toml +55 -0
- atlas_backend-0.1.0/tests/test_client.py +414 -0
- atlas_backend-0.1.0/tests/test_handshake.py +197 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atlas-backend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python backend SDK for Atlas — a typed client over the sk_ Backend API (BAPI).
|
|
5
|
+
Author: Atlas
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: atlas,auth,authentication,backend,bapi,sdk
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.23
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.5; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# atlas-backend
|
|
19
|
+
|
|
20
|
+
The official **Python backend SDK** for Atlas — a typed client over the `sk_`
|
|
21
|
+
Backend API (BAPI). It is the Python peer of the TypeScript `@atlas/backend`
|
|
22
|
+
SDK and mirrors it namespace-for-namespace.
|
|
23
|
+
|
|
24
|
+
- Typed (`py.typed`), one namespace per BAPI area.
|
|
25
|
+
- Sync `AtlasClient` **and** async `AsyncAtlasClient`, built on `httpx`.
|
|
26
|
+
- The `{ errors: [{ code, message, param }] }` envelope surfaces as `AtlasError`.
|
|
27
|
+
- Cursor pagination helper, `Idempotency-Key` on creates.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install atlas-backend
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from atlas_backend import AtlasClient, AtlasError, paginate
|
|
39
|
+
|
|
40
|
+
atlas = AtlasClient("sk_live_...", base_url="https://api.atlasauth.net")
|
|
41
|
+
|
|
42
|
+
# Create a user (idempotency key optional, forwarded as Idempotency-Key).
|
|
43
|
+
user = atlas.users.create(
|
|
44
|
+
{"email_address": "ada@example.com", "first_name": "Ada"},
|
|
45
|
+
idempotency_key="signup-ada-001",
|
|
46
|
+
)
|
|
47
|
+
print(user["id"])
|
|
48
|
+
|
|
49
|
+
# List organizations (a single cursor page).
|
|
50
|
+
page = atlas.organizations.list(limit=20)
|
|
51
|
+
print(page["data"], page["has_more"], page["next_cursor"])
|
|
52
|
+
|
|
53
|
+
# Auto-iterate every page.
|
|
54
|
+
for org in paginate(atlas.organizations.list):
|
|
55
|
+
print(org["name"])
|
|
56
|
+
|
|
57
|
+
# Errors carry the HTTP status and the stable BAPI error code.
|
|
58
|
+
try:
|
|
59
|
+
atlas.users.get("user_does_not_exist")
|
|
60
|
+
except AtlasError as err:
|
|
61
|
+
print(err.status) # e.g. 404
|
|
62
|
+
print(err.code) # e.g. "NOT_FOUND"
|
|
63
|
+
print(err.errors[0].get("param"))
|
|
64
|
+
if err.has_code("NOT_FOUND"):
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Context manager
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
with AtlasClient("sk_live_...") as atlas:
|
|
72
|
+
atlas.users.list(limit=10)
|
|
73
|
+
# underlying httpx.Client is closed on exit
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Async
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import asyncio
|
|
80
|
+
from atlas_backend import AsyncAtlasClient, acollect
|
|
81
|
+
|
|
82
|
+
async def main() -> None:
|
|
83
|
+
async with AsyncAtlasClient("sk_live_...") as atlas:
|
|
84
|
+
user = await atlas.users.create({"email_address": "grace@example.com"})
|
|
85
|
+
orgs = await acollect(atlas.organizations.list)
|
|
86
|
+
print(user["id"], len(orgs))
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
| Argument | Default | Notes |
|
|
94
|
+
| ------------- | ------------------------- | ------------------------------------------------ |
|
|
95
|
+
| `secret_key` | — | `sk_...`; sent as `Authorization: Bearer <key>`. |
|
|
96
|
+
| `base_url` | `https://api.atlasauth.net` | The instance's BAPI origin (`BAPI_ORIGIN`). |
|
|
97
|
+
| `timeout` | `30.0` | Per-request timeout, seconds. |
|
|
98
|
+
| `http_client` | a new `httpx.Client` | Pass your own for pooling / proxies / retries. |
|
|
99
|
+
|
|
100
|
+
## Namespaces
|
|
101
|
+
|
|
102
|
+
`users`, `sessions`, `organizations` (+ `memberships`, `invitations`,
|
|
103
|
+
`domains`, `group_roles`), `roles`, `permissions`, `oauth_clients` (+ `grants`),
|
|
104
|
+
`resource_servers`, `sso_connections`, `scim_tokens`, `domains`, `waitlist`,
|
|
105
|
+
`allowlist`, `blocklist`, `attack_protection`, `actor_tokens`, `invitations`,
|
|
106
|
+
`webhooks` (+ `endpoints`), `sign_in_tokens`, `audit_logs`, `jwt_templates`.
|
|
107
|
+
|
|
108
|
+
## Pagination
|
|
109
|
+
|
|
110
|
+
`paginate(list_fn, **params)` and `collect(list_fn, **params)` walk every
|
|
111
|
+
cursor page of any `list` that returns `{ data, has_more, next_cursor }`. Async
|
|
112
|
+
peers: `aiterate` / `acollect`.
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
Every non-2xx raises `AtlasError`, carrying:
|
|
117
|
+
|
|
118
|
+
- `status` — the HTTP status code.
|
|
119
|
+
- `errors` — the full `[{ code, message, param?, meta? }]` envelope.
|
|
120
|
+
- `code` — the first error's stable code (what you branch on).
|
|
121
|
+
- `has_code(code)` — True if any error in the envelope has that code.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
ruff check .
|
|
128
|
+
mypy .
|
|
129
|
+
pytest
|
|
130
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# atlas-backend
|
|
2
|
+
|
|
3
|
+
The official **Python backend SDK** for Atlas — a typed client over the `sk_`
|
|
4
|
+
Backend API (BAPI). It is the Python peer of the TypeScript `@atlas/backend`
|
|
5
|
+
SDK and mirrors it namespace-for-namespace.
|
|
6
|
+
|
|
7
|
+
- Typed (`py.typed`), one namespace per BAPI area.
|
|
8
|
+
- Sync `AtlasClient` **and** async `AsyncAtlasClient`, built on `httpx`.
|
|
9
|
+
- The `{ errors: [{ code, message, param }] }` envelope surfaces as `AtlasError`.
|
|
10
|
+
- Cursor pagination helper, `Idempotency-Key` on creates.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install atlas-backend
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from atlas_backend import AtlasClient, AtlasError, paginate
|
|
22
|
+
|
|
23
|
+
atlas = AtlasClient("sk_live_...", base_url="https://api.atlasauth.net")
|
|
24
|
+
|
|
25
|
+
# Create a user (idempotency key optional, forwarded as Idempotency-Key).
|
|
26
|
+
user = atlas.users.create(
|
|
27
|
+
{"email_address": "ada@example.com", "first_name": "Ada"},
|
|
28
|
+
idempotency_key="signup-ada-001",
|
|
29
|
+
)
|
|
30
|
+
print(user["id"])
|
|
31
|
+
|
|
32
|
+
# List organizations (a single cursor page).
|
|
33
|
+
page = atlas.organizations.list(limit=20)
|
|
34
|
+
print(page["data"], page["has_more"], page["next_cursor"])
|
|
35
|
+
|
|
36
|
+
# Auto-iterate every page.
|
|
37
|
+
for org in paginate(atlas.organizations.list):
|
|
38
|
+
print(org["name"])
|
|
39
|
+
|
|
40
|
+
# Errors carry the HTTP status and the stable BAPI error code.
|
|
41
|
+
try:
|
|
42
|
+
atlas.users.get("user_does_not_exist")
|
|
43
|
+
except AtlasError as err:
|
|
44
|
+
print(err.status) # e.g. 404
|
|
45
|
+
print(err.code) # e.g. "NOT_FOUND"
|
|
46
|
+
print(err.errors[0].get("param"))
|
|
47
|
+
if err.has_code("NOT_FOUND"):
|
|
48
|
+
...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Context manager
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
with AtlasClient("sk_live_...") as atlas:
|
|
55
|
+
atlas.users.list(limit=10)
|
|
56
|
+
# underlying httpx.Client is closed on exit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Async
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from atlas_backend import AsyncAtlasClient, acollect
|
|
64
|
+
|
|
65
|
+
async def main() -> None:
|
|
66
|
+
async with AsyncAtlasClient("sk_live_...") as atlas:
|
|
67
|
+
user = await atlas.users.create({"email_address": "grace@example.com"})
|
|
68
|
+
orgs = await acollect(atlas.organizations.list)
|
|
69
|
+
print(user["id"], len(orgs))
|
|
70
|
+
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
| Argument | Default | Notes |
|
|
77
|
+
| ------------- | ------------------------- | ------------------------------------------------ |
|
|
78
|
+
| `secret_key` | — | `sk_...`; sent as `Authorization: Bearer <key>`. |
|
|
79
|
+
| `base_url` | `https://api.atlasauth.net` | The instance's BAPI origin (`BAPI_ORIGIN`). |
|
|
80
|
+
| `timeout` | `30.0` | Per-request timeout, seconds. |
|
|
81
|
+
| `http_client` | a new `httpx.Client` | Pass your own for pooling / proxies / retries. |
|
|
82
|
+
|
|
83
|
+
## Namespaces
|
|
84
|
+
|
|
85
|
+
`users`, `sessions`, `organizations` (+ `memberships`, `invitations`,
|
|
86
|
+
`domains`, `group_roles`), `roles`, `permissions`, `oauth_clients` (+ `grants`),
|
|
87
|
+
`resource_servers`, `sso_connections`, `scim_tokens`, `domains`, `waitlist`,
|
|
88
|
+
`allowlist`, `blocklist`, `attack_protection`, `actor_tokens`, `invitations`,
|
|
89
|
+
`webhooks` (+ `endpoints`), `sign_in_tokens`, `audit_logs`, `jwt_templates`.
|
|
90
|
+
|
|
91
|
+
## Pagination
|
|
92
|
+
|
|
93
|
+
`paginate(list_fn, **params)` and `collect(list_fn, **params)` walk every
|
|
94
|
+
cursor page of any `list` that returns `{ data, has_more, next_cursor }`. Async
|
|
95
|
+
peers: `aiterate` / `acollect`.
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Every non-2xx raises `AtlasError`, carrying:
|
|
100
|
+
|
|
101
|
+
- `status` — the HTTP status code.
|
|
102
|
+
- `errors` — the full `[{ code, message, param?, meta? }]` envelope.
|
|
103
|
+
- `code` — the first error's stable code (what you branch on).
|
|
104
|
+
- `has_code(code)` — True if any error in the envelope has that code.
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install -e ".[dev]"
|
|
110
|
+
ruff check .
|
|
111
|
+
mypy .
|
|
112
|
+
pytest
|
|
113
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""atlas-backend — the official Python backend SDK for Atlas.
|
|
2
|
+
|
|
3
|
+
A typed client over the ``sk_`` Backend API (BAPI), the Python peer of the
|
|
4
|
+
TypeScript ``@atlas/backend`` SDK.
|
|
5
|
+
|
|
6
|
+
from atlas_backend import AtlasClient, AtlasError
|
|
7
|
+
|
|
8
|
+
atlas = AtlasClient("sk_live_...")
|
|
9
|
+
user = atlas.users.create({"email_address": "ada@example.com"})
|
|
10
|
+
for org in paginate(atlas.organizations.list):
|
|
11
|
+
print(org["name"])
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ._client import AsyncAtlasClient, AtlasClient
|
|
17
|
+
from ._errors import AtlasError, ErrorItem
|
|
18
|
+
from ._handshake import (
|
|
19
|
+
HandshakeParams,
|
|
20
|
+
HandshakeSession,
|
|
21
|
+
PKCEPair,
|
|
22
|
+
aredeem_handshake,
|
|
23
|
+
create_pkce_pair,
|
|
24
|
+
read_handshake_params,
|
|
25
|
+
redeem_handshake,
|
|
26
|
+
)
|
|
27
|
+
from ._pagination import acollect, aiterate, collect, paginate
|
|
28
|
+
from ._transport import DEFAULT_BASE_URL
|
|
29
|
+
from ._types import CursorPage, DeletedObject, ListPage, Metadata
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AtlasClient",
|
|
35
|
+
"AsyncAtlasClient",
|
|
36
|
+
"AtlasError",
|
|
37
|
+
"ErrorItem",
|
|
38
|
+
"DEFAULT_BASE_URL",
|
|
39
|
+
"paginate",
|
|
40
|
+
"collect",
|
|
41
|
+
"aiterate",
|
|
42
|
+
"acollect",
|
|
43
|
+
"CursorPage",
|
|
44
|
+
"ListPage",
|
|
45
|
+
"DeletedObject",
|
|
46
|
+
"Metadata",
|
|
47
|
+
"HandshakeSession",
|
|
48
|
+
"HandshakeParams",
|
|
49
|
+
"PKCEPair",
|
|
50
|
+
"create_pkce_pair",
|
|
51
|
+
"redeem_handshake",
|
|
52
|
+
"aredeem_handshake",
|
|
53
|
+
"read_handshake_params",
|
|
54
|
+
"__version__",
|
|
55
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""The typed management clients for the Atlas Backend API.
|
|
2
|
+
|
|
3
|
+
:class:`AtlasClient` is the secret-key surface — the Python peer of the official
|
|
4
|
+
TypeScript SDK's ``createAtlasClient``. Each namespace is one attribute handing
|
|
5
|
+
the shared, config-bound transport to a resource class, so this file reads as a
|
|
6
|
+
table of contents for the whole surface. :class:`AsyncAtlasClient` is the same
|
|
7
|
+
surface over ``asyncio``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from types import TracebackType
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from . import aresources as ar
|
|
17
|
+
from . import resources as r
|
|
18
|
+
from ._transport import DEFAULT_BASE_URL, _AsyncTransport, _SyncTransport
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AtlasClient:
|
|
22
|
+
"""Synchronous Backend API client.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
secret_key: The instance secret key (``sk_...``). Sent as
|
|
26
|
+
``Authorization: Bearer <key>`` on every request; never logged,
|
|
27
|
+
never placed in a URL.
|
|
28
|
+
base_url: Base URL of the instance's Backend API. Defaults to
|
|
29
|
+
``https://api.atlasauth.net``. Trailing slashes are tolerated.
|
|
30
|
+
timeout: Per-request timeout in seconds.
|
|
31
|
+
http_client: An optional pre-configured :class:`httpx.Client` (for
|
|
32
|
+
connection pooling, proxies, retries). One is created if omitted.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
secret_key: str,
|
|
38
|
+
*,
|
|
39
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
40
|
+
timeout: float = 30.0,
|
|
41
|
+
http_client: httpx.Client | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not secret_key:
|
|
44
|
+
raise ValueError("AtlasClient requires a secret_key.")
|
|
45
|
+
self._owns_client = http_client is None
|
|
46
|
+
self._http = http_client or httpx.Client(timeout=timeout)
|
|
47
|
+
transport = _SyncTransport(secret_key, base_url, self._http)
|
|
48
|
+
|
|
49
|
+
self.users = r.Users(transport)
|
|
50
|
+
self.sessions = r.Sessions(transport)
|
|
51
|
+
self.organizations = r.Organizations(transport)
|
|
52
|
+
self.roles = r.Roles(transport)
|
|
53
|
+
self.permissions = r.Permissions(transport)
|
|
54
|
+
self.oauth_clients = r.OAuthClients(transport)
|
|
55
|
+
self.resource_servers = r.ResourceServers(transport)
|
|
56
|
+
self.sso_connections = r.SsoConnections(transport)
|
|
57
|
+
self.scim_tokens = r.ScimTokens(transport)
|
|
58
|
+
self.domains = r.Domains(transport)
|
|
59
|
+
self.waitlist = r.Waitlist(transport)
|
|
60
|
+
self.allowlist = r._Restriction(transport, "/v1/allowlist_identifiers")
|
|
61
|
+
self.blocklist = r._Restriction(transport, "/v1/blocklist_identifiers")
|
|
62
|
+
self.attack_protection = r.AttackProtection(transport)
|
|
63
|
+
self.actor_tokens = r.ActorTokens(transport)
|
|
64
|
+
self.invitations = r.Invitations(transport)
|
|
65
|
+
self.webhooks = r.Webhooks(transport)
|
|
66
|
+
self.sign_in_tokens = r.SignInTokens(transport)
|
|
67
|
+
self.audit_logs = r.AuditLogs(transport)
|
|
68
|
+
self.jwt_templates = r.JwtTemplates(transport)
|
|
69
|
+
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
"""Close the underlying HTTP client, if this client created it."""
|
|
72
|
+
if self._owns_client:
|
|
73
|
+
self._http.close()
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> AtlasClient:
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(
|
|
79
|
+
self,
|
|
80
|
+
exc_type: type[BaseException] | None,
|
|
81
|
+
exc: BaseException | None,
|
|
82
|
+
tb: TracebackType | None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.close()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AsyncAtlasClient:
|
|
88
|
+
"""Asynchronous Backend API client — the ``asyncio`` peer of :class:`AtlasClient`."""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
secret_key: str,
|
|
93
|
+
*,
|
|
94
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
95
|
+
timeout: float = 30.0,
|
|
96
|
+
http_client: httpx.AsyncClient | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
if not secret_key:
|
|
99
|
+
raise ValueError("AsyncAtlasClient requires a secret_key.")
|
|
100
|
+
self._owns_client = http_client is None
|
|
101
|
+
self._http = http_client or httpx.AsyncClient(timeout=timeout)
|
|
102
|
+
transport = _AsyncTransport(secret_key, base_url, self._http)
|
|
103
|
+
|
|
104
|
+
self.users = ar.AsyncUsers(transport)
|
|
105
|
+
self.sessions = ar.AsyncSessions(transport)
|
|
106
|
+
self.organizations = ar.AsyncOrganizations(transport)
|
|
107
|
+
self.roles = ar.AsyncRoles(transport)
|
|
108
|
+
self.permissions = ar.AsyncPermissions(transport)
|
|
109
|
+
self.oauth_clients = ar.AsyncOAuthClients(transport)
|
|
110
|
+
self.resource_servers = ar.AsyncResourceServers(transport)
|
|
111
|
+
self.sso_connections = ar.AsyncSsoConnections(transport)
|
|
112
|
+
self.scim_tokens = ar.AsyncScimTokens(transport)
|
|
113
|
+
self.domains = ar.AsyncDomains(transport)
|
|
114
|
+
self.waitlist = ar.AsyncWaitlist(transport)
|
|
115
|
+
self.allowlist = ar._AsyncRestriction(transport, "/v1/allowlist_identifiers")
|
|
116
|
+
self.blocklist = ar._AsyncRestriction(transport, "/v1/blocklist_identifiers")
|
|
117
|
+
self.attack_protection = ar.AsyncAttackProtection(transport)
|
|
118
|
+
self.actor_tokens = ar.AsyncActorTokens(transport)
|
|
119
|
+
self.invitations = ar.AsyncInvitations(transport)
|
|
120
|
+
self.webhooks = ar.AsyncWebhooks(transport)
|
|
121
|
+
self.sign_in_tokens = ar.AsyncSignInTokens(transport)
|
|
122
|
+
self.audit_logs = ar.AsyncAuditLogs(transport)
|
|
123
|
+
self.jwt_templates = ar.AsyncJwtTemplates(transport)
|
|
124
|
+
|
|
125
|
+
async def aclose(self) -> None:
|
|
126
|
+
"""Close the underlying async HTTP client, if this client created it."""
|
|
127
|
+
if self._owns_client:
|
|
128
|
+
await self._http.aclose()
|
|
129
|
+
|
|
130
|
+
async def __aenter__(self) -> AsyncAtlasClient:
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(
|
|
134
|
+
self,
|
|
135
|
+
exc_type: type[BaseException] | None,
|
|
136
|
+
exc: BaseException | None,
|
|
137
|
+
tb: TracebackType | None,
|
|
138
|
+
) -> None:
|
|
139
|
+
await self.aclose()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The single error type every Backend API call raises on a non-2xx.
|
|
2
|
+
|
|
3
|
+
Atlas answers a failed BAPI call with the §9.1 envelope::
|
|
4
|
+
|
|
5
|
+
{ "errors": [ { "code", "message", "param?", "meta?" } ] }
|
|
6
|
+
|
|
7
|
+
``code`` is the stable, machine-readable part of that contract — integrators
|
|
8
|
+
branch on it (``LAST_ADMIN``, ``NOT_FOUND``, ``SCOPE_MISSING``, ...) — so it is
|
|
9
|
+
surfaced first-class here rather than buried in a parsed body. The raw
|
|
10
|
+
``errors`` list and the HTTP ``status`` are both kept so a caller can inspect
|
|
11
|
+
``param``/``meta`` (e.g. a rate limit's ``retry_after``) when they need to.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, TypedDict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ErrorItem(TypedDict, total=False):
|
|
20
|
+
"""One entry of the BAPI error envelope."""
|
|
21
|
+
|
|
22
|
+
code: str
|
|
23
|
+
message: str
|
|
24
|
+
param: str
|
|
25
|
+
meta: dict[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AtlasError(Exception):
|
|
29
|
+
"""Raised on any non-2xx response from the Backend API.
|
|
30
|
+
|
|
31
|
+
Carries the HTTP ``status`` and the full parsed ``errors`` envelope. Branch
|
|
32
|
+
on :attr:`code` (the first error's stable code) or use :meth:`has_code`.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
status: int
|
|
36
|
+
errors: list[ErrorItem]
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
status: int,
|
|
41
|
+
errors: list[ErrorItem],
|
|
42
|
+
message: str | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
first_message = errors[0].get("message") if errors else None
|
|
45
|
+
super().__init__(
|
|
46
|
+
message
|
|
47
|
+
or first_message
|
|
48
|
+
or f"Atlas API request failed with status {status}"
|
|
49
|
+
)
|
|
50
|
+
self.status = status
|
|
51
|
+
self.errors = errors
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def code(self) -> str | None:
|
|
55
|
+
"""The first error's stable code, the field callers branch on most."""
|
|
56
|
+
if not self.errors:
|
|
57
|
+
return None
|
|
58
|
+
return self.errors[0].get("code")
|
|
59
|
+
|
|
60
|
+
def has_code(self, code: str) -> bool:
|
|
61
|
+
"""True when any error in the envelope carries the given stable code."""
|
|
62
|
+
return any(e.get("code") == code for e in self.errors)
|