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
zrb/builtin/git.py CHANGED
@@ -24,31 +24,31 @@ from zrb.util.git import (
24
24
  name="source",
25
25
  description="Source branch/tag/commit",
26
26
  prompt="Source branch/tag/commit",
27
- default_str="main",
27
+ default="main",
28
28
  ),
29
29
  StrInput(
30
30
  name="current",
31
31
  description="Current branch/tag/commit",
32
32
  prompt="Current branch/tag/commit",
33
- default_str="HEAD",
33
+ default="HEAD",
34
34
  ),
35
35
  BoolInput(
36
36
  name="created",
37
37
  description="Include created files",
38
38
  prompt="Include created files",
39
- default_str="True",
39
+ default="True",
40
40
  ),
41
41
  BoolInput(
42
42
  name="removed",
43
43
  description="Include removed files",
44
44
  prompt="Include removed files",
45
- default_str="True",
45
+ default="True",
46
46
  ),
47
47
  BoolInput(
48
48
  name="updated",
49
49
  description="Include updated files",
50
50
  prompt="Include updated files",
51
- default_str="True",
51
+ default="True",
52
52
  ),
53
53
  ],
54
54
  description="🔍 Get modified files",
@@ -109,7 +109,7 @@ async def prune_local_branches(ctx: AnyContext):
109
109
  name="message",
110
110
  description="Commit message",
111
111
  prompt="Commit message",
112
- default_str="Add feature/fix bug",
112
+ default="Add feature/fix bug",
113
113
  ),
114
114
  description="📝 Commit changes",
115
115
  group=git_group,
@@ -131,7 +131,7 @@ async def git_commit(ctx: AnyContext):
131
131
  name="remote",
132
132
  description="Remote name",
133
133
  prompt="Remote name",
134
- default_str="origin",
134
+ default="origin",
135
135
  ),
136
136
  upstream=git_commit,
137
137
  group=git_group,
@@ -154,7 +154,7 @@ async def git_pull(ctx: AnyContext):
154
154
  name="remote",
155
155
  description="Remote name",
156
156
  prompt="Remote name",
157
- default_str="origin",
157
+ default="origin",
158
158
  ),
159
159
  upstream=git_commit,
160
160
  group=git_group,
@@ -80,21 +80,21 @@ llm_chat: LLMTask = llm_group.add_task(
80
80
  "model",
81
81
  description="LLM Model",
82
82
  prompt="LLM Model",
83
- default_str=LLM_MODEL,
83
+ default=LLM_MODEL,
84
84
  allow_positional_parsing=False,
85
85
  ),
86
86
  TextInput(
87
87
  "system-prompt",
88
88
  description="System prompt",
89
89
  prompt="System prompt",
90
- default_str=LLM_SYSTEM_PROMPT,
90
+ default=LLM_SYSTEM_PROMPT,
91
91
  allow_positional_parsing=False,
92
92
  ),
93
93
  BoolInput(
94
94
  "start-new",
95
95
  description="Start new conversation (LLM will forget everything)",
96
96
  prompt="Start new conversation (LLM will forget everything)",
97
- default_str="false",
97
+ default=False,
98
98
  allow_positional_parsing=False,
99
99
  ),
100
100
  TextInput("message", description="User message", prompt="Your message"),
@@ -6,7 +6,7 @@ project_dir_input = StrInput(
6
6
  name="project-dir",
7
7
  description="Project directory",
8
8
  prompt="Project directory",
9
- default_str=lambda _: os.getcwd(),
9
+ default=lambda _: os.getcwd(),
10
10
  )
11
11
 
