pawabase 0.3.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.
- pawabase-0.3.0/.gitignore +8 -0
- pawabase-0.3.0/LICENSE +29 -0
- pawabase-0.3.0/PKG-INFO +183 -0
- pawabase-0.3.0/README.md +151 -0
- pawabase-0.3.0/pawabase/__init__.py +72 -0
- pawabase-0.3.0/pawabase/_core.py +78 -0
- pawabase-0.3.0/pawabase/admin.py +230 -0
- pawabase-0.3.0/pawabase/auth.py +607 -0
- pawabase-0.3.0/pawabase/callables.py +101 -0
- pawabase-0.3.0/pawabase/client.py +301 -0
- pawabase-0.3.0/pawabase/data.py +584 -0
- pawabase-0.3.0/pawabase/errors.py +131 -0
- pawabase-0.3.0/pawabase/manage/__init__.py +90 -0
- pawabase-0.3.0/pawabase/manage/_base.py +70 -0
- pawabase-0.3.0/pawabase/manage/access.py +158 -0
- pawabase-0.3.0/pawabase/manage/automation.py +144 -0
- pawabase-0.3.0/pawabase/manage/data.py +149 -0
- pawabase-0.3.0/pawabase/manage/files.py +63 -0
- pawabase-0.3.0/pawabase/manage/functions.py +61 -0
- pawabase-0.3.0/pawabase/manage/lifecycle.py +182 -0
- pawabase-0.3.0/pawabase/manage/mail.py +27 -0
- pawabase-0.3.0/pawabase/manage/observe.py +170 -0
- pawabase-0.3.0/pawabase/manage/realtime.py +69 -0
- pawabase-0.3.0/pawabase/py.typed +0 -0
- pawabase-0.3.0/pawabase/realtime.py +391 -0
- pawabase-0.3.0/pawabase/storage.py +166 -0
- pawabase-0.3.0/pyproject.toml +55 -0
- pawabase-0.3.0/tests/conftest.py +66 -0
- pawabase-0.3.0/tests/test_auth.py +198 -0
- pawabase-0.3.0/tests/test_data.py +122 -0
- pawabase-0.3.0/tests/test_live.py +119 -0
- pawabase-0.3.0/tests/test_manage_admin.py +153 -0
- pawabase-0.3.0/tests/test_realtime.py +130 -0
- pawabase-0.3.0/tests/test_storage_callables.py +84 -0
pawabase-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Pawabase contributors
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
pawabase-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pawabase
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python client for Pawabase: records, auth, storage, functions, flows, realtime, user administration and environment management.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Pawabase/pawabase-python
|
|
6
|
+
Project-URL: Documentation, https://github.com/Pawabase/pawabase-docs
|
|
7
|
+
Project-URL: Issues, https://github.com/Pawabase/pawabase-python/issues
|
|
8
|
+
Author: Pawabase contributors
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: auth,baas,backend,client,pawabase,realtime,sdk,storage
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: realtime
|
|
26
|
+
Requires-Dist: websockets>=13; extra == 'realtime'
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'test'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
30
|
+
Requires-Dist: websockets>=13; extra == 'test'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# pawabase
|
|
34
|
+
|
|
35
|
+
The Python client SDK for [Pawabase](https://github.com/Pawabase/pawabase): records, auth, storage, functions, flows, realtime, user administration
|
|
36
|
+
and environment management, for apps, scripts and servers. Blocking and `asyncio` clients with the same methods.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install pawabase # httpx only
|
|
40
|
+
pip install "pawabase[realtime]" # + websockets, for live channels
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Writing functions that run *on* Pawabase, and the `pawabase` command, are a separate package: [`pawabase-functions`](https://github.com/Pawabase/pawabase-functions-python).
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pawabase import Pawabase
|
|
47
|
+
|
|
48
|
+
pb = Pawabase("https://api.example.com", "pb_pk_…") # AsyncPawabase for asyncio: the same methods, awaited
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A **publishable** key (`pb_pk_…`) is what an app holds: policies decide what it can do, and a signed-in user's token goes on top. A **secret** key
|
|
52
|
+
(`pb_sk_…`) belongs on servers only: it bypasses policies and unlocks `pb.admin` and `pb.manage`.
|
|
53
|
+
|
|
54
|
+
## Records
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
posts = pb.from_("posts")
|
|
58
|
+
|
|
59
|
+
page = posts.eq("status", "live").gte("views", 10).order("-created_at").select("id", "title").expand("author").per_page(20).execute()
|
|
60
|
+
page.data, page.total, page.has_next
|
|
61
|
+
|
|
62
|
+
for post in posts.eq("status", "live").iterate(): # every row, page after page
|
|
63
|
+
...
|
|
64
|
+
posts.where(status="live", views={"gte": 10}).all() # a list
|
|
65
|
+
posts.eq("slug", "hello").single() # NotFoundError if none
|
|
66
|
+
posts.eq("status", "draft").count()
|
|
67
|
+
|
|
68
|
+
row = posts.create(title="Hello")
|
|
69
|
+
posts.get(row["id"], expand=["author"])
|
|
70
|
+
posts.update(row["id"], title="Hello, world") # PATCH
|
|
71
|
+
posts.delete(row["id"])
|
|
72
|
+
posts.create_many(rows) # failures are collected, not raised
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Operators: `eq neq gt gte lt lte like ilike in_ not_in is_ is_not`. The server applies **one filter per field**; a second filter on a field raises `QueryError`
|
|
76
|
+
before anything is sent, instead of silently dropping one. `eq(col, None)` and `eq(col, True)` become `is`, because `eq.false` matches true rows on the server.
|
|
77
|
+
|
|
78
|
+
## Accounts
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
pb.auth.sign_up(email="ada@example.com", password="…", name="Ada")
|
|
82
|
+
result = pb.auth.sign_in_with_password(email="ada@example.com", password="…")
|
|
83
|
+
if result.status == "mfa_required":
|
|
84
|
+
result = result.verify(input("Code: "))
|
|
85
|
+
pb.auth.get_user(); pb.auth.update_user(name="Ada L.")
|
|
86
|
+
pb.auth.sign_out() # scope="global" signs out everywhere
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The session is kept in memory (pass `session_store=FileSessionStore("~/.myapp/session.json")` to stay signed in between runs), its token goes on every
|
|
90
|
+
request, and an expiring or rejected token is renewed with the refresh token. Also: magic links, social sign-in (`oauth_url`, `exchange_code`), email
|
|
91
|
+
verification, password reset, linked identities, `pb.auth.mfa` (authenticator app, emailed codes, recovery codes), `pb.auth.sessions`, `pb.auth.devices`,
|
|
92
|
+
`pb.auth.api_keys` (keys a user makes for themselves), and `pb.auth.orgs` (organizations, members, invitations, teams).
|
|
93
|
+
|
|
94
|
+
More ways in: guest sessions (`sign_in_anonymously()`, then `upgrade_anonymous(email=…, password=…)` keeps what the guest made), emailed codes
|
|
95
|
+
(`send_email_code(email=…)` then `verify_email_code(email=…, code=…)`), and passkeys (`pb.auth.passkeys`: `register_options` / `register_verify` and
|
|
96
|
+
`authenticate_options` / `authenticate_verify` carry the WebAuthn ceremony; the device does the signing). `issue_token("billing")` trades the session for
|
|
97
|
+
a short-lived token for another service, and `jwks()` returns the public keys a server checks such tokens with.
|
|
98
|
+
|
|
99
|
+
A web server acts as the visiting user without sharing a connection's credentials:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
with pb.as_user(request_token) as user_client: # same connection pool, this user's token
|
|
103
|
+
user_client.from_("orders").execute()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Files
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
avatars = pb.storage.from_("avatars")
|
|
110
|
+
avatars.upload("ada.png", open("ada.png", "rb"), content_type="image/png")
|
|
111
|
+
data = avatars.download("ada.png")
|
|
112
|
+
avatars.list(prefix="2026/"); list(avatars.list_all())
|
|
113
|
+
avatars.public_url("ada.png") # public buckets: no key needed
|
|
114
|
+
avatars.transform_url("ada.png", width=200, height=200, fit="cover", format="webp")
|
|
115
|
+
avatars.create_signed_url("report.pdf", expires_in=600) # a temporary link anyone can read
|
|
116
|
+
avatars.create_signed_upload_url("in/x.png", max_bytes=1_000_000) # a browser uploads straight to storage
|
|
117
|
+
avatars.create_share_link("report.pdf", expires_in=3600) # for people with no account (secret key)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Functions, flows, routes
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
pb.functions.invoke("orders.create", sku="A1") # branch="my-feature" runs a branch's version
|
|
124
|
+
pb.flows.invoke("welcome", email="ada@example.com")
|
|
125
|
+
pb.routes.post("orders/42/pay", method="card")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Realtime
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
async with AsyncPawabase(url, key) as pb:
|
|
132
|
+
room = pb.realtime.channel("chat:lobby", presence={"name": "Ada"})
|
|
133
|
+
room.on("message", lambda m: print(m["payload"]))
|
|
134
|
+
await room.subscribe()
|
|
135
|
+
await room.send("message", {"text": "hello"})
|
|
136
|
+
async for change in pb.realtime.channel("resource:posts"): # live record changes, as you may read them
|
|
137
|
+
print(change["event"], change["payload"]["record"])
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Publishing, presence and history also work over plain HTTP on the blocking client: `pb.realtime.publish(...)`, `.presence(channel)`, `.history(channel)`.
|
|
141
|
+
|
|
142
|
+
Without a socket: `pb.realtime.sse(["chat:lobby"])` listens over Server-Sent Events (a generator on the sync client, an async generator on the async one), and
|
|
143
|
+
`pb.realtime.publish(...)` sends. Channel rules and dashboards are managed with `pb.manage.realtime_channels` and `pb.manage.dashboards`.
|
|
144
|
+
|
|
145
|
+
## Administering users and the environment (secret key)
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
admin = Pawabase(url, "pb_sk_…", environment="production")
|
|
149
|
+
|
|
150
|
+
admin.admin.users.create(email="ada@example.com", password="…", roles=["editor"])
|
|
151
|
+
admin.admin.users.update(user_id, disabled=True)
|
|
152
|
+
admin.admin.orgs.add_member("acme", email="ada@example.com", role="admin")
|
|
153
|
+
|
|
154
|
+
admin.manage.secrets.set("STRIPE_KEY", "sk_live_…")
|
|
155
|
+
admin.manage.firewall.ban("203.0.113.7", seconds=3600)
|
|
156
|
+
admin.manage.jobs.replay(job="send_mail", since_minutes=60, dry_run=True)
|
|
157
|
+
admin.manage.observability.logs(q="timeout", minutes=15)
|
|
158
|
+
admin.manage.backups.snapshot("before-launch")
|
|
159
|
+
admin.manage.environment("staging").flows.list() # the same API for another environment the key may manage
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`pb.admin` administers the people who sign in to your application: `users`, `roles`, `orgs`, `invitations`, `sessions`, `api_keys`, `devices`, `insight`
|
|
163
|
+
(events, statistics, identity backup, hosted pages). `pb.manage` is the environment's management plane, in four families:
|
|
164
|
+
|
|
165
|
+
| Family | Services |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| Data | `resources`, `schema`, `policies`, `schemas`, `transformers`, `routes`, `database`, `runtime` |
|
|
168
|
+
| Automation | `flows`, `schedules`, `subscriptions`, `events`, `webhooks`, `inbound_hooks`, `jobs`, `functions` |
|
|
169
|
+
| Files and mail | `buckets`, `cache`, `mail`, `mail_templates` |
|
|
170
|
+
| Access | `keys`, `secrets`, `domains`, `firewall` |
|
|
171
|
+
| Observe | `observability`, `alarms`, `status_page`, `health` (service health, plan usage, audit) |
|
|
172
|
+
| Lifecycle | `environments` (previews, promote, blueprints), `releases` (branches, revisions, API versions), `backups` (backups, snapshots, restore) |
|
|
173
|
+
|
|
174
|
+
Definitions (`flows`, `policies`, `webhooks`…) share `list`, `get_one`, `create`, `replace` and `remove`, and take `branch=` to work on a feature branch.
|
|
175
|
+
Both are scoped to the client's environment; a key manages only its own.
|
|
176
|
+
|
|
177
|
+
## Errors and retries
|
|
178
|
+
|
|
179
|
+
Every failure raises `PawabaseError` (`.status_code`, `.code`, `.message`, `.details`, `.request_id`) or a subclass: `AuthenticationError` (401), `ForbiddenError`
|
|
180
|
+
(403), `QuotaExceededError` (403, the plan's limit: `details` has `limit`, `allowed`, `used`), `NotFoundError`, `ConflictError` (409), `ValidationError` (400/422),
|
|
181
|
+
`RateLimitedError`, `ServerError`, `NetworkError`. Reads are retried on connection errors and `502/503/504`; a `429` waits as the gateway asks.
|
|
182
|
+
|
|
183
|
+
BSD 3-Clause licensed.
|
pawabase-0.3.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# pawabase
|
|
2
|
+
|
|
3
|
+
The Python client SDK for [Pawabase](https://github.com/Pawabase/pawabase): records, auth, storage, functions, flows, realtime, user administration
|
|
4
|
+
and environment management, for apps, scripts and servers. Blocking and `asyncio` clients with the same methods.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install pawabase # httpx only
|
|
8
|
+
pip install "pawabase[realtime]" # + websockets, for live channels
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Writing functions that run *on* Pawabase, and the `pawabase` command, are a separate package: [`pawabase-functions`](https://github.com/Pawabase/pawabase-functions-python).
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pawabase import Pawabase
|
|
15
|
+
|
|
16
|
+
pb = Pawabase("https://api.example.com", "pb_pk_…") # AsyncPawabase for asyncio: the same methods, awaited
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A **publishable** key (`pb_pk_…`) is what an app holds: policies decide what it can do, and a signed-in user's token goes on top. A **secret** key
|
|
20
|
+
(`pb_sk_…`) belongs on servers only: it bypasses policies and unlocks `pb.admin` and `pb.manage`.
|
|
21
|
+
|
|
22
|
+
## Records
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
posts = pb.from_("posts")
|
|
26
|
+
|
|
27
|
+
page = posts.eq("status", "live").gte("views", 10).order("-created_at").select("id", "title").expand("author").per_page(20).execute()
|
|
28
|
+
page.data, page.total, page.has_next
|
|
29
|
+
|
|
30
|
+
for post in posts.eq("status", "live").iterate(): # every row, page after page
|
|
31
|
+
...
|
|
32
|
+
posts.where(status="live", views={"gte": 10}).all() # a list
|
|
33
|
+
posts.eq("slug", "hello").single() # NotFoundError if none
|
|
34
|
+
posts.eq("status", "draft").count()
|
|
35
|
+
|
|
36
|
+
row = posts.create(title="Hello")
|
|
37
|
+
posts.get(row["id"], expand=["author"])
|
|
38
|
+
posts.update(row["id"], title="Hello, world") # PATCH
|
|
39
|
+
posts.delete(row["id"])
|
|
40
|
+
posts.create_many(rows) # failures are collected, not raised
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Operators: `eq neq gt gte lt lte like ilike in_ not_in is_ is_not`. The server applies **one filter per field**; a second filter on a field raises `QueryError`
|
|
44
|
+
before anything is sent, instead of silently dropping one. `eq(col, None)` and `eq(col, True)` become `is`, because `eq.false` matches true rows on the server.
|
|
45
|
+
|
|
46
|
+
## Accounts
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
pb.auth.sign_up(email="ada@example.com", password="…", name="Ada")
|
|
50
|
+
result = pb.auth.sign_in_with_password(email="ada@example.com", password="…")
|
|
51
|
+
if result.status == "mfa_required":
|
|
52
|
+
result = result.verify(input("Code: "))
|
|
53
|
+
pb.auth.get_user(); pb.auth.update_user(name="Ada L.")
|
|
54
|
+
pb.auth.sign_out() # scope="global" signs out everywhere
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The session is kept in memory (pass `session_store=FileSessionStore("~/.myapp/session.json")` to stay signed in between runs), its token goes on every
|
|
58
|
+
request, and an expiring or rejected token is renewed with the refresh token. Also: magic links, social sign-in (`oauth_url`, `exchange_code`), email
|
|
59
|
+
verification, password reset, linked identities, `pb.auth.mfa` (authenticator app, emailed codes, recovery codes), `pb.auth.sessions`, `pb.auth.devices`,
|
|
60
|
+
`pb.auth.api_keys` (keys a user makes for themselves), and `pb.auth.orgs` (organizations, members, invitations, teams).
|
|
61
|
+
|
|
62
|
+
More ways in: guest sessions (`sign_in_anonymously()`, then `upgrade_anonymous(email=…, password=…)` keeps what the guest made), emailed codes
|
|
63
|
+
(`send_email_code(email=…)` then `verify_email_code(email=…, code=…)`), and passkeys (`pb.auth.passkeys`: `register_options` / `register_verify` and
|
|
64
|
+
`authenticate_options` / `authenticate_verify` carry the WebAuthn ceremony; the device does the signing). `issue_token("billing")` trades the session for
|
|
65
|
+
a short-lived token for another service, and `jwks()` returns the public keys a server checks such tokens with.
|
|
66
|
+
|
|
67
|
+
A web server acts as the visiting user without sharing a connection's credentials:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with pb.as_user(request_token) as user_client: # same connection pool, this user's token
|
|
71
|
+
user_client.from_("orders").execute()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Files
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
avatars = pb.storage.from_("avatars")
|
|
78
|
+
avatars.upload("ada.png", open("ada.png", "rb"), content_type="image/png")
|
|
79
|
+
data = avatars.download("ada.png")
|
|
80
|
+
avatars.list(prefix="2026/"); list(avatars.list_all())
|
|
81
|
+
avatars.public_url("ada.png") # public buckets: no key needed
|
|
82
|
+
avatars.transform_url("ada.png", width=200, height=200, fit="cover", format="webp")
|
|
83
|
+
avatars.create_signed_url("report.pdf", expires_in=600) # a temporary link anyone can read
|
|
84
|
+
avatars.create_signed_upload_url("in/x.png", max_bytes=1_000_000) # a browser uploads straight to storage
|
|
85
|
+
avatars.create_share_link("report.pdf", expires_in=3600) # for people with no account (secret key)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Functions, flows, routes
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
pb.functions.invoke("orders.create", sku="A1") # branch="my-feature" runs a branch's version
|
|
92
|
+
pb.flows.invoke("welcome", email="ada@example.com")
|
|
93
|
+
pb.routes.post("orders/42/pay", method="card")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Realtime
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
async with AsyncPawabase(url, key) as pb:
|
|
100
|
+
room = pb.realtime.channel("chat:lobby", presence={"name": "Ada"})
|
|
101
|
+
room.on("message", lambda m: print(m["payload"]))
|
|
102
|
+
await room.subscribe()
|
|
103
|
+
await room.send("message", {"text": "hello"})
|
|
104
|
+
async for change in pb.realtime.channel("resource:posts"): # live record changes, as you may read them
|
|
105
|
+
print(change["event"], change["payload"]["record"])
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Publishing, presence and history also work over plain HTTP on the blocking client: `pb.realtime.publish(...)`, `.presence(channel)`, `.history(channel)`.
|
|
109
|
+
|
|
110
|
+
Without a socket: `pb.realtime.sse(["chat:lobby"])` listens over Server-Sent Events (a generator on the sync client, an async generator on the async one), and
|
|
111
|
+
`pb.realtime.publish(...)` sends. Channel rules and dashboards are managed with `pb.manage.realtime_channels` and `pb.manage.dashboards`.
|
|
112
|
+
|
|
113
|
+
## Administering users and the environment (secret key)
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
admin = Pawabase(url, "pb_sk_…", environment="production")
|
|
117
|
+
|
|
118
|
+
admin.admin.users.create(email="ada@example.com", password="…", roles=["editor"])
|
|
119
|
+
admin.admin.users.update(user_id, disabled=True)
|
|
120
|
+
admin.admin.orgs.add_member("acme", email="ada@example.com", role="admin")
|
|
121
|
+
|
|
122
|
+
admin.manage.secrets.set("STRIPE_KEY", "sk_live_…")
|
|
123
|
+
admin.manage.firewall.ban("203.0.113.7", seconds=3600)
|
|
124
|
+
admin.manage.jobs.replay(job="send_mail", since_minutes=60, dry_run=True)
|
|
125
|
+
admin.manage.observability.logs(q="timeout", minutes=15)
|
|
126
|
+
admin.manage.backups.snapshot("before-launch")
|
|
127
|
+
admin.manage.environment("staging").flows.list() # the same API for another environment the key may manage
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`pb.admin` administers the people who sign in to your application: `users`, `roles`, `orgs`, `invitations`, `sessions`, `api_keys`, `devices`, `insight`
|
|
131
|
+
(events, statistics, identity backup, hosted pages). `pb.manage` is the environment's management plane, in four families:
|
|
132
|
+
|
|
133
|
+
| Family | Services |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| Data | `resources`, `schema`, `policies`, `schemas`, `transformers`, `routes`, `database`, `runtime` |
|
|
136
|
+
| Automation | `flows`, `schedules`, `subscriptions`, `events`, `webhooks`, `inbound_hooks`, `jobs`, `functions` |
|
|
137
|
+
| Files and mail | `buckets`, `cache`, `mail`, `mail_templates` |
|
|
138
|
+
| Access | `keys`, `secrets`, `domains`, `firewall` |
|
|
139
|
+
| Observe | `observability`, `alarms`, `status_page`, `health` (service health, plan usage, audit) |
|
|
140
|
+
| Lifecycle | `environments` (previews, promote, blueprints), `releases` (branches, revisions, API versions), `backups` (backups, snapshots, restore) |
|
|
141
|
+
|
|
142
|
+
Definitions (`flows`, `policies`, `webhooks`…) share `list`, `get_one`, `create`, `replace` and `remove`, and take `branch=` to work on a feature branch.
|
|
143
|
+
Both are scoped to the client's environment; a key manages only its own.
|
|
144
|
+
|
|
145
|
+
## Errors and retries
|
|
146
|
+
|
|
147
|
+
Every failure raises `PawabaseError` (`.status_code`, `.code`, `.message`, `.details`, `.request_id`) or a subclass: `AuthenticationError` (401), `ForbiddenError`
|
|
148
|
+
(403), `QuotaExceededError` (403, the plan's limit: `details` has `limit`, `allowed`, `used`), `NotFoundError`, `ConflictError` (409), `ValidationError` (400/422),
|
|
149
|
+
`RateLimitedError`, `ServerError`, `NetworkError`. Reads are retried on connection errors and `502/503/504`; a `429` waits as the gateway asks.
|
|
150
|
+
|
|
151
|
+
BSD 3-Clause licensed.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Pawabase for Python: the client SDK.
|
|
2
|
+
|
|
3
|
+
Talk to a Pawabase environment from an app, a script or a server:
|
|
4
|
+
|
|
5
|
+
from pawabase import Pawabase
|
|
6
|
+
|
|
7
|
+
pb = Pawabase("https://api.example.com", "pb_pk_…") # AsyncPawabase for asyncio
|
|
8
|
+
pb.auth.sign_in_with_password(email="ada@example.com", password="…")
|
|
9
|
+
posts = pb.from_("posts").eq("status", "live").order("-created_at").limit(10).execute()
|
|
10
|
+
pb.storage.from_("avatars").upload("ada.png", data)
|
|
11
|
+
pb.functions.invoke("orders.create", sku="A1")
|
|
12
|
+
|
|
13
|
+
* ``pb.from_(resource)`` records: queries, create, update, delete (:mod:`pawabase.data`)
|
|
14
|
+
* ``pb.auth`` accounts, sessions, MFA, devices, API keys, organizations (:mod:`pawabase.auth`)
|
|
15
|
+
* ``pb.storage`` buckets, objects, signed links, image transforms (:mod:`pawabase.storage`)
|
|
16
|
+
* ``pb.functions`` / ``pb.flows`` / ``pb.routes`` call what you have defined (:mod:`pawabase.callables`)
|
|
17
|
+
* ``pb.realtime`` channels, presence, live record changes (:mod:`pawabase.realtime`)
|
|
18
|
+
* ``pb.admin`` manage users with a secret key (:mod:`pawabase.admin`)
|
|
19
|
+
* ``pb.manage`` configure the environment with a secret key (:mod:`pawabase.manage`)
|
|
20
|
+
|
|
21
|
+
Writing functions that run *on* Pawabase (and the ``pawabase`` command) is a separate package: ``pawabase-functions``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import pkgutil
|
|
27
|
+
|
|
28
|
+
# ``pawabase-functions`` installs its modules (``pawabase.functions``…) into this same package.
|
|
29
|
+
__path__ = pkgutil.extend_path(__path__, __name__)
|
|
30
|
+
|
|
31
|
+
from .auth import FileSessionStore, MemorySessionStore, Session, SessionStore, SignInResult # noqa: E402
|
|
32
|
+
from .client import AsyncPawabase, Pawabase # noqa: E402
|
|
33
|
+
from .data import Page, QueryError # noqa: E402
|
|
34
|
+
from .errors import ( # noqa: E402
|
|
35
|
+
AuthenticationError,
|
|
36
|
+
ConfigError,
|
|
37
|
+
ConflictError,
|
|
38
|
+
ForbiddenError,
|
|
39
|
+
NetworkError,
|
|
40
|
+
NotFoundError,
|
|
41
|
+
PawabaseError,
|
|
42
|
+
QuotaExceededError,
|
|
43
|
+
RateLimitedError,
|
|
44
|
+
RealtimeError,
|
|
45
|
+
ServerError,
|
|
46
|
+
ValidationError,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__version__ = "0.3.0"
|
|
50
|
+
__all__ = [
|
|
51
|
+
"AsyncPawabase",
|
|
52
|
+
"AuthenticationError",
|
|
53
|
+
"ConfigError",
|
|
54
|
+
"ConflictError",
|
|
55
|
+
"FileSessionStore",
|
|
56
|
+
"ForbiddenError",
|
|
57
|
+
"MemorySessionStore",
|
|
58
|
+
"NetworkError",
|
|
59
|
+
"NotFoundError",
|
|
60
|
+
"Page",
|
|
61
|
+
"Pawabase",
|
|
62
|
+
"PawabaseError",
|
|
63
|
+
"QueryError",
|
|
64
|
+
"QuotaExceededError",
|
|
65
|
+
"RateLimitedError",
|
|
66
|
+
"RealtimeError",
|
|
67
|
+
"ServerError",
|
|
68
|
+
"Session",
|
|
69
|
+
"SessionStore",
|
|
70
|
+
"SignInResult",
|
|
71
|
+
"ValidationError",
|
|
72
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Helpers shared by the blocking and the async client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .errors import ConfigError
|
|
13
|
+
|
|
14
|
+
RETRIED_STATUSES = frozenset({502, 503, 504})
|
|
15
|
+
#: A 429 means the request was *not* processed, so it is safe to wait and send it again whatever the method.
|
|
16
|
+
MAX_RATE_WAITS = 8
|
|
17
|
+
MAX_RATE_WAIT_SECONDS = 20.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def rate_wait(response: httpx.Response) -> float:
|
|
21
|
+
"""How long the gateway asked to wait (``Retry-After`` or the body's ``retry_after``), at least one second, at most 20."""
|
|
22
|
+
asked: Any = response.headers.get("retry-after")
|
|
23
|
+
if asked is None:
|
|
24
|
+
try:
|
|
25
|
+
asked = response.json().get("retry_after")
|
|
26
|
+
except (ValueError, AttributeError):
|
|
27
|
+
asked = None
|
|
28
|
+
try:
|
|
29
|
+
return min(max(float(asked), 1.0), MAX_RATE_WAIT_SECONDS)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return 1.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def then(value: Any, function: Callable[[Any], Any]) -> Any:
|
|
35
|
+
"""Apply *function* to a result that is a value (blocking client) or an awaitable (async client): the logic is written once."""
|
|
36
|
+
if inspect.isawaitable(value):
|
|
37
|
+
|
|
38
|
+
async def finish() -> Any:
|
|
39
|
+
return function(await value)
|
|
40
|
+
|
|
41
|
+
return finish()
|
|
42
|
+
return function(value)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def chain(value: Any, function: Callable[[Any], Any]) -> Any:
|
|
46
|
+
"""Like :func:`then`, for a *function* that itself returns a value or an awaitable."""
|
|
47
|
+
if inspect.isawaitable(value):
|
|
48
|
+
|
|
49
|
+
async def finish() -> Any:
|
|
50
|
+
out = function(await value)
|
|
51
|
+
return await out if inspect.isawaitable(out) else out
|
|
52
|
+
|
|
53
|
+
return finish()
|
|
54
|
+
return function(value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def path_of(*parts: Any) -> str:
|
|
58
|
+
"""Join URL path parts, quoting each (a key may contain ``/``: it stays a path)."""
|
|
59
|
+
return "/".join(quote(str(p), safe="") for p in parts)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def key_path(key: str) -> str:
|
|
63
|
+
"""An object key as a path: segments quoted, slashes kept."""
|
|
64
|
+
return "/".join(quote(segment, safe="") for segment in key.lstrip("/").split("/"))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def compact(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
68
|
+
"""*values* without the ``None``\\ s."""
|
|
69
|
+
return {k: v for k, v in values.items() if v is not None}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_secret_key(key: str) -> bool:
|
|
73
|
+
return key.startswith("pb_sk_")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def require_async(client: Any, what: str) -> None:
|
|
77
|
+
if not getattr(client, "is_async", False):
|
|
78
|
+
raise ConfigError(f"{what} needs the async client: use AsyncPawabase.")
|