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.
Files changed (34) hide show
  1. pawabase-0.3.0/.gitignore +8 -0
  2. pawabase-0.3.0/LICENSE +29 -0
  3. pawabase-0.3.0/PKG-INFO +183 -0
  4. pawabase-0.3.0/README.md +151 -0
  5. pawabase-0.3.0/pawabase/__init__.py +72 -0
  6. pawabase-0.3.0/pawabase/_core.py +78 -0
  7. pawabase-0.3.0/pawabase/admin.py +230 -0
  8. pawabase-0.3.0/pawabase/auth.py +607 -0
  9. pawabase-0.3.0/pawabase/callables.py +101 -0
  10. pawabase-0.3.0/pawabase/client.py +301 -0
  11. pawabase-0.3.0/pawabase/data.py +584 -0
  12. pawabase-0.3.0/pawabase/errors.py +131 -0
  13. pawabase-0.3.0/pawabase/manage/__init__.py +90 -0
  14. pawabase-0.3.0/pawabase/manage/_base.py +70 -0
  15. pawabase-0.3.0/pawabase/manage/access.py +158 -0
  16. pawabase-0.3.0/pawabase/manage/automation.py +144 -0
  17. pawabase-0.3.0/pawabase/manage/data.py +149 -0
  18. pawabase-0.3.0/pawabase/manage/files.py +63 -0
  19. pawabase-0.3.0/pawabase/manage/functions.py +61 -0
  20. pawabase-0.3.0/pawabase/manage/lifecycle.py +182 -0
  21. pawabase-0.3.0/pawabase/manage/mail.py +27 -0
  22. pawabase-0.3.0/pawabase/manage/observe.py +170 -0
  23. pawabase-0.3.0/pawabase/manage/realtime.py +69 -0
  24. pawabase-0.3.0/pawabase/py.typed +0 -0
  25. pawabase-0.3.0/pawabase/realtime.py +391 -0
  26. pawabase-0.3.0/pawabase/storage.py +166 -0
  27. pawabase-0.3.0/pyproject.toml +55 -0
  28. pawabase-0.3.0/tests/conftest.py +66 -0
  29. pawabase-0.3.0/tests/test_auth.py +198 -0
  30. pawabase-0.3.0/tests/test_data.py +122 -0
  31. pawabase-0.3.0/tests/test_live.py +119 -0
  32. pawabase-0.3.0/tests/test_manage_admin.py +153 -0
  33. pawabase-0.3.0/tests/test_realtime.py +130 -0
  34. pawabase-0.3.0/tests/test_storage_callables.py +84 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ *.egg-info/
5
+ .venv/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .env
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.
@@ -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.
@@ -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.")