12
12
  app_name_input = StrInput(
@@ -1,93 +1,137 @@
1
1
  import os
2
2
 
3
+ from my_app_name._zrb.column.add_column_util import (
4
+ update_fastapp_schema,
5
+ update_fastapp_test_create,
6
+ update_fastapp_test_delete,
7
+ update_fastapp_test_read,
8
+ update_fastapp_test_update,
9
+ update_fastapp_ui,
10
+ )
3
11
  from my_app_name._zrb.config import APP_DIR
4
12
  from my_app_name._zrb.format_task import format_my_app_name_code
5
13
  from my_app_name._zrb.group import app_create_group
6
14
  from my_app_name._zrb.input import (
7
15
  existing_entity_input,
16
+ existing_module_input,
8
17
  new_column_input,
9
18
  new_column_type_input,
10
19
  )
11
- from my_app_name._zrb.util import get_existing_schema_names
20
+ from my_app_name._zrb.util import get_existing_module_names, get_existing_schema_names
12
21
 
13
22
  from zrb import AnyContext, Task, make_task
14
- from zrb.util.codemod.modify_class_property import append_property_to_class
15
- from zrb.util.file import read_file, write_file
16
- from zrb.util.string.conversion import to_pascal_case, to_snake_case
17
23
 
18
24
 
19
25
  @make_task(
20
- name="validate-add-my-app-name-column",
21
- input=existing_entity_input,
26
+ name="validate-add-fastapp-column",
27
+ input=[
28
+ existing_module_input,
29
+ existing_entity_input,
30
+ ],
22
31
  retries=0,
23
32
  )
24
- async def validate_add_my_app_name_column(ctx: AnyContext):
33
+ async def validate_add_fastapp_column(ctx: AnyContext):
34
+ module_name = ctx.input.module
35
+ if module_name not in get_existing_module_names():
36
+ raise ValueError(f"Module not exist: {module_name}")
25
37
  schema_name = ctx.input.entity
26
38
  if schema_name not in get_existing_schema_names():
27
39
  raise ValueError(f"Schema not exist: {schema_name}")
28
40
 
29
41
 
30
- @make_task(
31
- name="update-my-app-name-schema",
42
+ update_fastapp_schema_task = Task(
43
+ name="update-fastapp-schema",
44
+ input=[
45
+ existing_module_input,
46
+ existing_entity_input,
47
+ new_column_input,
48
+ new_column_type_input,
49
+ ],
50
+ action=update_fastapp_schema,
51
+ retries=0,
52
+ upstream=validate_add_fastapp_column,
53
+ )
54
+
55
+ update_fastapp_ui_task = Task(
56
+ name="update-fastapp-ui",
32
57
  input=[
58
+ existing_module_input,
33
59
  existing_entity_input,
34
60
  new_column_input,
35
61
  new_column_type_input,
36
62
  ],
63
+ action=update_fastapp_ui,
37
64
  retries=0,
38
- upstream=validate_add_my_app_name_column,
65
+ upstream=validate_add_fastapp_column,
39
66
  )
40
- def update_my_app_name_schema(ctx: AnyContext):
41
- snake_entity_name = to_snake_case(ctx.input.entity)
42
- pascal_entity_name = to_pascal_case(ctx.input.entity)
43
- schema_file_path = os.path.join(APP_DIR, "schema", f"{snake_entity_name}.py")
44
- existing_code = read_file(schema_file_path)
45
- snake_column_name = to_snake_case(ctx.input.column)
46
- column_type = ctx.input.type
47
- # Base
48
- new_code = append_property_to_class(
49
- original_code=existing_code,
50
- class_name=f"{pascal_entity_name}Base",
51
- property_name=snake_column_name,
52
- annotation=column_type,
53
- default_value=_get_default_value(column_type),
54
- )
55
- # Update
56
- new_code = append_property_to_class(
57
- original_code=new_code,
58
- class_name=f"{pascal_entity_name}Update",
59
- property_name=snake_column_name,
60
- annotation=f"{column_type} | None",
61
- default_value="None",
62
- )
63
- # Table
64
- new_code = append_property_to_class(
65
- original_code=new_code,
66
- class_name=f"{pascal_entity_name}",
67
- property_name=snake_column_name,
68
- annotation=f"{column_type} | None",
69
- default_value="Field(index=False)",
70
- )
71
- write_file(schema_file_path, new_code)
72
67
 
68
+ update_fastapp_test_create_task = Task(
69
+ name="update-fastapp-test-create",
70
+ input=[
71
+ existing_module_input,
72
+ existing_entity_input,
73
+ new_column_input,
74
+ new_column_type_input,
75
+ ],
76
+ action=update_fastapp_test_create,
77
+ retries=0,
78
+ upstream=validate_add_fastapp_column,
79
+ )
73
80
 
74
- add_my_app_name_column = app_create_group.add_task(
81
+ update_fastapp_test_read_task = Task(
82
+ name="update-fastapp-test-read",
83
+ input=[
84
+ existing_module_input,
85
+ existing_entity_input,
86
+ new_column_input,
87
+ new_column_type_input,
88
+ ],
89
+ action=update_fastapp_test_read,
90
+ retries=0,
91
+ upstream=validate_add_fastapp_column,
92
+ )
93
+
94
+ update_fastapp_test_update_task = Task(
95
+ name="update-fastapp-test-update",
96
+ input=[
97
+ existing_module_input,
98
+ existing_entity_input,
99
+ new_column_input,
100
+ new_column_type_input,
101
+ ],
102
+ action=update_fastapp_test_update,
103
+ retries=0,
104
+ upstream=validate_add_fastapp_column,
105
+ )
106
+
107
+ update_fastapp_test_delete_task = Task(
108
+ name="update-fastapp-test-delete",
109
+ input=[
110
+ existing_module_input,
111
+ existing_entity_input,
112
+ new_column_input,
113
+ new_column_type_input,
114
+ ],
115
+ action=update_fastapp_test_delete,
116
+ retries=0,
117
+ upstream=validate_add_fastapp_column,
118
+ )
119
+
120
+
121
+ add_fastapp_column = app_create_group.add_task(
75
122
  Task(
76
- name="add-my-app-name-column",
123
+ name="add-fastapp-column",
77
124
  description="📊 Create new column on an entity",
78
- upstream=update_my_app_name_schema,
125
+ upstream=[
126
+ update_fastapp_schema_task,
127
+ update_fastapp_ui_task,
128
+ update_fastapp_test_create_task,
129
+ update_fastapp_test_read_task,
130
+ update_fastapp_test_update_task,
131
+ update_fastapp_test_delete_task,
132
+ ],
79
133
  successor=format_my_app_name_code,
80
134
  retries=0,
81
135
  ),
82
136
  alias="column",
83
137
  )
84
-
85
-
86
- def _get_default_value(data_type: str) -> str:
87
- if data_type == "str":
88
- return '""'
89
- if data_type in ("int", "float"):
90
- return "0"
91
- if data_type == "bool":
92
- return "True"
93
- return "None"
@@ -0,0 +1,301 @@
1
+ import os
2
+ import re
3
+ import textwrap
4
+
5
+ from bs4 import BeautifulSoup, formatter
6
+ from my_app_name._zrb.config import APP_DIR
7
+
8
+ from zrb.context.any_context import AnyContext
9
+ from zrb.util.codemod.modify_class import append_code_to_class
10
+ from zrb.util.codemod.modify_class_parent import prepend_parent_class
11
+ from zrb.util.codemod.modify_class_property import append_property_to_class
12
+ from zrb.util.codemod.modify_function import append_code_to_function
13
+ from zrb.util.codemod.modify_module import prepend_code_to_module
14
+ from zrb.util.file import read_file, write_file
15
+ from zrb.util.string.conversion import (
16
+ to_human_case,
17
+ to_kebab_case,
18
+ to_pascal_case,
19
+ to_snake_case,
20
+ )
21
+
22
+
23
+ def update_fastapp_schema(ctx: AnyContext):
24
+ snake_entity_name = to_snake_case(ctx.input.entity)
25
+ pascal_entity_name = to_pascal_case(ctx.input.entity)
26
+ snake_column_name = to_snake_case(ctx.input.column)
27
+ column_type = ctx.input.type
28
+ schema_file_path = os.path.join(APP_DIR, "schema", f"{snake_entity_name}.py")
29
+ existing_code = read_file(schema_file_path)
30
+ # Base
31
+ new_code = append_property_to_class(
32
+ original_code=existing_code,
33
+ class_name=f"{pascal_entity_name}Base",
34
+ property_name=snake_column_name,
35
+ annotation=column_type,
36
+ default_value=_get_default_column_value(column_type),
37
+ )
38
+ # Update
39
+ new_code = append_property_to_class(
40
+ original_code=new_code,
41
+ class_name=f"{pascal_entity_name}Update",
42
+ property_name=snake_column_name,
43
+ annotation=f"{column_type} | None",
44
+ default_value="None",
45
+ )
46
+ # Table
47
+ new_code = append_property_to_class(
48
+ original_code=new_code,
49
+ class_name=f"{pascal_entity_name}",
50
+ property_name=snake_column_name,
51
+ annotation=f"{column_type} | None",
52
+ default_value="Field(index=False)",
53
+ )
54
+ write_file(schema_file_path, new_code)
55
+
56
+
57
+ def _get_default_column_value(data_type: str) -> str:
58
+ if data_type == "str":
59
+ return '""'
60
+ if data_type in ("int", "float"):
61
+ return "0"
62
+ if data_type == "bool":
63
+ return "True"
64
+ return "None"
65
+
66
+
67
+ def update_fastapp_ui(ctx: AnyContext):
68
+ kebab_module_name = to_kebab_case(ctx.input.module)
69
+ kebab_entity_name = to_kebab_case(ctx.input.entity)
70
+ snake_column_name = to_snake_case(ctx.input.column)
71
+ human_column_name = to_human_case(ctx.input.column).title()
72
+ subroute_file_path = os.path.join(
73
+ APP_DIR,
74
+ "module",
75
+ "gateway",
76
+ "view",
77
+ "content",
78
+ kebab_module_name,
79
+ f"{kebab_entity_name}.html",
80
+ )
81
+ existing_code = read_file(subroute_file_path)
82
+ # Add table header
83
+ new_code = _add_th_before_last(
84
+ existing_code, table_id="crud-table", th_content=human_column_name
85
+ )
86
+ # Forms
87
+ new_code = _add_input_to_form(
88
+ new_code,
89
+ form_id="crud-create-form",
90
+ column_label=human_column_name,
91
+ column_name=snake_column_name,
92
+ )
93
+ new_code = _add_input_to_form(
94
+ new_code,
95
+ form_id="crud-update-form",
96
+ column_label=human_column_name,
97
+ column_name=snake_column_name,
98
+ )
99
+ new_code = _add_input_to_form(
100
+ new_code,
101
+ form_id="crud-delete-form",
102
+ column_label=human_column_name,
103
+ column_name=snake_column_name,
104
+ )
105
+ # JS Function
106
+ new_code = _alter_js_function_returned_array(
107
+ new_code,
108
+ js_function_name="getRowComponents",
109
+ js_array_name="rowComponents",
110
+ js_new_value=f"`<td>${{row.{snake_column_name}}}</td>`",
111
+ )
112
+ write_file(subroute_file_path, new_code)
113
+
114
+
115
+ def _add_th_before_last(html_str, table_id, th_content):
116
+ # Use the html.parser; you might try html5lib if you find that it preserves formatting better.
117
+ soup = BeautifulSoup(html_str, "html.parser")
118
+ # Locate the table with the specified id
119
+ table = soup.find("table", id=table_id)
120
+ if not table:
121
+ # Table not found; return original HTML unchanged.
122
+ return html_str
123
+ # Find the thead element in the table.
124
+ thead = table.find("thead")
125
+ if not thead:
126
+ return html_str
127
+ # For this example, we assume there's a single row (<tr>) in the thead.
128
+ row = thead.find("tr")
129
+ if not row:
130
+ return html_str
131
+ # Find all existing th elements in the row.
132
+ th_elements = row.find_all("th")
133
+ if not th_elements:
134
+ return html_str
135
+ # Create a new th element and set its content.
136
+ new_th = soup.new_tag("th")
137
+ new_th.string = th_content
138
+ # Insert the new th right before the last existing th.
139
+ th_elements[-1].insert_before(new_th)
140
+ # Return the modified HTML as a string.
141
+ return soup.prettify(
142
+ formatter=formatter.HTMLFormatter(indent=_infer_html_indent_width(html_str))
143
+ )
144
+
145
+
146
+ def _add_input_to_form(
147
+ html_str: str, form_id: str, column_label: str, column_name: str
148
+ ) -> str:
149
+ soup = BeautifulSoup(html_str, "html.parser")
150
+ # Find the form by id.
151
+ form = soup.find("form", id=form_id)
152
+ if not form:
153
+ return html_str # Return unchanged if no matching form is found.
154
+ # Create a new label element with the provided column label.
155
+ new_label = soup.new_tag("label")
156
+ new_label.append(f"{column_label}: ")
157
+ # Create a new input element with the provided column name.
158
+ new_input = soup.new_tag(
159
+ "input", attrs={"type": "text", "name": column_name, "required": "required"}
160
+ )
161
+ new_label.append(new_input)
162
+ # Look for a footer element inside the form.
163
+ footer = form.find("footer")
164
+ if footer:
165
+ # Insert the new label before the footer.
166
+ footer.insert_before(new_label)
167
+ else:
168
+ # If no footer exists, simply append the new label to the form.
169
+ form.append(new_label)
170
+ return soup.prettify(
171
+ formatter=formatter.HTMLFormatter(indent=_infer_html_indent_width(html_str))
172
+ )
173
+
174
+
175
+ def _infer_html_indent_width(html_str: str) -> int:
176
+ """
177
+ Infer the indentation width (number of spaces) from the HTML string.
178
+ It looks for the first non-empty line that starts with whitespace
179
+ followed by '<' and returns the number of leading spaces.
180
+ If none is found, defaults to 2.
181
+ """
182
+ for line in textwrap.dedent(html_str).splitlines():
183
+ stripped = line.lstrip()
184
+ if stripped.startswith("<") and line != stripped:
185
+ return len(line) - len(stripped)
186
+ return 2
187
+
188
+
189
+ def _alter_js_function_returned_array(
190
+ html_str: str, js_function_name: str, js_array_name: str, js_new_value: str
191
+ ) -> str:
192
+ """
193
+ Inserts a new push into the specified JavaScript function.
194
+
195
+ It finds the function definition with the given js_function_name,
196
+ then looks for the first return statement inside the function body,
197
+ and inserts a new line that pushes js_new_value into js_arr_name.
198
+
199
+ Parameters:
200
+ html_str (str): The HTML containing the JavaScript.
201
+ js_function_name (str): The name of the JavaScript function to modify.
202
+ js_arr_name (str): The name of the array inside that function.
203
+ js_new_value (str): The new value to push. (Pass the JS literal as a string,
204
+ e.g. "`<td>NEW</td>`" or '"<td>NEW</td>"'.)
205
+
206
+ Returns:
207
+ str: The modified HTML.
208
+ """
209
+ # This pattern finds:
210
+ # 1. The function signature and opening brace.
211
+ # 2. All content (non-greedily) until we hit a newline containing a return statement.
212
+ # 3. Captures the newline and leading whitespace (indent) of the return statement.
213
+ # 4. Captures the rest of the return line.
214
+ pattern = (
215
+ r"(function\s+"
216
+ + re.escape(js_function_name)
217
+ + r"\s*\([^)]*\)\s*\{)" # group1: function header
218
+ r"([\s\S]*?)" # group2: code before return
219
+ r"(\n(\s*)return\s+[^;]+;)" # group3: newline+return line, group4: indent
220
+ )
221
+
222
+ def replacer(match):
223
+ header = match.group(1)
224
+ before_return = match.group(2)
225
+ return_line = match.group(3)
226
+ indent = match.group(4)
227
+ # Create the new push statement. We add a newline plus the same indent.
228
+ # The resulting line will be, e.g.,
229
+ # " rowComponents.push(`<td>NEW</td>`);"
230
+ injection = f"\n{indent}{js_array_name}.push({js_new_value});"
231
+ return header + before_return + injection + return_line
232
+
233
+ # Use re.sub to replace only the first occurrence of the function
234
+ new_html, count = re.subn(
235
+ pattern, replacer, html_str, count=1, flags=re.MULTILINE | re.DOTALL
236
+ )
237
+ return new_html
238
+
239
+
240
+ def update_fastapp_test_create(ctx: AnyContext):
241
+ snake_module_name = to_snake_case(ctx.input.module)
242
+ snake_entity_name = to_snake_case(ctx.input.entity)
243
+ test_file_path = os.path.join(
244
+ APP_DIR,
245
+ "test",
246
+ snake_module_name,
247
+ snake_entity_name,
248
+ f"test_create_{snake_entity_name}.py",
249
+ )
250
+ existing_code = read_file(test_file_path)
251
+ new_code = existing_code
252
+ # TODO: update test
253
+ write_file(test_file_path, new_code)
254
+
255
+
256
+ def update_fastapp_test_read(ctx: AnyContext):
257
+ snake_module_name = to_snake_case(ctx.input.module)
258
+ snake_entity_name = to_snake_case(ctx.input.entity)
259
+ test_file_path = os.path.join(
260
+ APP_DIR,
261
+ "test",
262
+ snake_module_name,
263
+ snake_entity_name,
264
+ f"test_read_{snake_entity_name}.py",
265
+ )
266
+ existing_code = read_file(test_file_path)
267
+ new_code = existing_code
268
+ # TODO: update test
269
+ write_file(test_file_path, new_code)
270
+
271
+
272
+ def update_fastapp_test_update(ctx: AnyContext):
273
+ snake_module_name = to_snake_case(ctx.input.module)
274
+ snake_entity_name = to_snake_case(ctx.input.entity)
275
+ test_file_path = os.path.join(
276
+ APP_DIR,
277
+ "test",
278
+ snake_module_name,
279
+ snake_entity_name,
280
+ f"test_update_{snake_entity_name}.py",
281
+ )
282
+ existing_code = read_file(test_file_path)
283
+ new_code = existing_code
284
+ # TODO: update test
285
+ write_file(test_file_path, new_code)
286
+
287
+
288
+ def update_fastapp_test_delete(ctx: AnyContext):
289
+ snake_module_name = to_snake_case(ctx.input.module)
290
+ snake_entity_name = to_snake_case(ctx.input.entity)
291
+ test_file_path = os.path.join(
292
+ APP_DIR,
293
+ "test",
294
+ snake_module_name,
295
+ snake_entity_name,
296
+ f"test_delete_{snake_entity_name}.py",
297
+ )
298
+ existing_code = read_file(test_file_path)
299
+ new_code = existing_code
300
+ # TODO: update test
301
+ write_file(test_file_path, new_code)
@@ -7,6 +7,7 @@ from my_app_name._zrb.entity.add_entity_util import (
7
7
  get_existing_auth_migration_file_names,
8
8
  get_existing_auth_migration_xcom_key,
9
9
  get_remove_permission_migration_script,
10
+ is_gateway_navigation_config_file,
10
11
  is_in_app_schema_dir,
11
12
  is_in_module_entity_dir,
12
13
  is_in_module_entity_test_dir,
@@ -14,11 +15,13 @@ from my_app_name._zrb.entity.add_entity_util import (
14
15
  is_module_client_file,
15
16
  is_module_direct_client_file,
16
17
  is_module_gateway_subroute_file,
18
+ is_module_gateway_subroute_view_file,
17
19
  is_module_migration_metadata_file,
18
20
  is_module_route_file,
19
21
  update_api_client_file,
20
22
  update_client_file,
21
23
  update_direct_client_file,
24
+ update_gateway_navigation_config_file,
22
25
  update_gateway_subroute_file,
23
26
  update_migration_metadata_file,
24
27
  update_route_file,
@@ -88,7 +91,9 @@ scaffold_my_app_name_entity = Scaffolder(
88
91
  destination_path=APP_DIR,
89
92
  transform_path={
90
93
  "my_module": "{to_snake_case(ctx.input.module)}",
94
+ "my-module": "{to_kebab_case(ctx.input.module)}",
91
95
  "my_entity": "{to_snake_case(ctx.input.entity)}",
96
+ "my-entity": "{to_kebab_case(ctx.input.entity)}",
92
97
  },
93
98
  transform_content=[
94
99
  # Schema tranformation (my_app_name/schema/snake_entity_name)
@@ -167,6 +172,24 @@ scaffold_my_app_name_entity = Scaffolder(
167
172
  match=is_module_gateway_subroute_file,
168
173
  transform=update_gateway_subroute_file,
169
174
  ),
175
+ # Update module gateway subroute view
176
+ # (my_app_name/module/gateway/view/content/kebab-module-name/kebab-entity-name.py)
177
+ ContentTransformer(
178
+ name="transform-module-gateway-subroute-view",
179
+ match=is_module_gateway_subroute_view_file,
180
+ transform={
181
+ "My Entity": "{to_human_case(ctx.input.entity).title()}",
182
+ "my-entities": "{to_kebab_case(ctx.input.plural)}",
183
+ "My Column": "{to_human_case(ctx.input.column).title()}",
184
+ "my_column": "{to_snake_case(ctx.input.column)}",
185
+ },
186
+ ),
187
+ # Register entity's page to my_app_name/gateway/config/navigation.py
188
+ ContentTransformer(
189
+ name="transform-gateway-navigation-config",
190
+ match=is_gateway_navigation_config_file,
191
+ transform=update_gateway_navigation_config_file,
192
+ ),
170
193
  ],
171
194
  retries=0,
172
195
  upstream=validate_add_my_app_name_entity,
@@ -283,7 +306,7 @@ def update_my_app_name_entity_permission(ctx: AnyContext):
283
306
  add_my_app_name_entity = app_create_group.add_task(
284
307
  Task(
285
308
  name="add-my-app-name-entity",
286
- description="🏗️ Create new entity on a module",
309
+ description="📦 Create new entity on a module",
287
310
  upstream=[
288
311
  create_my_app_name_entity_migration,
289
312
  update_my_app_name_entity_permission,