erupt-sdk 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.
@@ -0,0 +1,238 @@
1
+ Metadata-Version: 2.4
2
+ Name: erupt-sdk
3
+ Version: 0.1.0
4
+ Summary: Serve Python models as an erupt admin UI through an erupt cloud server
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+
9
+ # erupt-sdk (Python)
10
+
11
+ Serve Python models as an erupt admin UI. Zero dependencies, Python 3.9+.
12
+
13
+ ```bash
14
+ pip install erupt-sdk
15
+ ```
16
+
17
+ ```python
18
+ import datetime, os
19
+ from erupt import Field, Node, SqliteRepository, erupt
20
+
21
+ @erupt("Article", order_by="id desc")
22
+ class Article:
23
+ id: int = Field(pk=True)
24
+ title: str = Field("Title", search=True, not_null=True, sortable=True)
25
+ status: str = Field("Status", choices={"draft": "Draft", "published": "Published"}, search=True)
26
+ publish_date: datetime.date = Field("Publish Date", search=True)
27
+ body: str = Field("Body", type="TEXTAREA", table=False)
28
+
29
+ node = Node("blog", os.environ["ERUPT_ACCESS_TOKEN"], "http://erupt-server:8080")
30
+ node.register(Article, SqliteRepository("blog.db", "article"))
31
+ node.serve(port=9090)
32
+ ```
33
+
34
+ 1. On the erupt server create node `blog` (Node Config) and copy its access token.
35
+ 2. Add a menu of type `table` with value `blog.Article`.
36
+ 3. Run the node. The server must be able to reach it on `http://<node ip>:9090`; set `address=` otherwise.
37
+
38
+ The edit type follows the Python annotation (`str` INPUT, `int`/`float` NUMBER, `bool` BOOLEAN,
39
+ `date` DATE, `datetime` DATE_TIME, `choices=` CHOICE) unless `type=` is given. `view={...}` / `edit={...}`
40
+ on a field and keyword arguments on `@erupt` pass raw annotation attributes through.
41
+
42
+ ## References
43
+
44
+ Annotate a field with another `@erupt` class (declared earlier, registered on the same node). The repository
45
+ stores the referenced primary key; the UI shows `label` (default `name`).
46
+
47
+ ```python
48
+ @erupt("Category")
49
+ class Category:
50
+ id: int = Field(pk=True)
51
+ name: str = Field("Name", search=True)
52
+ parent: int = Field("Parent Id")
53
+
54
+ class Article:
55
+ category: Category = Field("Category", search=True) # table picker
56
+ topic: Category = Field("Topic", tree="parent") # tree picker over Category.parent
57
+ ```
58
+
59
+ ## Row operations
60
+
61
+ ```python
62
+ @erupt("Publish Options")
63
+ class PublishForm: # a form model needs no primary key
64
+ publish_date: datetime.date = Field("Publish Date", not_null=True)
65
+
66
+ class Article:
67
+ @operation("Publish", form=PublishForm, if_expr="item.status !== 'published'")
68
+ def publish(op): # op.rows, op.param, op.repo, op.user
69
+ for row in op.rows:
70
+ op.repo.update(row["id"], {"status": "published", "publish_date": op.param["publish_date"]})
71
+ ```
72
+
73
+ `mode`: `SINGLE` / `MULTI` (default) / `MULTI_ONLY` / `BUTTON`. `confirm=False` skips the dialog, a string
74
+ replaces its text. `show=lambda user: ...` hides the operation and refuses to run it. A returned string is
75
+ executed as JavaScript in the browser, so return only code you wrote.
76
+
77
+ ## DataProxy
78
+
79
+ Hooks around a model's reads and writes, as erupt's Java `DataProxy`. Override what you need; raise `Fail` to
80
+ refuse with a message the user sees.
81
+
82
+ ```python
83
+ from erupt import DataProxy, Fail, current_user
84
+
85
+ class ArticleProxy(DataProxy):
86
+ def add_behavior(self, record): # the new form's values
87
+ record["status"] = "draft"
88
+
89
+ def validate(self, record): # before every add and update
90
+ if "spam" in (record.get("title") or ""):
91
+ raise Fail("no spam")
92
+
93
+ def before_fetch(self, conditions): # SQL ANDed to the table's WHERE, values bound
94
+ return "owner = ?", [current_user()["uid"]]
95
+
96
+ def after_fetch(self, rows): # a table page, before it is sent
97
+ for row in rows:
98
+ row["title"] = row["title"].strip()
99
+
100
+ @erupt("Article", data_proxy=ArticleProxy) # one proxy or a list, classes or instances
101
+ class Article: ...
102
+ ```
103
+
104
+ | Hook | Runs |
105
+ |------|------|
106
+ | `search_condition(condition)` | building the model: default values of the search bar |
107
+ | `before_fetch(conditions)` | before a table page is read: SQL ANDed to its WHERE as written, `"sql"` or `("sql with ?", [values])` (with values, each `?` is one of them); SQL repositories only, `MemoryRepository` refuses it |
108
+ | `after_fetch(rows)` | after a table page is read |
109
+ | `add_behavior(record)` | for the new record form (`init-value`) |
110
+ | `edit_behavior(record)` | after a record is read by its key (edit form, detail view) |
111
+ | `validate(record)` | before an add or an update |
112
+ | `before_add` / `after_add(record)` | around `repository.add` |
113
+ | `before_update` / `after_update(record)` | around `repository.update`, with the submitted values |
114
+ | `before_delete` / `after_delete(record)` | around `repository.delete`, once per record as stored |
115
+
116
+ ## Views
117
+
118
+ `vis` declares the extra views of `@Erupt(vis = ...)`. A view's `filter` (one SQL string or a list, ANDed as
119
+ written) and `orderBy` (as `order_by`) are applied by the node when the table is read for that view, and kept
120
+ out of the build, as Java keeps them `@Transient`. A view without a `code` is addressed by its title. Like
121
+ `before_fetch`, a filter needs a SQL repository.
122
+
123
+ ```python
124
+ @erupt("Task", vis=[
125
+ {"title": "Board", "type": "BOARD", "boardView": {"groupField": "status"}, "filter": "archived = 0"},
126
+ {"title": "Recent", "type": "TABLE", "orderBy": "created desc"},
127
+ ])
128
+ class Task: ...
129
+ ```
130
+
131
+ Dragging on a board, calendar or gantt view is saved by the erupt server through the node's `get` and `update`.
132
+
133
+ ## Trees
134
+
135
+ `tree={"id": "id", "label": "name", "pid": "parent"}` shows the model as a tree, and
136
+ `linkTree={"field": "category"}` puts the tree of a reference field beside the table: the erupt server builds
137
+ both trees from the node's table, and the node keeps the rows under the picked tree node (`dependNode: True`
138
+ shows none until one is picked). `linkTree.field` must be a reference field.
139
+
140
+ ## Field types and field code
141
+
142
+ Besides the inferred types, `type=` takes `TAGS`, `AUTO_COMPLETE`, `ATTACHMENT` (the erupt server stores the
143
+ upload, the node keeps its path), `HTML_EDITOR`, `MARKDOWN`, `CODE_EDITOR` (all text), `BUTTON` (form only),
144
+ and `CHECKBOX` / `TAB_TREE` with `ref=`: the picked records, stored as the JSON list of their keys. A
145
+ `list`-annotated field with `choices` is a `MULTI_CHOICE`.
146
+
147
+ A field can run its own code; the erupt server calls it through the node's `invoke` endpoint with the form's
148
+ values:
149
+
150
+ ```python
151
+ @erupt("Order")
152
+ class Order:
153
+ id: int = Field(pk=True)
154
+ status: str = Field("Status", fetch=lambda form: {"new": "New", "paid": "Paid"}) # options, also TAGS: [...]
155
+ qty: int = Field("Qty", onchange=lambda form: {"formData": {"total": form["qty"] * 9}, "editExpr": {}})
156
+ city: str = Field("City", type="AUTO_COMPLETE", complete=lambda form, typed: lookup(typed))
157
+ total: float = Field("Total")
158
+ recalc = Field("Recalc", type="BUTTON", click=lambda form: {"eval": "alert('done')", "formData": {}})
159
+ tags: list = Field("Tags", type="CHECKBOX", ref=Tag)
160
+ ```
161
+
162
+ `fetch` belongs to `CHOICE` / `MULTI_CHOICE` (`{value: label}` or pairs) and `TAGS` (a list), `complete` to
163
+ `AUTO_COMPLETE`, `click` to `BUTTON`; `onchange` to any field. Raise `Fail` to refuse with a message.
164
+
165
+ Also: `COLOR`, `ICON`, `SIGNATURE`, `MAP`, `HIDDEN` (text), `RATE`, `SLIDER` (numbers) and `TRANSFER` (like
166
+ `CHECKBOX`). A reference table depending on another field takes
167
+ `edit={"referenceTableType": {"dependField": "province", "dependColumn": "province"}}`: the erupt server keeps the
168
+ picked model's rows whose `dependColumn` holds the depend field's value.
169
+
170
+ ## Drills
171
+
172
+ ```python
173
+ @erupt("Author", drills=[{"title": "Books", "link": {"linkErupt": "Book", "joinColumn": "author"}}])
174
+ ```
175
+
176
+ opens `Book` filtered to the rows whose `author` holds the author's `id` (`link.column`, `id` by default);
177
+ `link.linkCondition` adds SQL as written. Both models must be on the node.
178
+
179
+ ## Server config and user info
180
+
181
+ `node.config()` and `node.group_config()` return the config text set for the node and its group on the erupt
182
+ server; `node.user_info()` the acting user's full info (roles, org, post), while a request is served.
183
+
184
+ ## Storage
185
+
186
+ Storage is any `Repository` (`page`, `get`, `add`, `update`, `delete`). Shipped:
187
+
188
+ - `MemoryRepository` for tests and demos;
189
+ - `SqliteRepository(path, table)`, which creates the table;
190
+ - `SqlRepository(connect, table, dialect=..., release=..., paramstyle=...)` over any DB-API 2.0 driver.
191
+ `dialect` (`sqlite`, `postgres`, `mysql`, `mssql`, `oracle`) decides identifier quotes and paging; the
192
+ driver's paramstyle is detected. Create the tables yourself, with columns named like the fields.
193
+
194
+ ```python
195
+ import psycopg # PostgreSQL
196
+ repo = SqlRepository(lambda: psycopg.connect(DSN), "article", dialect="postgres")
197
+
198
+ from psycopg_pool import ConnectionPool
199
+ pool = ConnectionPool(DSN) # pooled: hand connections back instead of closing them
200
+ repo = SqlRepository(pool.getconn, "article", dialect="postgres", release=pool.putconn)
201
+
202
+ import pymysql # MySQL
203
+ repo = SqlRepository(lambda: pymysql.connect(**MYSQL), "article", dialect="mysql")
204
+ ```
205
+
206
+ Without a sort, every repository returns rows by primary key ascending. `erupt.current_user()` returns the
207
+ acting user inside a repository call.
208
+
209
+ Not yet: sub-tables (TAB_TABLE_*), TPL operations, i18n of messages. Trees, file
210
+ upload and Excel are done by the erupt server.
211
+
212
+ ## Without a registry
213
+
214
+ From the repository, without PyPI:
215
+
216
+ ```bash
217
+ pip install "erupt-sdk @ git+https://github.com/erupt-io/erupt-sdk@python/v0.1.0#subdirectory=python"
218
+ ```
219
+
220
+ Or copy `python/src/erupt/` into your project as the package `erupt`; it has no dependencies, keep the two
221
+ `.json` files next to the code. A copy does not update itself: when erupt changes the protocol or the annotation schema, replace the whole directory with the next release instead of merging by hand, and leave `__version__` in `node.py` untouched so the server shows which release the node runs.
222
+
223
+ ## Generated code
224
+
225
+ `src/erupt/_types.py` (TypedDicts and Literals for every annotation attribute: `Edit`, `View`, `Erupt`,
226
+ `RowOperation`, `EditType`, …) is generated from the erupt annotation JSON Schema. Raw `view=` / `edit=` /
227
+ `@erupt(...)` / `@operation(...)` attributes are checked against the same schema when a model is declared,
228
+ so a misspelt key or enum value fails at import instead of silently doing nothing in the browser.
229
+
230
+ After an annotation change in erupt (regenerate there with `-Derupt.node-schema.write=true` /
231
+ `-Derupt.node-defaults.write=true`):
232
+
233
+ ```bash
234
+ python tools/gen_types.py <erupt repo>/schema # sync schemas, regenerate _types.py
235
+ python tools/gen_types.py <erupt repo>/schema --check # CI: fail when stale
236
+ ```
237
+
238
+ Tests: `PYTHONPATH=src python -m unittest discover -s tests`
@@ -0,0 +1,230 @@
1
+ # erupt-sdk (Python)
2
+
3
+ Serve Python models as an erupt admin UI. Zero dependencies, Python 3.9+.
4
+
5
+ ```bash
6
+ pip install erupt-sdk
7
+ ```
8
+
9
+ ```python
10
+ import datetime, os
11
+ from erupt import Field, Node, SqliteRepository, erupt
12
+
13
+ @erupt("Article", order_by="id desc")
14
+ class Article:
15
+ id: int = Field(pk=True)
16
+ title: str = Field("Title", search=True, not_null=True, sortable=True)
17
+ status: str = Field("Status", choices={"draft": "Draft", "published": "Published"}, search=True)
18
+ publish_date: datetime.date = Field("Publish Date", search=True)
19
+ body: str = Field("Body", type="TEXTAREA", table=False)
20
+
21
+ node = Node("blog", os.environ["ERUPT_ACCESS_TOKEN"], "http://erupt-server:8080")
22
+ node.register(Article, SqliteRepository("blog.db", "article"))
23
+ node.serve(port=9090)
24
+ ```
25
+
26
+ 1. On the erupt server create node `blog` (Node Config) and copy its access token.
27
+ 2. Add a menu of type `table` with value `blog.Article`.
28
+ 3. Run the node. The server must be able to reach it on `http://<node ip>:9090`; set `address=` otherwise.
29
+
30
+ The edit type follows the Python annotation (`str` INPUT, `int`/`float` NUMBER, `bool` BOOLEAN,
31
+ `date` DATE, `datetime` DATE_TIME, `choices=` CHOICE) unless `type=` is given. `view={...}` / `edit={...}`
32
+ on a field and keyword arguments on `@erupt` pass raw annotation attributes through.
33
+
34
+ ## References
35
+
36
+ Annotate a field with another `@erupt` class (declared earlier, registered on the same node). The repository
37
+ stores the referenced primary key; the UI shows `label` (default `name`).
38
+
39
+ ```python
40
+ @erupt("Category")
41
+ class Category:
42
+ id: int = Field(pk=True)
43
+ name: str = Field("Name", search=True)
44
+ parent: int = Field("Parent Id")
45
+
46
+ class Article:
47
+ category: Category = Field("Category", search=True) # table picker
48
+ topic: Category = Field("Topic", tree="parent") # tree picker over Category.parent
49
+ ```
50
+
51
+ ## Row operations
52
+
53
+ ```python
54
+ @erupt("Publish Options")
55
+ class PublishForm: # a form model needs no primary key
56
+ publish_date: datetime.date = Field("Publish Date", not_null=True)
57
+
58
+ class Article:
59
+ @operation("Publish", form=PublishForm, if_expr="item.status !== 'published'")
60
+ def publish(op): # op.rows, op.param, op.repo, op.user
61
+ for row in op.rows:
62
+ op.repo.update(row["id"], {"status": "published", "publish_date": op.param["publish_date"]})
63
+ ```
64
+
65
+ `mode`: `SINGLE` / `MULTI` (default) / `MULTI_ONLY` / `BUTTON`. `confirm=False` skips the dialog, a string
66
+ replaces its text. `show=lambda user: ...` hides the operation and refuses to run it. A returned string is
67
+ executed as JavaScript in the browser, so return only code you wrote.
68
+
69
+ ## DataProxy
70
+
71
+ Hooks around a model's reads and writes, as erupt's Java `DataProxy`. Override what you need; raise `Fail` to
72
+ refuse with a message the user sees.
73
+
74
+ ```python
75
+ from erupt import DataProxy, Fail, current_user
76
+
77
+ class ArticleProxy(DataProxy):
78
+ def add_behavior(self, record): # the new form's values
79
+ record["status"] = "draft"
80
+
81
+ def validate(self, record): # before every add and update
82
+ if "spam" in (record.get("title") or ""):
83
+ raise Fail("no spam")
84
+
85
+ def before_fetch(self, conditions): # SQL ANDed to the table's WHERE, values bound
86
+ return "owner = ?", [current_user()["uid"]]
87
+
88
+ def after_fetch(self, rows): # a table page, before it is sent
89
+ for row in rows:
90
+ row["title"] = row["title"].strip()
91
+
92
+ @erupt("Article", data_proxy=ArticleProxy) # one proxy or a list, classes or instances
93
+ class Article: ...
94
+ ```
95
+
96
+ | Hook | Runs |
97
+ |------|------|
98
+ | `search_condition(condition)` | building the model: default values of the search bar |
99
+ | `before_fetch(conditions)` | before a table page is read: SQL ANDed to its WHERE as written, `"sql"` or `("sql with ?", [values])` (with values, each `?` is one of them); SQL repositories only, `MemoryRepository` refuses it |
100
+ | `after_fetch(rows)` | after a table page is read |
101
+ | `add_behavior(record)` | for the new record form (`init-value`) |
102
+ | `edit_behavior(record)` | after a record is read by its key (edit form, detail view) |
103
+ | `validate(record)` | before an add or an update |
104
+ | `before_add` / `after_add(record)` | around `repository.add` |
105
+ | `before_update` / `after_update(record)` | around `repository.update`, with the submitted values |
106
+ | `before_delete` / `after_delete(record)` | around `repository.delete`, once per record as stored |
107
+
108
+ ## Views
109
+
110
+ `vis` declares the extra views of `@Erupt(vis = ...)`. A view's `filter` (one SQL string or a list, ANDed as
111
+ written) and `orderBy` (as `order_by`) are applied by the node when the table is read for that view, and kept
112
+ out of the build, as Java keeps them `@Transient`. A view without a `code` is addressed by its title. Like
113
+ `before_fetch`, a filter needs a SQL repository.
114
+
115
+ ```python
116
+ @erupt("Task", vis=[
117
+ {"title": "Board", "type": "BOARD", "boardView": {"groupField": "status"}, "filter": "archived = 0"},
118
+ {"title": "Recent", "type": "TABLE", "orderBy": "created desc"},
119
+ ])
120
+ class Task: ...
121
+ ```
122
+
123
+ Dragging on a board, calendar or gantt view is saved by the erupt server through the node's `get` and `update`.
124
+
125
+ ## Trees
126
+
127
+ `tree={"id": "id", "label": "name", "pid": "parent"}` shows the model as a tree, and
128
+ `linkTree={"field": "category"}` puts the tree of a reference field beside the table: the erupt server builds
129
+ both trees from the node's table, and the node keeps the rows under the picked tree node (`dependNode: True`
130
+ shows none until one is picked). `linkTree.field` must be a reference field.
131
+
132
+ ## Field types and field code
133
+
134
+ Besides the inferred types, `type=` takes `TAGS`, `AUTO_COMPLETE`, `ATTACHMENT` (the erupt server stores the
135
+ upload, the node keeps its path), `HTML_EDITOR`, `MARKDOWN`, `CODE_EDITOR` (all text), `BUTTON` (form only),
136
+ and `CHECKBOX` / `TAB_TREE` with `ref=`: the picked records, stored as the JSON list of their keys. A
137
+ `list`-annotated field with `choices` is a `MULTI_CHOICE`.
138
+
139
+ A field can run its own code; the erupt server calls it through the node's `invoke` endpoint with the form's
140
+ values:
141
+
142
+ ```python
143
+ @erupt("Order")
144
+ class Order:
145
+ id: int = Field(pk=True)
146
+ status: str = Field("Status", fetch=lambda form: {"new": "New", "paid": "Paid"}) # options, also TAGS: [...]
147
+ qty: int = Field("Qty", onchange=lambda form: {"formData": {"total": form["qty"] * 9}, "editExpr": {}})
148
+ city: str = Field("City", type="AUTO_COMPLETE", complete=lambda form, typed: lookup(typed))
149
+ total: float = Field("Total")
150
+ recalc = Field("Recalc", type="BUTTON", click=lambda form: {"eval": "alert('done')", "formData": {}})
151
+ tags: list = Field("Tags", type="CHECKBOX", ref=Tag)
152
+ ```
153
+
154
+ `fetch` belongs to `CHOICE` / `MULTI_CHOICE` (`{value: label}` or pairs) and `TAGS` (a list), `complete` to
155
+ `AUTO_COMPLETE`, `click` to `BUTTON`; `onchange` to any field. Raise `Fail` to refuse with a message.
156
+
157
+ Also: `COLOR`, `ICON`, `SIGNATURE`, `MAP`, `HIDDEN` (text), `RATE`, `SLIDER` (numbers) and `TRANSFER` (like
158
+ `CHECKBOX`). A reference table depending on another field takes
159
+ `edit={"referenceTableType": {"dependField": "province", "dependColumn": "province"}}`: the erupt server keeps the
160
+ picked model's rows whose `dependColumn` holds the depend field's value.
161
+
162
+ ## Drills
163
+
164
+ ```python
165
+ @erupt("Author", drills=[{"title": "Books", "link": {"linkErupt": "Book", "joinColumn": "author"}}])
166
+ ```
167
+
168
+ opens `Book` filtered to the rows whose `author` holds the author's `id` (`link.column`, `id` by default);
169
+ `link.linkCondition` adds SQL as written. Both models must be on the node.
170
+
171
+ ## Server config and user info
172
+
173
+ `node.config()` and `node.group_config()` return the config text set for the node and its group on the erupt
174
+ server; `node.user_info()` the acting user's full info (roles, org, post), while a request is served.
175
+
176
+ ## Storage
177
+
178
+ Storage is any `Repository` (`page`, `get`, `add`, `update`, `delete`). Shipped:
179
+
180
+ - `MemoryRepository` for tests and demos;
181
+ - `SqliteRepository(path, table)`, which creates the table;
182
+ - `SqlRepository(connect, table, dialect=..., release=..., paramstyle=...)` over any DB-API 2.0 driver.
183
+ `dialect` (`sqlite`, `postgres`, `mysql`, `mssql`, `oracle`) decides identifier quotes and paging; the
184
+ driver's paramstyle is detected. Create the tables yourself, with columns named like the fields.
185
+
186
+ ```python
187
+ import psycopg # PostgreSQL
188
+ repo = SqlRepository(lambda: psycopg.connect(DSN), "article", dialect="postgres")
189
+
190
+ from psycopg_pool import ConnectionPool
191
+ pool = ConnectionPool(DSN) # pooled: hand connections back instead of closing them
192
+ repo = SqlRepository(pool.getconn, "article", dialect="postgres", release=pool.putconn)
193
+
194
+ import pymysql # MySQL
195
+ repo = SqlRepository(lambda: pymysql.connect(**MYSQL), "article", dialect="mysql")
196
+ ```
197
+
198
+ Without a sort, every repository returns rows by primary key ascending. `erupt.current_user()` returns the
199
+ acting user inside a repository call.
200
+
201
+ Not yet: sub-tables (TAB_TABLE_*), TPL operations, i18n of messages. Trees, file
202
+ upload and Excel are done by the erupt server.
203
+
204
+ ## Without a registry
205
+
206
+ From the repository, without PyPI:
207
+
208
+ ```bash
209
+ pip install "erupt-sdk @ git+https://github.com/erupt-io/erupt-sdk@python/v0.1.0#subdirectory=python"
210
+ ```
211
+
212
+ Or copy `python/src/erupt/` into your project as the package `erupt`; it has no dependencies, keep the two
213
+ `.json` files next to the code. A copy does not update itself: when erupt changes the protocol or the annotation schema, replace the whole directory with the next release instead of merging by hand, and leave `__version__` in `node.py` untouched so the server shows which release the node runs.
214
+
215
+ ## Generated code
216
+
217
+ `src/erupt/_types.py` (TypedDicts and Literals for every annotation attribute: `Edit`, `View`, `Erupt`,
218
+ `RowOperation`, `EditType`, …) is generated from the erupt annotation JSON Schema. Raw `view=` / `edit=` /
219
+ `@erupt(...)` / `@operation(...)` attributes are checked against the same schema when a model is declared,
220
+ so a misspelt key or enum value fails at import instead of silently doing nothing in the browser.
221
+
222
+ After an annotation change in erupt (regenerate there with `-Derupt.node-schema.write=true` /
223
+ `-Derupt.node-defaults.write=true`):
224
+
225
+ ```bash
226
+ python tools/gen_types.py <erupt repo>/schema # sync schemas, regenerate _types.py
227
+ python tools/gen_types.py <erupt repo>/schema --check # CI: fail when stale
228
+ ```
229
+
230
+ Tests: `PYTHONPATH=src python -m unittest discover -s tests`
@@ -0,0 +1,15 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "erupt-sdk"
7
+ version = "0.1.0"
8
+ description = "Serve Python models as an erupt admin UI through an erupt cloud server"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ dependencies = []
13
+
14
+ [tool.setuptools.package-data]
15
+ erupt = ["*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """Erupt node SDK: give any Python service an erupt admin UI through an erupt cloud server."""
2
+ from .model import DataProxy, Field, erupt, operation
3
+ from .node import Fail, Node, OperationContext, __version__, current_user
4
+ from .repo import Cond, MemoryRepository, Query, Repository, SqliteRepository, SqlRepository
5
+
6
+ __all__ = ["erupt", "Field", "operation", "OperationContext", "DataProxy", "Fail", "Node", "current_user", "Repository",
7
+ "MemoryRepository", "SqliteRepository", "SqlRepository", "Query", "Cond", "__version__"]