pawabase-iac 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.
- pawabase_iac-0.1.0/.gitignore +11 -0
- pawabase_iac-0.1.0/CHANGELOG.md +11 -0
- pawabase_iac-0.1.0/LICENSE +29 -0
- pawabase_iac-0.1.0/PKG-INFO +262 -0
- pawabase_iac-0.1.0/README.md +234 -0
- pawabase_iac-0.1.0/examples/school/.gitignore +4 -0
- pawabase_iac-0.1.0/examples/school/README.md +87 -0
- pawabase_iac-0.1.0/examples/school/infra/academics.py +138 -0
- pawabase_iac-0.1.0/examples/school/infra/admissions.py +40 -0
- pawabase_iac-0.1.0/examples/school/infra/assessment.py +183 -0
- pawabase_iac-0.1.0/examples/school/infra/attendance.py +81 -0
- pawabase_iac-0.1.0/examples/school/infra/campus.py +194 -0
- pawabase_iac-0.1.0/examples/school/infra/common.py +24 -0
- pawabase_iac-0.1.0/examples/school/infra/delivery.py +118 -0
- pawabase_iac-0.1.0/examples/school/infra/finance.py +114 -0
- pawabase_iac-0.1.0/examples/school/infra/flows_academics.py +298 -0
- pawabase_iac-0.1.0/examples/school/infra/flows_campus.py +253 -0
- pawabase_iac-0.1.0/examples/school/infra/flows_finance.py +228 -0
- pawabase_iac-0.1.0/examples/school/infra/flows_portal.py +75 -0
- pawabase_iac-0.1.0/examples/school/infra/flows_students.py +230 -0
- pawabase_iac-0.1.0/examples/school/infra/people.py +154 -0
- pawabase_iac-0.1.0/examples/school/infra/policies.py +69 -0
- pawabase_iac-0.1.0/examples/school/infra/roles.py +29 -0
- pawabase_iac-0.1.0/examples/school/pawabase.toml +4 -0
- pawabase_iac-0.1.0/examples/school/requirements.txt +3 -0
- pawabase_iac-0.1.0/examples/school/scripts/smoke_test.py +359 -0
- pawabase_iac-0.1.0/examples/school/scripts/trace.py +23 -0
- pawabase_iac-0.1.0/examples/school/tests/test_project.py +80 -0
- pawabase_iac-0.1.0/examples/store/infra/flows.py +86 -0
- pawabase_iac-0.1.0/examples/store/infra/messaging.py +26 -0
- pawabase_iac-0.1.0/examples/store/infra/policies.py +7 -0
- pawabase_iac-0.1.0/examples/store/infra/tables.py +71 -0
- pawabase_iac-0.1.0/examples/store/pawabase.toml +4 -0
- pawabase_iac-0.1.0/pawabase_iac/__init__.py +30 -0
- pawabase_iac-0.1.0/pawabase_iac/_version.py +1 -0
- pawabase_iac-0.1.0/pawabase_iac/access/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/access/role.py +43 -0
- pawabase_iac-0.1.0/pawabase_iac/api/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/api/route.py +59 -0
- pawabase_iac-0.1.0/pawabase_iac/cli/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/cli/__main__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/cli/main.py +277 -0
- pawabase_iac-0.1.0/pawabase_iac/cli/starter.py +47 -0
- pawabase_iac-0.1.0/pawabase_iac/core/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/core/base.py +129 -0
- pawabase_iac-0.1.0/pawabase_iac/engine/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/engine/plan.py +164 -0
- pawabase_iac-0.1.0/pawabase_iac/engine/surface.py +41 -0
- pawabase_iac-0.1.0/pawabase_iac/environments/__init__.py +5 -0
- pawabase_iac-0.1.0/pawabase_iac/environments/base.py +47 -0
- pawabase_iac-0.1.0/pawabase_iac/environments/emulated.py +71 -0
- pawabase_iac-0.1.0/pawabase_iac/environments/remote.py +77 -0
- pawabase_iac-0.1.0/pawabase_iac/events/__init__.py +6 -0
- pawabase_iac-0.1.0/pawabase_iac/events/inbound.py +61 -0
- pawabase_iac-0.1.0/pawabase_iac/events/schedule.py +70 -0
- pawabase_iac-0.1.0/pawabase_iac/events/subscription.py +55 -0
- pawabase_iac-0.1.0/pawabase_iac/events/webhook.py +39 -0
- pawabase_iac-0.1.0/pawabase_iac/expr/__init__.py +6 -0
- pawabase_iac-0.1.0/pawabase_iac/expr/flow.py +147 -0
- pawabase_iac-0.1.0/pawabase_iac/expr/policy.py +218 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/__init__.py +7 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/block.py +71 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/__init__.py +34 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/auth.py +42 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/cache.py +58 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/control.py +130 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/crypto.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/db.py +34 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/error.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/event.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/flow.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/http.py +28 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/json.py +34 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/log.py +27 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/mail.py +27 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/math.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/metric.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/policy.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/queue.py +25 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/realtime.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/resource.py +75 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/response.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/secret.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/storage.py +63 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/time.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/transform.py +159 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/trigger.py +116 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/util.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/validate.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/blocks/webhook.py +23 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/catalogue.py +576 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/flow.py +132 -0
- pawabase_iac-0.1.0/pawabase_iac/flows/graph.py +159 -0
- pawabase_iac-0.1.0/pawabase_iac/kinds.py +19 -0
- pawabase_iac-0.1.0/pawabase_iac/mail/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/mail/template.py +43 -0
- pawabase_iac-0.1.0/pawabase_iac/policies/__init__.py +4 -0
- pawabase_iac-0.1.0/pawabase_iac/policies/check.py +88 -0
- pawabase_iac-0.1.0/pawabase_iac/policies/policy.py +25 -0
- pawabase_iac-0.1.0/pawabase_iac/policies/transformer.py +51 -0
- pawabase_iac-0.1.0/pawabase_iac/project/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/project/config.py +109 -0
- pawabase_iac-0.1.0/pawabase_iac/py.typed +0 -0
- pawabase_iac-0.1.0/pawabase_iac/storage/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/storage/bucket.py +50 -0
- pawabase_iac-0.1.0/pawabase_iac/tables/__init__.py +11 -0
- pawabase_iac-0.1.0/pawabase_iac/tables/check.py +107 -0
- pawabase_iac-0.1.0/pawabase_iac/tables/fields.py +170 -0
- pawabase_iac-0.1.0/pawabase_iac/tables/schema.py +24 -0
- pawabase_iac-0.1.0/pawabase_iac/tables/table.py +86 -0
- pawabase_iac-0.1.0/pawabase_iac/validate/__init__.py +3 -0
- pawabase_iac-0.1.0/pawabase_iac/validate/refs.py +117 -0
- pawabase_iac-0.1.0/pyproject.toml +56 -0
- pawabase_iac-0.1.0/scripts/generate_blocks.py +163 -0
- pawabase_iac-0.1.0/tests/conftest.py +10 -0
- pawabase_iac-0.1.0/tests/test_cli.py +52 -0
- pawabase_iac-0.1.0/tests/test_engine.py +113 -0
- pawabase_iac-0.1.0/tests/test_events.py +61 -0
- pawabase_iac-0.1.0/tests/test_flows.py +123 -0
- pawabase_iac-0.1.0/tests/test_policy.py +34 -0
- pawabase_iac-0.1.0/tests/test_readme.py +24 -0
- pawabase_iac-0.1.0/tests/test_server_conformance.py +83 -0
- pawabase_iac-0.1.0/tests/test_tables.py +91 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
* Define a Pawabase backend as Python: tables (typed fields, constraints, relations, indexes), schemas, transformers, roles, policies (as Python expressions), flows, routes, subscriptions, schedules, webhooks, inbound hooks, buckets and mail templates.
|
|
8
|
+
* A flow builder over all 69 server flow blocks, generated from the server's registry, with every option typed and documented.
|
|
9
|
+
* Offline validation: each definition, template syntax, block settings, flow graphs, and the references between definitions.
|
|
10
|
+
* `pawabase validate | plan | deploy | emulate | init | whoami`; merge and override modes; deploys need a secret API key (a deploy key is enough); table migrations after changes; secrets only through `env()`.
|
|
11
|
+
* Conformance tests against the server's own request models and flow validator.
|
|
@@ -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,262 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pawabase-iac
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Define a whole Pawabase backend as Python code, validate it, plan it, emulate it and deploy it.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Pawabase/pawabase-iac
|
|
6
|
+
Project-URL: Source, https://github.com/Pawabase/pawabase-iac
|
|
7
|
+
Project-URL: Issues, https://github.com/Pawabase/pawabase-iac/issues
|
|
8
|
+
Project-URL: Pawabase, https://github.com/Pawabase/pawabase
|
|
9
|
+
Author: Pawabase contributors
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: backend,cli,deploy,iac,infrastructure-as-code,pawabase,policies,workflows
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pawabase-iac
|
|
30
|
+
|
|
31
|
+
**Pawabase as code.** Define a whole [Pawabase](https://github.com/Pawabase/pawabase) backend in Python (tables, access policies, flows, routes, events, schedules, webhooks, buckets, email templates, roles), check it before anything is sent, see what a deploy would change, rehearse it against a simulated environment, and deploy it with an API key.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install pawabase-iac
|
|
35
|
+
|
|
36
|
+
pawabase init # pawabase.toml, infra/app.py, .env.example
|
|
37
|
+
pawabase validate # offline: every definition and every reference between them
|
|
38
|
+
pawabase emulate # a pretend deploy: nothing real is touched, no key needed
|
|
39
|
+
pawabase plan # what a deploy would change in the real environment (reads only)
|
|
40
|
+
pawabase deploy # apply it; needs a deploy API key
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
* **Everything is code.** A table, a policy, a flow with branches and loops, a cron schedule: all Python you can review, test and version.
|
|
44
|
+
* **Checked before it is sent.** Names, field types, constraints, policy conditions, flow graphs, template syntax, block settings and the references between all of them are validated offline, with the server's own rules.
|
|
45
|
+
* **A plan before a change.** `plan` and `deploy --dry-run` show what would be created (`+`), changed (`~`, with the fields that differ) or removed (`-`).
|
|
46
|
+
* **Safe by default.** Pushing needs a secret API key; `merge` mode never deletes what your code does not mention; production asks first; secrets never sit in the code.
|
|
47
|
+
|
|
48
|
+
## Contents
|
|
49
|
+
|
|
50
|
+
[Quick start](#quick-start) · [Defining things](#defining-things) · [Policies](#policies) · [Flows](#flows) · [Commands](#commands) · [Deploying](#deploying) · [Emulate](#emulate) · [Using it in CI](#using-it-in-ci) · [Examples](#examples) · [Development](#development) · [Limits](#limits)
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mkdir shop && cd shop
|
|
56
|
+
pawabase init
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`pawabase.toml` says where to deploy (the key does not live here):
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
url = "http://localhost:8080" # your Pawabase gateway
|
|
63
|
+
environment = "development" # the environment to deploy to
|
|
64
|
+
entry = "infra" # a file or a folder of .py files holding your definitions
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`infra/app.py` is a small working backend. Make the API key (Studio → API keys → New key → **Secret**, preset **Deploy and configure**) and put it in `.env`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
echo 'PAWABASE_API_KEY=pb_sk_...' >> .env
|
|
71
|
+
pawabase validate && pawabase emulate && pawabase deploy
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Edit a definition and run `pawabase plan` to see exactly what changes before you deploy it.
|
|
75
|
+
|
|
76
|
+
## Defining things
|
|
77
|
+
|
|
78
|
+
Definitions register themselves as they are made. Files in your `entry` folder can import each other (`from infra.policies import mine`), and each file runs once. Pass a definition wherever a name is expected (`resource=todos`, `handler=summary`, `run=nightly`, `operations=crud(mine)`) and it is replaced by its name, so a rename follows and `validate` can see the dependency.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from pawabase_iac import (
|
|
82
|
+
Allow, Array, BelongsTo, Boolean, Desc, Email, Index, Integer, P, Policy, RateLimit, Role, String, Table, Where, crud,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
Role("teacher", permissions=["grades.write"], description="Teaching staff")
|
|
86
|
+
|
|
87
|
+
mine = Policy("mine", P.owner("user_id"))
|
|
88
|
+
staff = Policy("staff", P.role("admin") | P.role("teacher"))
|
|
89
|
+
|
|
90
|
+
students = Table(
|
|
91
|
+
"students",
|
|
92
|
+
fields=[
|
|
93
|
+
String("user_id", unique=True),
|
|
94
|
+
String("admission_number", required=True, unique=True, max_length=20),
|
|
95
|
+
Email("email"),
|
|
96
|
+
Integer("age", minimum=3, maximum=25),
|
|
97
|
+
String("status", enum=["active", "graduated", "withdrawn"], default="active"),
|
|
98
|
+
Array("guardian_ids", of=String("guardian_id")),
|
|
99
|
+
],
|
|
100
|
+
operations=crud(staff, get=Allow(mine.condition | staff.condition), delete=Allow(P.role("admin"))),
|
|
101
|
+
indexes=[Index("status", Desc("age"), where=[Where("status", "!=", "withdrawn")])],
|
|
102
|
+
rate_limit=RateLimit(120, 60),
|
|
103
|
+
realtime=True,
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Kind | Class | What you set |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| Table | `Table` (also `Resource`) | typed fields, operations with a policy each, relations (`BelongsTo`, `HasMany`), indexes (`Index`, `Desc`, `Where`), rate limit, caching, owner field, realtime, event capture |
|
|
110
|
+
| Fields | `String`, `Text`, `Integer`, `Number`, `Boolean`, `DateTime`, `Date`, `UUID`, `ULID`, `Email`, `URL`, `JSON`, `Array`, `Object`, `Ref` | `required`, `nullable`, `default`, `unique`, `indexed`, `enum`, `min_length`/`max_length`, `minimum`/`maximum`, `pattern`, `read_only`, `write_only`, nested fields and items |
|
|
111
|
+
| Shapes | `Schema`, `Transformer` | reusable data shapes; omit / pick / set / rename / case on the way out |
|
|
112
|
+
| Access | `Role`, `Policy` | roles and their permission names; policies as Python expressions |
|
|
113
|
+
| Logic | `Flow`, `Route` | a graph of blocks; a custom HTTP endpoint that runs a flow |
|
|
114
|
+
| Events | `Subscription`, `Schedule`, `Webhook`, `InboundHook` | react to events, run on cron or an interval, call out, be called (signed) |
|
|
115
|
+
| Files | `Bucket` | per-action access policies, accepted types, size, signed URLs, image transforms, caching |
|
|
116
|
+
| Email | `MailTemplate` | subject / html / text with `{{ variables }}`, status, categories |
|
|
117
|
+
|
|
118
|
+
Secrets are never written in code: `Webhook(..., secret=env("ERP_SECRET"))` reads the variable when you deploy and refuses to deploy if it is unset.
|
|
119
|
+
|
|
120
|
+
## Policies
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from pawabase_iac import P, Policy, all_of, any_of
|
|
124
|
+
|
|
125
|
+
Policy("owner_or_staff", P.owner("user_id") | P.role("admin"))
|
|
126
|
+
Policy("paid_and_verified", P.authenticated() & (P.auth.plan != "free") & P.auth.email_verified.truthy())
|
|
127
|
+
Policy("family", (P.record.student_user_id == P.auth.user_id) | P.record.guardian_ids.contains(P.auth.user_id))
|
|
128
|
+
Policy("not_suspended", ~P.record.suspended.truthy())
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`P.auth`, `P.record`, `P.input`, `P.request`, `P.env` and `P.credential` are paths into what a policy can see. Comparing a path builds a condition (`==`, `!=`, `>`, `>=`, `<`, `<=`, `.in_()`, `.not_in()`, `.contains()`, `.startswith()`, `.endswith()`, `.matches()`, `.exists()`, `.truthy()`, `.empty()`); `&`, `|` and `~` combine them. The shorthand checks are `P.authenticated()`, `P.role()`, `P.permission()`, `P.owner()`, `P.service()`, `P.kind()`, `P.scope()` and `P.aal()`. A condition has no truth value in Python, so `and` / `or` / `not` raise instead of silently dropping half a policy.
|
|
132
|
+
|
|
133
|
+
## Flows
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from pawabase_iac import Flow, P, blocks, ctx
|
|
137
|
+
|
|
138
|
+
invoice = Flow("issue_invoice", description="Invoice a student")
|
|
139
|
+
start = invoice.add(blocks.trigger.http(method="POST", policy="finance_staff").named("start"))
|
|
140
|
+
student = invoice.add(blocks.resource.get(resource="students", id=ctx.input.student_id).named("student"))
|
|
141
|
+
missing = invoice.add(blocks.error.raise_(status=404, message="No such student").named("missing"))
|
|
142
|
+
fees = invoice.add(blocks.resource.list(resource="fee_structures", filters={"grade": ctx.steps.student.output.grade}, limit=1).named("fees"))
|
|
143
|
+
total = invoice.add(blocks.transform.summarize(value=ctx.steps.fees.output.data[0].items, operation="sum", field="amount").named("total"))
|
|
144
|
+
big = invoice.add(blocks.control.if_(P.steps.total.output > 100000).named("big"))
|
|
145
|
+
review = invoice.add(blocks.event.emit(event="invoice.needs_review", payload={"total": ctx.steps.total.output}).named("review"))
|
|
146
|
+
done = invoice.add(blocks.response.return_(status=201, body={"total": ctx.steps.total.output}).named("done"))
|
|
147
|
+
|
|
148
|
+
start >> student
|
|
149
|
+
student.missing >> missing
|
|
150
|
+
student >> fees >> total >> big
|
|
151
|
+
big.true >> review >> done
|
|
152
|
+
big.false >> done
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`a >> b` follows the `next` handle; `a.true >> b`, `a.error >> b` or `a.on("done") >> b` follow others. `flow.chain(a, b, c)` adds and links in one go; `flow["id"]` returns a node you named with `.named("id")`.
|
|
156
|
+
|
|
157
|
+
All **69 flow blocks** the server knows are available as typed functions, one module per family, every option documented with its default, limits and allowed values: `auth` · `cache` · `control` (if, switch, foreach, while, retry, timeout, set, merge, delay, stop) · `crypto` · `db` · `error` · `event` · `flow` · `http` · `json` · `log` · `mail` · `math` · `metric` · `policy` · `queue` · `realtime` · `resource` · `response` · `secret` · `storage` · `time` · `transform` (map, filter, sort, group, unique, flatten, slice, batch, pick, rename, summarize, template) · `trigger` (http, event, schedule, webhook, resource, job, realtime, manual) · `util` · `validate` · `webhook`.
|
|
158
|
+
|
|
159
|
+
### The rules the server enforces (and `validate` checks)
|
|
160
|
+
|
|
161
|
+
* **Templates are lookups.** `{{ steps.load.output.id }}` and the filters `default`, `upper`, `lower`, `length`, `int`, `float`, `str`, `bool`, `first`, `last`, `keys`, `json`; nothing else. No arithmetic, no comparison, no path after a filter. Compute with `blocks.math.calculate`; decide with `blocks.control.if_(P.steps.x.output.n > 3)`. A value that is exactly one template keeps its type (a list stays a list).
|
|
162
|
+
* **Conditions use `P`** (`control.if_`, `control.while_`, `transform.filter`): `P.input.x`, `P.steps.<id>.output.y`, `P.vars.z`, `P.item.w`.
|
|
163
|
+
* **What `input` holds** depends on how the flow started: an HTTP flow gets the body (`POST /flows/v1/<name>` for a trigger with no `path`) or `{params, query, body, headers}` (a `Route`); an event flow gets `{"event": {name, payload}}` (a table's changes are `<table>.created|updated|deleted` with the row at `payload.record`); a schedule gets `{scheduled_at}` or its payload; an inbound hook gets `{hook, headers, body}`.
|
|
164
|
+
* **Loops.** `ctx.item` and `ctx.index` are the current element and a nested loop shadows the outer `item`: keep what you need with `blocks.control.set(values={...})` and read it from `ctx.vars`. A loop's output is the list of its body's last outputs.
|
|
165
|
+
* **`validate.schema` is strict**: declare every optional input as well, or it is refused.
|
|
166
|
+
* **Debug a deployed flow** with `POST /platform/v1/envs/<env>/flows/<name>/run {"input": {...}}`: it returns the result, or a step-by-step trace.
|
|
167
|
+
|
|
168
|
+
## Commands
|
|
169
|
+
|
|
170
|
+
| Command | |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `pawabase init` | start a project here |
|
|
173
|
+
| `pawabase validate` | check every definition and reference; contacts nothing |
|
|
174
|
+
| `pawabase plan` | compare your code with the environment; writes nothing |
|
|
175
|
+
| `pawabase deploy` | apply the plan (`--dry-run`, `--yes`, `--mode`, `--force-secrets`, `--no-migrate`) |
|
|
176
|
+
| `pawabase emulate` | rehearse a deploy against a simulated environment (`--reset`) |
|
|
177
|
+
| `pawabase whoami` | show the settings and test the API key |
|
|
178
|
+
|
|
179
|
+
Shared options: `-e/--env`, `--url`, `--entry`, `--json`. Exit code 0 on success, 1 on a failure or a refusal, 2 when `--yes` was needed but not given.
|
|
180
|
+
|
|
181
|
+
## Deploying
|
|
182
|
+
|
|
183
|
+
**A key is required to push.** `deploy` refuses without `PAWABASE_API_KEY` (in `.env` or the environment), and accepts only a *secret* key (`pb_sk…`). Use a **deploy key**: Studio → API keys → New key → Secret → preset *Deploy and configure*. It may change tables, policies, routes, flows, mail templates, roles, buckets, webhooks, hooks, subscriptions and schedules, and nothing else: not users, not API keys, not the database console, not the environment itself. The gateway refuses every other request from it.
|
|
184
|
+
|
|
185
|
+
**Modes.**
|
|
186
|
+
|
|
187
|
+
* `--mode merge` (default) creates and changes what your code defines and leaves everything else alone. Anything in the environment that your code does not mention is shown as `?` in the plan.
|
|
188
|
+
* `--mode override` makes the environment match your code exactly and **deletes the rest**. It asks first (or needs `--yes`).
|
|
189
|
+
|
|
190
|
+
**What happens.** Everything is validated first; if an `env("…")` variable is unset nothing is sent. Definitions are applied in dependency order (roles, schemas, transformers, policies, tables, flows, buckets, templates, routes, subscriptions, webhooks, hooks, schedules). After a table is created or changed its table is migrated: columns are created or **added**, never dropped. The deploy stops at the first refusal and tells you how many later changes were not attempted; what was applied stays applied, so fix it and deploy again. Production environments (`production`, `prod`, `live`) need `--yes` when not interactive.
|
|
191
|
+
|
|
192
|
+
**Secrets.** `Webhook` and `InboundHook` secrets are write-only: sent when the definition is created or otherwise changes, never compared. Rotate one with `--force-secrets`.
|
|
193
|
+
|
|
194
|
+
**How it compares.** The server fills in defaults and ids, so the comparison asks only whether what you wrote is present in what the server returns (an empty value matches a missing one). Fields the server derives when unset (a table's `table` name) are not compared.
|
|
195
|
+
|
|
196
|
+
## Emulate
|
|
197
|
+
|
|
198
|
+
`pawabase emulate` runs the same validation and planning against a **simulated** environment kept in `.pawabase/emulator-<env>.json`. It never touches a server and needs no key. It prints the plan, applies it to the pretend environment, and shows what the result would serve: every REST endpoint with its policy, custom routes, hooks, schedules, webhooks, subscriptions and buckets. Run it again after editing to see only what changed; `--reset` starts empty; `--mode override` shows what a destructive deploy would remove.
|
|
199
|
+
|
|
200
|
+
## Using it in CI
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
- run: pip install pawabase-iac
|
|
204
|
+
- run: pawabase validate # on every pull request
|
|
205
|
+
- run: pawabase plan # show the diff in the log
|
|
206
|
+
env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }} }
|
|
207
|
+
- run: pawabase deploy --yes # on merge to main
|
|
208
|
+
env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }}, ERP_SECRET: ${{ secrets.ERP_SECRET }} }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`PAWABASE_URL` and `PAWABASE_ENV` override `pawabase.toml`.
|
|
212
|
+
|
|
213
|
+
## Examples
|
|
214
|
+
|
|
215
|
+
* [`examples/store`](examples/store): a small shop (tables, a checkout flow with a loop, retry and error paths, a receipt flow, switch-based ticket routing, subscriptions, a webhook, a signed inbound hook, buckets, a mail template).
|
|
216
|
+
* [`examples/school`](examples/school): a complete **school management system**: 11 roles, 22 policies, 43 tables, 25 flows, routes, subscriptions, schedules, signed payment webhooks, buckets and email templates (136 definitions) with a smoke test that runs the real flows against a deployed environment and its own offline tests.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install -e '.[test]'
|
|
222
|
+
pytest # the library, offline
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The conformance tests compare every definition's request body with the server's own request models, run flows through the server's flow validator and check the generated block catalogue against the running server's. They run where `pawabase_server` is importable:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
cd ../pawabase && PYTHONPATH=../pawabase-iac uv run python -m pytest ../pawabase-iac/tests/test_server_conformance.py
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Regenerate the flow blocks when the server gains some:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cd ../pawabase && uv run python ../pawabase-iac/scripts/generate_blocks.py
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
pawabase_iac/
|
|
239
|
+
core/ base Definition, env(), the registry
|
|
240
|
+
expr/ P (policy conditions) and ctx (flow templates)
|
|
241
|
+
tables/ fields, Table, Schema, indexes, relations
|
|
242
|
+
policies/ Policy, Transformer, condition checks
|
|
243
|
+
access/ Role
|
|
244
|
+
flows/ Flow builder, graph checks, layout, catalogue.py and blocks/ (generated)
|
|
245
|
+
api/ Route
|
|
246
|
+
events/ Subscription, Schedule, Webhook, InboundHook
|
|
247
|
+
storage/ Bucket
|
|
248
|
+
mail/ MailTemplate
|
|
249
|
+
validate/ cross-definition reference checks
|
|
250
|
+
engine/ plan, apply, diff, what-it-serves
|
|
251
|
+
environments/ Remote (the Platform API) and Emulated (in memory)
|
|
252
|
+
project/ pawabase.toml, .env, importing your definitions
|
|
253
|
+
cli/ the pawabase command
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Limits
|
|
257
|
+
|
|
258
|
+
Environment-level settings are not managed from code yet: auth providers and settings, secrets, API keys, firewall rules, domains, alarms, dashboards, backup schedules, and mail and storage connection settings. Users are not managed either (give people roles in Accounts). The twelve kinds above are.
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# pawabase-iac
|
|
2
|
+
|
|
3
|
+
**Pawabase as code.** Define a whole [Pawabase](https://github.com/Pawabase/pawabase) backend in Python (tables, access policies, flows, routes, events, schedules, webhooks, buckets, email templates, roles), check it before anything is sent, see what a deploy would change, rehearse it against a simulated environment, and deploy it with an API key.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install pawabase-iac
|
|
7
|
+
|
|
8
|
+
pawabase init # pawabase.toml, infra/app.py, .env.example
|
|
9
|
+
pawabase validate # offline: every definition and every reference between them
|
|
10
|
+
pawabase emulate # a pretend deploy: nothing real is touched, no key needed
|
|
11
|
+
pawabase plan # what a deploy would change in the real environment (reads only)
|
|
12
|
+
pawabase deploy # apply it; needs a deploy API key
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
* **Everything is code.** A table, a policy, a flow with branches and loops, a cron schedule: all Python you can review, test and version.
|
|
16
|
+
* **Checked before it is sent.** Names, field types, constraints, policy conditions, flow graphs, template syntax, block settings and the references between all of them are validated offline, with the server's own rules.
|
|
17
|
+
* **A plan before a change.** `plan` and `deploy --dry-run` show what would be created (`+`), changed (`~`, with the fields that differ) or removed (`-`).
|
|
18
|
+
* **Safe by default.** Pushing needs a secret API key; `merge` mode never deletes what your code does not mention; production asks first; secrets never sit in the code.
|
|
19
|
+
|
|
20
|
+
## Contents
|
|
21
|
+
|
|
22
|
+
[Quick start](#quick-start) · [Defining things](#defining-things) · [Policies](#policies) · [Flows](#flows) · [Commands](#commands) · [Deploying](#deploying) · [Emulate](#emulate) · [Using it in CI](#using-it-in-ci) · [Examples](#examples) · [Development](#development) · [Limits](#limits)
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
mkdir shop && cd shop
|
|
28
|
+
pawabase init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`pawabase.toml` says where to deploy (the key does not live here):
|
|
32
|
+
|
|
33
|
+
```toml
|
|
34
|
+
url = "http://localhost:8080" # your Pawabase gateway
|
|
35
|
+
environment = "development" # the environment to deploy to
|
|
36
|
+
entry = "infra" # a file or a folder of .py files holding your definitions
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`infra/app.py` is a small working backend. Make the API key (Studio → API keys → New key → **Secret**, preset **Deploy and configure**) and put it in `.env`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
echo 'PAWABASE_API_KEY=pb_sk_...' >> .env
|
|
43
|
+
pawabase validate && pawabase emulate && pawabase deploy
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Edit a definition and run `pawabase plan` to see exactly what changes before you deploy it.
|
|
47
|
+
|
|
48
|
+
## Defining things
|
|
49
|
+
|
|
50
|
+
Definitions register themselves as they are made. Files in your `entry` folder can import each other (`from infra.policies import mine`), and each file runs once. Pass a definition wherever a name is expected (`resource=todos`, `handler=summary`, `run=nightly`, `operations=crud(mine)`) and it is replaced by its name, so a rename follows and `validate` can see the dependency.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from pawabase_iac import (
|
|
54
|
+
Allow, Array, BelongsTo, Boolean, Desc, Email, Index, Integer, P, Policy, RateLimit, Role, String, Table, Where, crud,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
Role("teacher", permissions=["grades.write"], description="Teaching staff")
|
|
58
|
+
|
|
59
|
+
mine = Policy("mine", P.owner("user_id"))
|
|
60
|
+
staff = Policy("staff", P.role("admin") | P.role("teacher"))
|
|
61
|
+
|
|
62
|
+
students = Table(
|
|
63
|
+
"students",
|
|
64
|
+
fields=[
|
|
65
|
+
String("user_id", unique=True),
|
|
66
|
+
String("admission_number", required=True, unique=True, max_length=20),
|
|
67
|
+
Email("email"),
|
|
68
|
+
Integer("age", minimum=3, maximum=25),
|
|
69
|
+
String("status", enum=["active", "graduated", "withdrawn"], default="active"),
|
|
70
|
+
Array("guardian_ids", of=String("guardian_id")),
|
|
71
|
+
],
|
|
72
|
+
operations=crud(staff, get=Allow(mine.condition | staff.condition), delete=Allow(P.role("admin"))),
|
|
73
|
+
indexes=[Index("status", Desc("age"), where=[Where("status", "!=", "withdrawn")])],
|
|
74
|
+
rate_limit=RateLimit(120, 60),
|
|
75
|
+
realtime=True,
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Kind | Class | What you set |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Table | `Table` (also `Resource`) | typed fields, operations with a policy each, relations (`BelongsTo`, `HasMany`), indexes (`Index`, `Desc`, `Where`), rate limit, caching, owner field, realtime, event capture |
|
|
82
|
+
| Fields | `String`, `Text`, `Integer`, `Number`, `Boolean`, `DateTime`, `Date`, `UUID`, `ULID`, `Email`, `URL`, `JSON`, `Array`, `Object`, `Ref` | `required`, `nullable`, `default`, `unique`, `indexed`, `enum`, `min_length`/`max_length`, `minimum`/`maximum`, `pattern`, `read_only`, `write_only`, nested fields and items |
|
|
83
|
+
| Shapes | `Schema`, `Transformer` | reusable data shapes; omit / pick / set / rename / case on the way out |
|
|
84
|
+
| Access | `Role`, `Policy` | roles and their permission names; policies as Python expressions |
|
|
85
|
+
| Logic | `Flow`, `Route` | a graph of blocks; a custom HTTP endpoint that runs a flow |
|
|
86
|
+
| Events | `Subscription`, `Schedule`, `Webhook`, `InboundHook` | react to events, run on cron or an interval, call out, be called (signed) |
|
|
87
|
+
| Files | `Bucket` | per-action access policies, accepted types, size, signed URLs, image transforms, caching |
|
|
88
|
+
| Email | `MailTemplate` | subject / html / text with `{{ variables }}`, status, categories |
|
|
89
|
+
|
|
90
|
+
Secrets are never written in code: `Webhook(..., secret=env("ERP_SECRET"))` reads the variable when you deploy and refuses to deploy if it is unset.
|
|
91
|
+
|
|
92
|
+
## Policies
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from pawabase_iac import P, Policy, all_of, any_of
|
|
96
|
+
|
|
97
|
+
Policy("owner_or_staff", P.owner("user_id") | P.role("admin"))
|
|
98
|
+
Policy("paid_and_verified", P.authenticated() & (P.auth.plan != "free") & P.auth.email_verified.truthy())
|
|
99
|
+
Policy("family", (P.record.student_user_id == P.auth.user_id) | P.record.guardian_ids.contains(P.auth.user_id))
|
|
100
|
+
Policy("not_suspended", ~P.record.suspended.truthy())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`P.auth`, `P.record`, `P.input`, `P.request`, `P.env` and `P.credential` are paths into what a policy can see. Comparing a path builds a condition (`==`, `!=`, `>`, `>=`, `<`, `<=`, `.in_()`, `.not_in()`, `.contains()`, `.startswith()`, `.endswith()`, `.matches()`, `.exists()`, `.truthy()`, `.empty()`); `&`, `|` and `~` combine them. The shorthand checks are `P.authenticated()`, `P.role()`, `P.permission()`, `P.owner()`, `P.service()`, `P.kind()`, `P.scope()` and `P.aal()`. A condition has no truth value in Python, so `and` / `or` / `not` raise instead of silently dropping half a policy.
|
|
104
|
+
|
|
105
|
+
## Flows
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from pawabase_iac import Flow, P, blocks, ctx
|
|
109
|
+
|
|
110
|
+
invoice = Flow("issue_invoice", description="Invoice a student")
|
|
111
|
+
start = invoice.add(blocks.trigger.http(method="POST", policy="finance_staff").named("start"))
|
|
112
|
+
student = invoice.add(blocks.resource.get(resource="students", id=ctx.input.student_id).named("student"))
|
|
113
|
+
missing = invoice.add(blocks.error.raise_(status=404, message="No such student").named("missing"))
|
|
114
|
+
fees = invoice.add(blocks.resource.list(resource="fee_structures", filters={"grade": ctx.steps.student.output.grade}, limit=1).named("fees"))
|
|
115
|
+
total = invoice.add(blocks.transform.summarize(value=ctx.steps.fees.output.data[0].items, operation="sum", field="amount").named("total"))
|
|
116
|
+
big = invoice.add(blocks.control.if_(P.steps.total.output > 100000).named("big"))
|
|
117
|
+
review = invoice.add(blocks.event.emit(event="invoice.needs_review", payload={"total": ctx.steps.total.output}).named("review"))
|
|
118
|
+
done = invoice.add(blocks.response.return_(status=201, body={"total": ctx.steps.total.output}).named("done"))
|
|
119
|
+
|
|
120
|
+
start >> student
|
|
121
|
+
student.missing >> missing
|
|
122
|
+
student >> fees >> total >> big
|
|
123
|
+
big.true >> review >> done
|
|
124
|
+
big.false >> done
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`a >> b` follows the `next` handle; `a.true >> b`, `a.error >> b` or `a.on("done") >> b` follow others. `flow.chain(a, b, c)` adds and links in one go; `flow["id"]` returns a node you named with `.named("id")`.
|
|
128
|
+
|
|
129
|
+
All **69 flow blocks** the server knows are available as typed functions, one module per family, every option documented with its default, limits and allowed values: `auth` · `cache` · `control` (if, switch, foreach, while, retry, timeout, set, merge, delay, stop) · `crypto` · `db` · `error` · `event` · `flow` · `http` · `json` · `log` · `mail` · `math` · `metric` · `policy` · `queue` · `realtime` · `resource` · `response` · `secret` · `storage` · `time` · `transform` (map, filter, sort, group, unique, flatten, slice, batch, pick, rename, summarize, template) · `trigger` (http, event, schedule, webhook, resource, job, realtime, manual) · `util` · `validate` · `webhook`.
|
|
130
|
+
|
|
131
|
+
### The rules the server enforces (and `validate` checks)
|
|
132
|
+
|
|
133
|
+
* **Templates are lookups.** `{{ steps.load.output.id }}` and the filters `default`, `upper`, `lower`, `length`, `int`, `float`, `str`, `bool`, `first`, `last`, `keys`, `json`; nothing else. No arithmetic, no comparison, no path after a filter. Compute with `blocks.math.calculate`; decide with `blocks.control.if_(P.steps.x.output.n > 3)`. A value that is exactly one template keeps its type (a list stays a list).
|
|
134
|
+
* **Conditions use `P`** (`control.if_`, `control.while_`, `transform.filter`): `P.input.x`, `P.steps.<id>.output.y`, `P.vars.z`, `P.item.w`.
|
|
135
|
+
* **What `input` holds** depends on how the flow started: an HTTP flow gets the body (`POST /flows/v1/<name>` for a trigger with no `path`) or `{params, query, body, headers}` (a `Route`); an event flow gets `{"event": {name, payload}}` (a table's changes are `<table>.created|updated|deleted` with the row at `payload.record`); a schedule gets `{scheduled_at}` or its payload; an inbound hook gets `{hook, headers, body}`.
|
|
136
|
+
* **Loops.** `ctx.item` and `ctx.index` are the current element and a nested loop shadows the outer `item`: keep what you need with `blocks.control.set(values={...})` and read it from `ctx.vars`. A loop's output is the list of its body's last outputs.
|
|
137
|
+
* **`validate.schema` is strict**: declare every optional input as well, or it is refused.
|
|
138
|
+
* **Debug a deployed flow** with `POST /platform/v1/envs/<env>/flows/<name>/run {"input": {...}}`: it returns the result, or a step-by-step trace.
|
|
139
|
+
|
|
140
|
+
## Commands
|
|
141
|
+
|
|
142
|
+
| Command | |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `pawabase init` | start a project here |
|
|
145
|
+
| `pawabase validate` | check every definition and reference; contacts nothing |
|
|
146
|
+
| `pawabase plan` | compare your code with the environment; writes nothing |
|
|
147
|
+
| `pawabase deploy` | apply the plan (`--dry-run`, `--yes`, `--mode`, `--force-secrets`, `--no-migrate`) |
|
|
148
|
+
| `pawabase emulate` | rehearse a deploy against a simulated environment (`--reset`) |
|
|
149
|
+
| `pawabase whoami` | show the settings and test the API key |
|
|
150
|
+
|
|
151
|
+
Shared options: `-e/--env`, `--url`, `--entry`, `--json`. Exit code 0 on success, 1 on a failure or a refusal, 2 when `--yes` was needed but not given.
|
|
152
|
+
|
|
153
|
+
## Deploying
|
|
154
|
+
|
|
155
|
+
**A key is required to push.** `deploy` refuses without `PAWABASE_API_KEY` (in `.env` or the environment), and accepts only a *secret* key (`pb_sk…`). Use a **deploy key**: Studio → API keys → New key → Secret → preset *Deploy and configure*. It may change tables, policies, routes, flows, mail templates, roles, buckets, webhooks, hooks, subscriptions and schedules, and nothing else: not users, not API keys, not the database console, not the environment itself. The gateway refuses every other request from it.
|
|
156
|
+
|
|
157
|
+
**Modes.**
|
|
158
|
+
|
|
159
|
+
* `--mode merge` (default) creates and changes what your code defines and leaves everything else alone. Anything in the environment that your code does not mention is shown as `?` in the plan.
|
|
160
|
+
* `--mode override` makes the environment match your code exactly and **deletes the rest**. It asks first (or needs `--yes`).
|
|
161
|
+
|
|
162
|
+
**What happens.** Everything is validated first; if an `env("…")` variable is unset nothing is sent. Definitions are applied in dependency order (roles, schemas, transformers, policies, tables, flows, buckets, templates, routes, subscriptions, webhooks, hooks, schedules). After a table is created or changed its table is migrated: columns are created or **added**, never dropped. The deploy stops at the first refusal and tells you how many later changes were not attempted; what was applied stays applied, so fix it and deploy again. Production environments (`production`, `prod`, `live`) need `--yes` when not interactive.
|
|
163
|
+
|
|
164
|
+
**Secrets.** `Webhook` and `InboundHook` secrets are write-only: sent when the definition is created or otherwise changes, never compared. Rotate one with `--force-secrets`.
|
|
165
|
+
|
|
166
|
+
**How it compares.** The server fills in defaults and ids, so the comparison asks only whether what you wrote is present in what the server returns (an empty value matches a missing one). Fields the server derives when unset (a table's `table` name) are not compared.
|
|
167
|
+
|
|
168
|
+
## Emulate
|
|
169
|
+
|
|
170
|
+
`pawabase emulate` runs the same validation and planning against a **simulated** environment kept in `.pawabase/emulator-<env>.json`. It never touches a server and needs no key. It prints the plan, applies it to the pretend environment, and shows what the result would serve: every REST endpoint with its policy, custom routes, hooks, schedules, webhooks, subscriptions and buckets. Run it again after editing to see only what changed; `--reset` starts empty; `--mode override` shows what a destructive deploy would remove.
|
|
171
|
+
|
|
172
|
+
## Using it in CI
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
- run: pip install pawabase-iac
|
|
176
|
+
- run: pawabase validate # on every pull request
|
|
177
|
+
- run: pawabase plan # show the diff in the log
|
|
178
|
+
env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }} }
|
|
179
|
+
- run: pawabase deploy --yes # on merge to main
|
|
180
|
+
env: { PAWABASE_URL: ${{ vars.PAWABASE_URL }}, PAWABASE_API_KEY: ${{ secrets.PAWABASE_DEPLOY_KEY }}, ERP_SECRET: ${{ secrets.ERP_SECRET }} }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`PAWABASE_URL` and `PAWABASE_ENV` override `pawabase.toml`.
|
|
184
|
+
|
|
185
|
+
## Examples
|
|
186
|
+
|
|
187
|
+
* [`examples/store`](examples/store): a small shop (tables, a checkout flow with a loop, retry and error paths, a receipt flow, switch-based ticket routing, subscriptions, a webhook, a signed inbound hook, buckets, a mail template).
|
|
188
|
+
* [`examples/school`](examples/school): a complete **school management system**: 11 roles, 22 policies, 43 tables, 25 flows, routes, subscriptions, schedules, signed payment webhooks, buckets and email templates (136 definitions) with a smoke test that runs the real flows against a deployed environment and its own offline tests.
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e '.[test]'
|
|
194
|
+
pytest # the library, offline
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The conformance tests compare every definition's request body with the server's own request models, run flows through the server's flow validator and check the generated block catalogue against the running server's. They run where `pawabase_server` is importable:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cd ../pawabase && PYTHONPATH=../pawabase-iac uv run python -m pytest ../pawabase-iac/tests/test_server_conformance.py
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Regenerate the flow blocks when the server gains some:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
cd ../pawabase && uv run python ../pawabase-iac/scripts/generate_blocks.py
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
pawabase_iac/
|
|
211
|
+
core/ base Definition, env(), the registry
|
|
212
|
+
expr/ P (policy conditions) and ctx (flow templates)
|
|
213
|
+
tables/ fields, Table, Schema, indexes, relations
|
|
214
|
+
policies/ Policy, Transformer, condition checks
|
|
215
|
+
access/ Role
|
|
216
|
+
flows/ Flow builder, graph checks, layout, catalogue.py and blocks/ (generated)
|
|
217
|
+
api/ Route
|
|
218
|
+
events/ Subscription, Schedule, Webhook, InboundHook
|
|
219
|
+
storage/ Bucket
|
|
220
|
+
mail/ MailTemplate
|
|
221
|
+
validate/ cross-definition reference checks
|
|
222
|
+
engine/ plan, apply, diff, what-it-serves
|
|
223
|
+
environments/ Remote (the Platform API) and Emulated (in memory)
|
|
224
|
+
project/ pawabase.toml, .env, importing your definitions
|
|
225
|
+
cli/ the pawabase command
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Limits
|
|
229
|
+
|
|
230
|
+
Environment-level settings are not managed from code yet: auth providers and settings, secrets, API keys, firewall rules, domains, alarms, dashboards, backup schedules, and mail and storage connection settings. Users are not managed either (give people roles in Accounts). The twelve kinds above are.
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|