zrb 1.0.0b10__py3-none-any.whl → 1.2.0__py3-none-any.whl

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 (61) hide show
  1. zrb/builtin/git.py +8 -8
  2. zrb/builtin/llm/llm_chat.py +3 -3
  3. zrb/builtin/project/add/fastapp/fastapp_input.py +1 -1
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +24 -1
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +61 -1
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +297 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +24 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +3 -3
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +40 -1
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +2 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +2 -2
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +18 -8
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +91 -8
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +9 -0
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -1
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +2 -4
  40. zrb/builtin/project/create/project_task.py +2 -2
  41. zrb/builtin/random.py +3 -3
  42. zrb/builtin/setup/common_input.py +5 -5
  43. zrb/builtin/setup/tmux/tmux.py +1 -1
  44. zrb/builtin/setup/zsh/zsh.py +1 -1
  45. zrb/builtin/todo.py +4 -4
  46. zrb/input/base_input.py +17 -12
  47. zrb/input/bool_input.py +12 -5
  48. zrb/input/float_input.py +12 -5
  49. zrb/input/int_input.py +12 -5
  50. zrb/input/option_input.py +5 -5
  51. zrb/input/password_input.py +5 -5
  52. zrb/input/text_input.py +4 -4
  53. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  54. zrb/runner/web_route/static/refresh-token.template.js +9 -0
  55. zrb/runner/web_route/static/static_route.py +1 -1
  56. zrb/util/load.py +13 -7
  57. {zrb-1.0.0b10.dist-info → zrb-1.2.0.dist-info}/METADATA +2 -2
  58. {zrb-1.0.0b10.dist-info → zrb-1.2.0.dist-info}/RECORD +60 -44
  59. zrb/util/llm/tool.py +0 -87
  60. {zrb-1.0.0b10.dist-info → zrb-1.2.0.dist-info}/WHEEL +0 -0
  61. {zrb-1.0.0b10.dist-info → zrb-1.2.0.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,18 @@ from zrb.util.codemod.modify_class_parent import prepend_parent_class
8
8
  from zrb.util.codemod.modify_function import append_code_to_function
9
9
  from zrb.util.codemod.modify_module import prepend_code_to_module
10
10
  from zrb.util.file import read_file, write_file
11
- from zrb.util.string.conversion import to_kebab_case, to_pascal_case, to_snake_case
11
+ from zrb.util.string.conversion import (
12
+ to_human_case,
13
+ to_kebab_case,
14
+ to_pascal_case,
15
+ to_snake_case,
16
+ )
17
+
18
+
19
+ def is_gateway_navigation_config_file(ctx: AnyContext, file_path: str) -> bool:
20
+ return file_path == os.path.join(
21
+ APP_DIR, "module", "gateway", "config", "navigation.py"
22
+ )
12
23
 
13
24
 
14
25
  def get_add_permission_migration_script(ctx: AnyContext) -> str:
@@ -156,6 +167,19 @@ def is_module_gateway_subroute_file(ctx: AnyContext, file_path: str) -> bool:
156
167
  return file_path == module_gateway_subroute_file
157
168
 
158
169
 
170
+ def is_module_gateway_subroute_view_file(ctx: AnyContext, file_path: str) -> bool:
171
+ module_gateway_subroute_file = os.path.join(
172
+ APP_DIR,
173
+ "module",
174
+ "gateway",
175
+ "view",
176
+ "content",
177
+ f"{to_kebab_case(ctx.input.module)}",
178
+ f"{to_kebab_case(ctx.input.entity)}.html",
179
+ )
180
+ return file_path == module_gateway_subroute_file
181
+
182
+
159
183
  def update_migration_metadata_file(ctx: AnyContext, migration_metadata_file_path: str):
160
184
  app_name = os.path.basename(APP_DIR)
161
185
  existing_migration_metadata_code = read_file(migration_metadata_file_path)
@@ -318,7 +342,9 @@ def update_route_file(ctx: AnyContext, route_file_path: str):
318
342
 
319
343
  def update_gateway_subroute_file(ctx: AnyContext, module_gateway_subroute_path: str):
320
344
  snake_module_name = to_snake_case(ctx.input.module)
345
+ kebab_module_name = to_kebab_case(ctx.input.module)
321
346
  snake_entity_name = to_snake_case(ctx.input.entity)
347
+ kebab_entity_name = to_kebab_case(ctx.input.entity)
322
348
  snake_plural_entity_name = to_snake_case(ctx.input.plural)
323
349
  kebab_plural_entity_name = to_kebab_case(ctx.input.plural)
324
350
  pascal_entity_name = to_pascal_case(ctx.input.entity)
@@ -341,7 +367,9 @@ def update_gateway_subroute_file(ctx: AnyContext, module_gateway_subroute_path:
341
367
  ),
342
368
  replace_map={
343
369
  "my_module": snake_module_name,
370
+ "my-module": kebab_module_name,
344
371
  "my_entity": snake_entity_name,
372
+ "my-entity": kebab_entity_name,
345
373
  "my_entities": snake_plural_entity_name,
346
374
  "my-entities": kebab_plural_entity_name,
347
375
  "MyEntity": pascal_entity_name,
@@ -382,3 +410,35 @@ def _get_import_schema_for_gateway_subroute_code(
382
410
  if new_code in existing_code:
383
411
  return None
384
412
  return new_code
413
+
414
+
415
+ def update_gateway_navigation_config_file(
416
+ ctx: AnyContext, gateway_navigation_config_file_path: str
417
+ ):
418
+ existing_gateway_navigation_config_code = read_file(
419
+ gateway_navigation_config_file_path
420
+ )
421
+ snake_module_name = to_snake_case(ctx.input.module)
422
+ kebab_module_name = to_kebab_case(ctx.input.module)
423
+ kebab_entity_name = to_kebab_case(ctx.input.entity)
424
+ human_entity_name = to_human_case(ctx.input.entity)
425
+ kebab_plural_name = to_kebab_case(ctx.input.plural)
426
+ new_navigation_config_code = read_file(
427
+ file_path=os.path.join(
428
+ os.path.dirname(__file__), "template", "navigation_config_file.py"
429
+ ),
430
+ replace_map={
431
+ "my_module": snake_module_name,
432
+ "my-module": kebab_module_name,
433
+ "my-entity": kebab_entity_name,
434
+ "My Entity": human_entity_name.title(),
435
+ "my-entities": kebab_plural_name,
436
+ },
437
+ ).strip()
438
+ write_file(
439
+ file_path=gateway_navigation_config_file_path,
440
+ content=[
441
+ existing_gateway_navigation_config_code,
442
+ new_navigation_config_code,
443
+ ],
444
+ )
@@ -0,0 +1,297 @@
1
+ <link rel="stylesheet" href="/static/crud/style.css">
2
+
3
+ <main class="container">
4
+ <article>
5
+ <h1>My Entity</h1>
6
+
7
+ <fieldset id="crud-table-fieldset" role="group" class="grid">
8
+ <input id="crud-filter" onchange="applySearch()" placeholder="🔍 Filter" aria-label="Search" />
9
+ <button onclick="applySearch()">🔍 Search</button>
10
+ {% if allow_create %}
11
+ <button class="contrast" onclick="showCreateForm(event)">➕ Add</button>
12
+ {% endif %}
13
+ </fieldset>
14
+ <div id="crud-table-container">
15
+ <table id="crud-table" class="striped">
16
+ <thead>
17
+ <tr>
18
+ <th scope="col">ID</th>
19
+ <th scope="col">My Column</th>
20
+ <!-- Update this -->
21
+ {% if allow_update or allow_delete %}
22
+ <th scope="col">Actions</th>
23
+ {% endif %}
24
+ </tr>
25
+ </thead>
26
+ <tbody></tbody>
27
+ </table>
28
+ </div>
29
+ <div id="crud-pagination"></div>
30
+
31
+ {% if allow_create %}
32
+ <dialog id="crud-create-form-dialog">
33
+ <article>
34
+ <h2>New My Entity</h2>
35
+ <form id="crud-create-form">
36
+ <label>
37
+ My Column:
38
+ <input type="text" name="my_column" required>
39
+ </label>
40
+ <!-- Update this -->
41
+ <footer>
42
+ <button onclick="createRow(event)">➕ Save</button>
43
+ <button class="secondary" onclick="hideCreateForm(event)">❌ Cancel</button>
44
+ </footer>
45
+ </form>
46
+ </article>
47
+ </dialog>
48
+ {% endif %}
49
+
50
+ {% if allow_update %}
51
+ <dialog id="crud-update-form-dialog">
52
+ <article>
53
+ <h2>Update My Entity</h2>
54
+ <form id="crud-update-form">
55
+ <label>
56
+ My Column:
57
+ <input type="text" name="my_column" required>
58
+ </label>
59
+ <!-- Update this -->
60
+ <footer>
61
+ <button onclick="updateRow(event)">✏️ Save</button>
62
+ <button class="secondary" onclick="hideUpdateForm(event)">❌ Cancel</button>
63
+ </footer>
64
+ </form>
65
+ </article>
66
+ </dialog>
67
+ {% endif %}
68
+
69
+ {% if allow_delete %}
70
+ <dialog id="crud-delete-form-dialog">
71
+ <article>
72
+ <h2>Delete My Entity</h2>
73
+ <form id="crud-delete-form">
74
+ <label>
75
+ My Column:
76
+ <input type="text" name="my_column" readonly>
77
+ </label>
78
+ <!-- Update this -->
79
+ <footer>
80
+ <button class="secondary" onclick="hideDeleteForm()">❌ Cancel</button>
81
+ <button onclick="deleteRow()">🗑️ Delete</button>
82
+ </footer>
83
+ </form>
84
+ </article>
85
+ </dialog>
86
+ {% endif %}
87
+
88
+ <dialog id="crud-alert-dialog">
89
+ <article>
90
+ <h2 id="crud-alert-title">Error</h2>
91
+ <pre id="crud-alert-message"></pre>
92
+ <footer>
93
+ <button onclick="hideAlert(event)">Close</button>
94
+ </footer>
95
+ </article>
96
+ </dialog>
97
+
98
+ </article>
99
+ </main>
100
+
101
+ <script src="/static/crud/util.js"></script>
102
+ <script>
103
+ const apiUrl = "/api/v1/my-entities";
104
+ const crudState = {
105
+ pageSize: {{page_size | tojson}},
106
+ currentPage: {{page | tojson}},
107
+ sort: {{sort | tojson}},
108
+ filter: {{filter | tojson}},
109
+ allowCreate: {{allow_create | tojson}},
110
+ allowUpdate: {{allow_update | tojson}},
111
+ allowDelete: {{allow_delete | tojson}},
112
+ updatedRowId: null,
113
+ deletedRowId: null,
114
+ };
115
+
116
+ async function applySearch() {
117
+ const filterInput = document.getElementById("crud-filter");
118
+ crudState.filter = filterInput.value;
119
+ return await fetchRows(crudState.currentPage);
120
+ }
121
+
122
+ async function fetchRows(page = null) {
123
+ try {
124
+ if (typeof page !== 'undefined' && page !== null) {
125
+ crudState.currentPage = page;
126
+ }
127
+ const defaultSearchColumn = "my_column"
128
+ // update address bar
129
+ const searchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, false);
130
+ const newUrl = `${window.location.pathname}?${searchParam}`;
131
+ window.history.pushState({ path: newUrl }, "", newUrl);
132
+ // update table and pagination
133
+ const apiSearchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, true);
134
+ const result = await UTIL.fetchAPI(
135
+ `${apiUrl}?${apiSearchParam}`, { method: "GET" }
136
+ );
137
+ renderRows(result.data);
138
+ const crudPagination = document.getElementById("crud-pagination");
139
+ CRUD_UTIL.renderPagination(
140
+ crudPagination, crudState, result.count, "fetchRows"
141
+ );
142
+ } catch (error) {
143
+ console.error("Error fetching items:", error);
144
+ }
145
+ }
146
+
147
+ function renderRows(rows) {
148
+ const tableBody = document.querySelector("#crud-table tbody");
149
+ tableBody.innerHTML = "";
150
+ rows.forEach(row => {
151
+ let rowComponent = getRowComponents(row);
152
+ actionColumn = "";
153
+ if (crudState.allowUpdate) {
154
+ actionColumn += `<button class="contrast" onclick="showUpdateForm('${row.id}')">✏️ Edit</button>`;
155
+ }
156
+ if (crudState.allowDelete) {
157
+ actionColumn += `<button class="secondary" onclick="showDeleteForm('${row.id}')">🗑️ Delete</button>`;
158
+ }
159
+ if (crudState.allowUpdate || crudState.allowDelete) {
160
+ actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
161
+ }
162
+ tableBody.innerHTML += `<tr>${rowComponent.join('')}${actionColumn}</tr>`;
163
+ });
164
+ }
165
+
166
+ function getRowComponents(row) {
167
+ let rowComponents = [];
168
+ rowComponents.push(`<td>${row.id}</td>`);
169
+ rowComponents.push(`<td>${row.my_column}</td>`);
170
+ // Update this
171
+ return rowComponents;
172
+ }
173
+
174
+ {% if allow_create %}
175
+ async function showCreateForm(id) {
176
+ const createFormDialog = document.getElementById("crud-create-form-dialog");
177
+ const createForm = document.getElementById("crud-create-form");
178
+ UTIL.clearFormData(createForm);
179
+ createFormDialog.showModal();
180
+ }
181
+
182
+ async function createRow(event = null) {
183
+ if (event != null) {
184
+ event.preventDefault();
185
+ }
186
+ try {
187
+ const createForm = document.getElementById("crud-create-form");
188
+ const formData = JSON.stringify(UTIL.getFormData(createForm));
189
+ await UTIL.fetchAPI(apiUrl, {method: "POST", body: formData});
190
+ await fetchRows();
191
+ hideCreateForm();
192
+ } catch(error) {
193
+ showAlert("Create My Entity Error", error);
194
+ }
195
+ }
196
+
197
+ function hideCreateForm(event = null) {
198
+ if (event != null) {
199
+ event.preventDefault();
200
+ }
201
+ const createFormDialog = document.getElementById("crud-create-form-dialog");
202
+ createFormDialog.close();
203
+ }
204
+ {% endif %}
205
+
206
+ {% if allow_update %}
207
+ async function showUpdateForm(id) {
208
+ crudState.updatedRowId = id;
209
+ const updateFormDialog = document.getElementById("crud-update-form-dialog");
210
+ const updateForm = document.getElementById("crud-update-form");
211
+ result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
212
+ UTIL.setFormData(updateForm, result);
213
+ updateFormDialog.showModal();
214
+ }
215
+
216
+ async function updateRow(event = null) {
217
+ if (event != null) {
218
+ event.preventDefault();
219
+ }
220
+ try {
221
+ const updateForm = document.getElementById("crud-update-form");
222
+ const formData = JSON.stringify(UTIL.getFormData(updateForm));
223
+ await UTIL.fetchAPI(
224
+ `${apiUrl}/${crudState.updatedRowId}`, {method: "PUT", body: formData},
225
+ );
226
+ await fetchRows();
227
+ hideUpdateForm();
228
+ } catch(error) {
229
+ showAlert("Update My Entity Error", error);
230
+ }
231
+ }
232
+
233
+ function hideUpdateForm(event = null) {
234
+ if (event != null) {
235
+ event.preventDefault();
236
+ }
237
+ const updateFormDialog = document.getElementById("crud-update-form-dialog");
238
+ updateFormDialog.close();
239
+ }
240
+ {% endif %}
241
+
242
+ {% if allow_delete %}
243
+ async function showDeleteForm(id) {
244
+ crudState.deletedRowId = id;
245
+ const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
246
+ const deleteForm = document.getElementById("crud-delete-form");
247
+ result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
248
+ UTIL.setFormData(deleteForm, result);
249
+ deleteFormDialog.showModal();
250
+ }
251
+
252
+ async function deleteRow(event = null) {
253
+ if (event != null) {
254
+ event.preventDefault();
255
+ }
256
+ try {
257
+ await UTIL.fetchAPI(`${apiUrl}/${crudState.deletedRowId}`, {method: "DELETE",});
258
+ await fetchRows();
259
+ hideDeleteForm();
260
+ } catch(error) {
261
+ showAlert("Delete My Entity Error", error);
262
+ }
263
+ }
264
+
265
+ function hideDeleteForm(event = null) {
266
+ if (event != null) {
267
+ event.preventDefault();
268
+ }
269
+ const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
270
+ deleteFormDialog.close();
271
+ }
272
+ {% endif %}
273
+
274
+ function showAlert(title, error) {
275
+ const alertDialog = document.getElementById("crud-alert-dialog");
276
+ const alertTitle = document.getElementById("crud-alert-title");
277
+ const alertMessage = document.getElementById("crud-alert-message");
278
+ const errorMessage = error.message ? error.message : String(error);
279
+ alertTitle.textContent = title;
280
+ alertMessage.textContent = errorMessage;
281
+ alertDialog.showModal();
282
+ }
283
+
284
+ function hideAlert(event = null) {
285
+ if (event != null) {
286
+ event.preventDefault();
287
+ }
288
+ const alertDialog = document.getElementById("crud-alert-dialog");
289
+ alertDialog.close();
290
+ }
291
+
292
+ document.addEventListener("DOMContentLoaded", () => {
293
+ const filterInput = document.getElementById("crud-filter");
294
+ filterInput.value = crudState.filter;
295
+ fetchRows(crudState.currentPage);
296
+ });
297
+ </script>
@@ -1,6 +1,30 @@
1
1
  # MyEntity routes
2
2
 
3
3
 
4
+ @app.get("/my-module/my-entities", include_in_schema=False)
5
+ def my_entities_crud_ui(
6
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
7
+ page: int = 1,
8
+ page_size: int = 10,
9
+ sort: str | None = None,
10
+ filter: str | None = None,
11
+ ):
12
+ if not current_user.has_permission("my-entity:read"):
13
+ return render_error(error_message="Access denied", status_code=403)
14
+ return render_content(
15
+ view_path=os.path.join("my-module", "my-entity.html"),
16
+ current_user=current_user,
17
+ page_name="my-module.my-entity",
18
+ page=page,
19
+ page_size=page_size,
20
+ sort=sort,
21
+ filter=filter,
22
+ allow_create=current_user.has_permission("my-entity:create"),
23
+ allow_update=current_user.has_permission("my-entity:update"),
24
+ allow_delete=current_user.has_permission("my-entity:delete"),
25
+ )
26
+
27
+
4
28
  @app.get("/api/v1/my-entities", response_model=MultipleMyEntityResponse)
5
29
  async def get_my_entities(
6
30
  current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
@@ -0,0 +1,8 @@
1
+ my_module_menu.append_page(
2
+ Page(
3
+ name="my-module.my-entity",
4
+ caption="My Entity",
5
+ url="/my-module/my-entities",
6
+ permission="my-entity:read",
7
+ )
8
+ )
@@ -8,7 +8,7 @@ run_env_input = OptionInput(
8
8
  description="Running environment",
9
9
  prompt="Running Environment",
10
10
  options=["dev", "prod"],
11
- default_str="prod",
11
+ default="prod",
12
12
  )
13
13
 
14
14
  new_module_input = StrInput(
@@ -37,14 +37,14 @@ plural_entity_input = StrInput(
37
37
  name="plural",
38
38
  description="Plural entity name",
39
39
  prompt="Plural entity name",
40
- default_str=lambda ctx: pluralize(ctx.input.entity),
40
+ default=lambda ctx: pluralize(ctx.input.entity),
41
41
  )
42
42
 
43
43
  new_entity_column_input = StrInput(
44
44
  name="column",
45
45
  description="Entity's column name",
46
46
  prompt="New entity's column name",
47
- default_str="name",
47
+ default="name",
48
48
  )
49
49
 
50
50
  new_column_input = StrInput(
@@ -10,12 +10,14 @@ from my_app_name._zrb.module.add_module_util import (
10
10
  is_app_zrb_config_file,
11
11
  is_app_zrb_task_file,
12
12
  is_gateway_module_subroute_file,
13
+ is_gateway_navigation_config_file,
13
14
  is_gateway_route_file,
14
15
  is_in_module_dir,
15
16
  update_app_config_file,
16
17
  update_app_main_file,
17
18
  update_app_zrb_config_file,
18
19
  update_app_zrb_task_file,
20
+ update_gateway_navigation_config_file,
19
21
  update_gateway_route_file,
20
22
  )
21
23
  from my_app_name._zrb.util import get_existing_module_names
@@ -91,6 +93,12 @@ scaffold_my_app_name_module = Scaffolder(
91
93
  match=is_gateway_route_file,
92
94
  transform=update_gateway_route_file,
93
95
  ),
96
+ # Register module's page to my_app_name/gateway/config/navigation.py
97
+ ContentTransformer(
98
+ name="transform-gateway-navigation-config",
99
+ match=is_gateway_navigation_config_file,
100
+ transform=update_gateway_navigation_config_file,
101
+ ),
94
102
  ],
95
103
  retries=0,
96
104
  upstream=validate_add_my_app_name_module,
@@ -7,7 +7,12 @@ from zrb.context.any_context import AnyContext
7
7
  from zrb.util.codemod.modify_dict import append_key_to_dict
8
8
  from zrb.util.codemod.modify_function import append_code_to_function
9
9
  from zrb.util.file import read_file, write_file
10
- from zrb.util.string.conversion import to_kebab_case, to_pascal_case, to_snake_case
10
+ from zrb.util.string.conversion import (
11
+ to_human_case,
12
+ to_kebab_case,
13
+ to_pascal_case,
14
+ to_snake_case,
15
+ )
11
16
 
12
17
 
13
18
  def is_app_config_file(ctx: AnyContext, file_path: str) -> bool:
@@ -22,6 +27,12 @@ def is_gateway_route_file(ctx: AnyContext, file_path: str) -> bool:
22
27
  return file_path == os.path.join(APP_DIR, "module", "gateway", "route.py")
23
28
 
24
29
 
30
+ def is_gateway_navigation_config_file(ctx: AnyContext, file_path: str) -> bool:
31
+ return file_path == os.path.join(
32
+ APP_DIR, "module", "gateway", "config", "navigation.py"
33
+ )
34
+
35
+
25
36
  def is_app_zrb_task_file(ctx: AnyContext, file_path: str) -> bool:
26
37
  return file_path == os.path.join(APP_DIR, "_zrb", "task.py")
27
38
 
@@ -194,3 +205,31 @@ def _get_module_subroute_import(existing_code: str, module_name: str) -> str | N
194
205
  if import_code in existing_code:
195
206
  return None
196
207
  return import_code
208
+
209
+
210
+ def update_gateway_navigation_config_file(
211
+ ctx: AnyContext, gateway_navigation_config_file_path: str
212
+ ):
213
+ existing_gateway_navigation_config_code = read_file(
214
+ gateway_navigation_config_file_path
215
+ )
216
+ snake_module_name = to_snake_case(ctx.input.module)
217
+ kebab_module_name = to_kebab_case(ctx.input.module)
218
+ human_module_name = to_human_case(ctx.input.module)
219
+ new_navigation_config_code = read_file(
220
+ file_path=os.path.join(
221
+ os.path.dirname(__file__), "template", "navigation_config_file.py"
222
+ ),
223
+ replace_map={
224
+ "my_module": snake_module_name,
225
+ "my-module": kebab_module_name,
226
+ "My Module": human_module_name.title(),
227
+ },
228
+ ).strip()
229
+ write_file(
230
+ file_path=gateway_navigation_config_file_path,
231
+ content=[
232
+ existing_gateway_navigation_config_code,
233
+ new_navigation_config_code,
234
+ ],
235
+ )
@@ -1,8 +1,10 @@
1
+ import os
1
2
  from typing import Annotated
2
3
 
3
4
  from fastapi import Depends, FastAPI
4
5
  from my_app_name.common.error import ForbiddenError
5
6
  from my_app_name.module.gateway.util.auth import get_current_user
7
+ from my_app_name.module.gateway.util.view import render_content, render_error
6
8
  from my_app_name.schema.user import AuthUserResponse
7
9
 
8
10
 
@@ -0,0 +1,6 @@
1
+ my_module_menu = APP_NAVIGATION.append_page_group(
2
+ PageGroup(
3
+ name="my-module",
4
+ caption="My Module",
5
+ )
6
+ )
@@ -72,7 +72,7 @@ def create_default_filter_param_parser() -> (
72
72
  # Returns [UserModel.age >= 18, UserModel.name.like("John%"), UserModel.role.in_(["admin", "user"]), UserModel.address == "123, Main St."]
73
73
  """
74
74
  filters: list[ClauseElement] = []
75
- filter_parts = split_by_comma(query)
75
+ filter_parts = split_unescaped(query)
76
76
  for part in filter_parts:
77
77
  match = re.match(r"(.+):(.+):(.+)", part)
78
78
  if match:
@@ -101,5 +101,5 @@ def create_default_filter_param_parser() -> (
101
101
  return parse_filter_param
102
102
 
103
103
 
104
- def split_by_comma(s: str, delimiter: str = ",") -> list[str]:
104
+ def split_unescaped(s: str, delimiter: str = ",") -> list[str]:
105
105
  return re.split(r"(?<!\\)" + re.escape(delimiter), s)
@@ -27,7 +27,7 @@ def render_str(template_path: str, **data: Any) -> str:
27
27
  def render_page(
28
28
  template_path: str,
29
29
  status_code: int = 200,
30
- headers: dict[str, str] = None,
30
+ headers: dict[str, str] | None = None,
31
31
  media_type: str | None = None,
32
32
  **data: Any
33
33
  ) -> HTMLResponse:
@@ -8,25 +8,35 @@ APP_VERSION = "0.1.0"
8
8
 
9
9
  APP_GATEWAY_VIEW_PATH = os.path.join(APP_PATH, "module", "gateway", "view")
10
10
  APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH = os.getenv(
11
- "MY_APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
11
+ "MY_APP_NAME_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
12
12
  os.path.join("template", "default.html"),
13
13
  )
14
- _DEFAULT_CSS_PATH = "/static/pico-css/pico.min.css"
14
+ APP_GATEWAY_PICO_CSS_COLOR = os.getenv("MY_APP_NAME_GATEWAY_PICO_CSS_COLOR", "")
15
+ APP_GATEWAY_PICO_CSS_PATH = (
16
+ "/static/pico-css/pico.min.css"
17
+ if APP_GATEWAY_PICO_CSS_COLOR == ""
18
+ else f"/static/pico-css/pico.{APP_GATEWAY_PICO_CSS_COLOR}.min.css"
19
+ )
15
20
  APP_GATEWAY_CSS_PATH_LIST = [
16
21
  path
17
- for path in os.getenv("MY_APP_GATEWAY_CSS_PATH", _DEFAULT_CSS_PATH).split(":")
22
+ for path in os.getenv("MY_APP_NAME_GATEWAY_CSS_PATH", "").split(":")
18
23
  if path != ""
19
24
  ]
20
25
  APP_GATEWAY_JS_PATH_LIST = [
21
- path for path in os.getenv("MY_APP_GATEWAY_JS_PATH", "").split(":") if path != ""
26
+ path
27
+ for path in os.getenv("MY_APP_NAME_GATEWAY_JS_PATH", "").split(":")
28
+ if path != ""
22
29
  ]
23
- APP_GATEWAY_TITLE = os.getenv("MY_APP_GATEWAY_TITLE", "My App Name")
24
- APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_GATEWAY_SUBTITLE", "Just Another App")
30
+ APP_GATEWAY_TITLE = os.getenv("MY_APP_NAME_GATEWAY_TITLE", "My App Name")
31
+ APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_NAME_GATEWAY_SUBTITLE", "Just Another App")
25
32
  APP_GATEWAY_LOGO_PATH = os.getenv(
26
- "MY_APP_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
33
+ "MY_APP_NAME_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
34
+ )
35
+ APP_GATEWAY_FOOTER = os.getenv(
36
+ "MY_APP_NAME_GATEWAY_FOOTER", f"{APP_GATEWAY_TITLE} &copy; 2025"
27
37
  )
28
38
  APP_GATEWAY_FAVICON_PATH = os.getenv(
29
- "MY_APP_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
39
+ "MY_APP_NAME_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
30
40
  )
31
41
 
32
42
  APP_MODE = os.getenv("MY_APP_NAME_MODE", "monolith")
@@ -0,0 +1,39 @@
1
+ from my_app_name.module.gateway.schema.navigation import Navigation, Page, PageGroup
2
+
3
+ APP_NAVIGATION = Navigation()
4
+
5
+ APP_NAVIGATION.append_page(Page(name="gateway.home", caption="Home", url="/"))
6
+
7
+ auth_menu = APP_NAVIGATION.append_page_group(
8
+ PageGroup(
9
+ name="auth",
10
+ caption="Authorization",
11
+ )
12
+ )
13
+
14
+ auth_menu.append_page(
15
+ Page(
16
+ name="auth.permission",
17
+ caption="Permission",
18
+ url="/auth/permissions",
19
+ permission="permission:read",
20
+ )
21
+ )
22
+
23
+ auth_menu.append_page(
24
+ Page(
25
+ name="auth.role",
26
+ caption="Role",
27
+ url="/auth/roles",
28
+ permission="role:read",
29
+ )
30
+ )
31
+
32
+ auth_menu.append_page(
33
+ Page(
34
+ name="auth.user",
35
+ caption="User",
36
+ url="/auth/users",
37
+ permission="user:read",
38
+ )
39
+ )