zrb 1.0.0a2__py3-none-any.whl → 1.0.0a4__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 (174) hide show
  1. zrb/__init__.py +49 -40
  2. zrb/__main__.py +5 -3
  3. zrb/attr/type.py +2 -1
  4. zrb/builtin/__init__.py +42 -2
  5. zrb/builtin/base64.py +34 -0
  6. zrb/builtin/git.py +156 -0
  7. zrb/builtin/git_subtree.py +88 -0
  8. zrb/builtin/group.py +34 -0
  9. zrb/builtin/llm/llm_chat.py +47 -0
  10. zrb/builtin/llm/tool/cli.py +9 -0
  11. zrb/builtin/llm/tool/rag.py +189 -0
  12. zrb/builtin/llm/tool/web.py +74 -0
  13. zrb/builtin/md5.py +36 -0
  14. zrb/builtin/project/add/fastapp.py +72 -0
  15. zrb/builtin/project/add/fastapp_template/.gitignore +4 -0
  16. zrb/builtin/project/add/fastapp_template/README.md +7 -0
  17. zrb/builtin/project/add/fastapp_template/_zrb/config.py +17 -0
  18. zrb/builtin/project/add/fastapp_template/_zrb/group.py +16 -0
  19. zrb/builtin/project/add/fastapp_template/_zrb/helper.py +97 -0
  20. zrb/builtin/project/add/fastapp_template/_zrb/main.py +132 -0
  21. zrb/builtin/project/add/fastapp_template/_zrb/venv_task.py +22 -0
  22. zrb/builtin/project/add/fastapp_template/common/app.py +18 -0
  23. zrb/builtin/project/add/fastapp_template/common/db_engine.py +5 -0
  24. zrb/builtin/project/add/fastapp_template/common/db_repository.py +134 -0
  25. zrb/builtin/project/add/fastapp_template/common/error.py +8 -0
  26. zrb/builtin/project/add/fastapp_template/common/schema.py +5 -0
  27. zrb/builtin/project/add/fastapp_template/common/usecase.py +232 -0
  28. zrb/builtin/project/add/fastapp_template/config.py +29 -0
  29. zrb/builtin/project/add/fastapp_template/main.py +7 -0
  30. zrb/builtin/project/add/fastapp_template/migrate.py +3 -0
  31. zrb/builtin/project/add/fastapp_template/module/__init__.py +0 -0
  32. zrb/builtin/project/add/fastapp_template/module/auth/alembic.ini +117 -0
  33. zrb/builtin/project/add/fastapp_template/module/auth/client/api_client.py +7 -0
  34. zrb/builtin/project/add/fastapp_template/module/auth/client/base_client.py +27 -0
  35. zrb/builtin/project/add/fastapp_template/module/auth/client/direct_client.py +6 -0
  36. zrb/builtin/project/add/fastapp_template/module/auth/client/factory.py +9 -0
  37. zrb/builtin/project/add/fastapp_template/module/auth/migration/README +1 -0
  38. zrb/builtin/project/add/fastapp_template/module/auth/migration/env.py +108 -0
  39. zrb/builtin/project/add/fastapp_template/module/auth/migration/script.py.mako +26 -0
  40. zrb/builtin/project/add/fastapp_template/module/auth/migration/versions/3093c7336477_add_user_table.py +37 -0
  41. zrb/builtin/project/add/fastapp_template/module/auth/migration_metadata.py +6 -0
  42. zrb/builtin/project/add/fastapp_template/module/auth/route.py +22 -0
  43. zrb/builtin/project/add/fastapp_template/module/auth/service/__init__.py +0 -0
  44. zrb/builtin/project/add/fastapp_template/module/auth/service/user/__init__.py +0 -0
  45. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/__init__.py +0 -0
  46. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/db_repository.py +39 -0
  47. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/factory.py +13 -0
  48. zrb/builtin/project/add/fastapp_template/module/auth/service/user/repository/repository.py +34 -0
  49. zrb/builtin/project/add/fastapp_template/module/auth/service/user/usecase.py +45 -0
  50. zrb/builtin/project/add/fastapp_template/module/gateway/alembic.ini +117 -0
  51. zrb/builtin/project/add/fastapp_template/module/gateway/migration/README +1 -0
  52. zrb/builtin/project/add/fastapp_template/module/gateway/migration/env.py +108 -0
  53. zrb/builtin/project/add/fastapp_template/module/gateway/migration/script.py.mako +26 -0
  54. zrb/builtin/project/add/fastapp_template/module/gateway/migration/versions/.gitkeep +0 -0
  55. zrb/builtin/project/add/fastapp_template/module/gateway/migration_metadata.py +3 -0
  56. zrb/builtin/project/add/fastapp_template/module/gateway/route.py +27 -0
  57. zrb/builtin/project/add/fastapp_template/requirements.txt +6 -0
  58. zrb/builtin/project/add/fastapp_template/schema/__init__.py +0 -0
  59. zrb/builtin/project/add/fastapp_template/schema/role.py +31 -0
  60. zrb/builtin/project/add/fastapp_template/schema/user.py +31 -0
  61. zrb/builtin/project/add/fastapp_template/template.env +2 -0
  62. zrb/builtin/project/create/__init__.py +0 -0
  63. zrb/builtin/project/create/create.py +41 -0
  64. zrb/builtin/project/create/project-template/README.md +3 -0
  65. zrb/builtin/project/create/project-template/zrb_init.py +7 -0
  66. zrb/builtin/python.py +11 -0
  67. zrb/builtin/shell/__init__.py +0 -5
  68. zrb/builtin/shell/autocomplete/__init__.py +0 -9
  69. zrb/builtin/shell/autocomplete/bash.py +5 -6
  70. zrb/builtin/shell/autocomplete/subcmd.py +7 -8
  71. zrb/builtin/shell/autocomplete/zsh.py +5 -6
  72. zrb/builtin/todo.py +219 -0
  73. zrb/callback/any_callback.py +1 -1
  74. zrb/callback/callback.py +5 -5
  75. zrb/cmd/cmd_val.py +2 -2
  76. zrb/config.py +16 -3
  77. zrb/content_transformer/any_content_transformer.py +1 -1
  78. zrb/content_transformer/content_transformer.py +2 -2
  79. zrb/context/any_context.py +1 -1
  80. zrb/context/any_shared_context.py +3 -3
  81. zrb/context/context.py +10 -8
  82. zrb/context/shared_context.py +9 -8
  83. zrb/env/__init__.py +0 -3
  84. zrb/env/any_env.py +1 -1
  85. zrb/env/env.py +3 -4
  86. zrb/env/env_file.py +4 -4
  87. zrb/env/env_map.py +2 -2
  88. zrb/group/__init__.py +0 -3
  89. zrb/group/any_group.py +3 -3
  90. zrb/group/group.py +7 -6
  91. zrb/input/any_input.py +1 -1
  92. zrb/input/base_input.py +4 -4
  93. zrb/input/bool_input.py +5 -5
  94. zrb/input/float_input.py +3 -3
  95. zrb/input/int_input.py +3 -3
  96. zrb/input/option_input.py +51 -0
  97. zrb/input/password_input.py +2 -2
  98. zrb/input/str_input.py +1 -1
  99. zrb/input/text_input.py +12 -10
  100. zrb/runner/cli.py +80 -45
  101. zrb/runner/web_app.py +150 -0
  102. zrb/runner/web_controller/__init__.py +0 -0
  103. zrb/runner/web_controller/group_info_ui/__init__.py +0 -0
  104. zrb/runner/{web_app → web_controller}/group_info_ui/controller.py +7 -8
  105. zrb/runner/{web_app → web_controller}/group_info_ui/view.html +2 -2
  106. zrb/runner/web_controller/home_page/__init__.py +0 -0
  107. zrb/runner/{web_app → web_controller}/home_page/controller.py +7 -6
  108. zrb/runner/{web_app → web_controller}/home_page/view.html +2 -2
  109. zrb/runner/web_controller/task_ui/__init__.py +0 -0
  110. zrb/runner/{web_app → web_controller}/task_ui/controller.py +8 -12
  111. zrb/runner/{web_app → web_controller}/task_ui/view.html +2 -2
  112. zrb/runner/web_util.py +5 -35
  113. zrb/session/any_session.py +13 -7
  114. zrb/session/session.py +78 -40
  115. zrb/session_state_log/session_state_log.py +7 -5
  116. zrb/session_state_logger/any_session_state_logger.py +1 -1
  117. zrb/session_state_logger/default_session_state_logger.py +2 -2
  118. zrb/session_state_logger/file_session_state_logger.py +19 -27
  119. zrb/task/any_task.py +4 -4
  120. zrb/task/base_task.py +33 -23
  121. zrb/task/base_trigger.py +11 -12
  122. zrb/task/cmd_task.py +72 -65
  123. zrb/task/http_check.py +13 -13
  124. zrb/task/llm_task.py +215 -0
  125. zrb/task/make_task.py +9 -9
  126. zrb/task/rsync_task.py +25 -25
  127. zrb/task/scaffolder.py +18 -15
  128. zrb/task/scheduler.py +6 -7
  129. zrb/task/task.py +1 -1
  130. zrb/task/tcp_check.py +11 -13
  131. zrb/util/attr.py +19 -3
  132. zrb/util/cli/style.py +71 -2
  133. zrb/util/cli/subcommand.py +2 -2
  134. zrb/util/codemod/__init__.py +0 -0
  135. zrb/util/codemod/add_code_to_class.py +35 -0
  136. zrb/util/codemod/add_code_to_function.py +36 -0
  137. zrb/util/codemod/add_code_to_method.py +55 -0
  138. zrb/util/codemod/add_key_to_dict.py +51 -0
  139. zrb/util/codemod/add_param_to_function_call.py +39 -0
  140. zrb/util/codemod/add_property_to_class.py +55 -0
  141. zrb/util/git.py +156 -0
  142. zrb/util/git_subtree.py +94 -0
  143. zrb/util/group.py +2 -2
  144. zrb/util/llm/tool.py +63 -0
  145. zrb/util/string/conversion.py +7 -0
  146. zrb/util/todo.py +259 -0
  147. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/METADATA +13 -5
  148. zrb-1.0.0a4.dist-info/RECORD +197 -0
  149. zrb/builtin/shell/_group.py +0 -9
  150. zrb/builtin/shell/autocomplete/_group.py +0 -6
  151. zrb/runner/web_app/any_request_handler.py +0 -24
  152. zrb/runner/web_server.py +0 -224
  153. zrb-1.0.0a2.dist-info/RECORD +0 -120
  154. /zrb/{runner/web_app → builtin/project}/__init__.py +0 -0
  155. /zrb/{runner/web_app/group_info_ui → builtin/project/add}/__init__.py +0 -0
  156. /zrb/{runner/web_app/home_page → builtin/project/add/fastapp_template}/__init__.py +0 -0
  157. /zrb/{runner/web_app/task_ui → builtin/project/add/fastapp_template/common}/__init__.py +0 -0
  158. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_info.html +0 -0
  159. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/group_li.html +0 -0
  160. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_info.html +0 -0
  161. /zrb/runner/{web_app → web_controller}/group_info_ui/partial/task_li.html +0 -0
  162. /zrb/runner/{web_app → web_controller}/home_page/partial/group_info.html +0 -0
  163. /zrb/runner/{web_app → web_controller}/home_page/partial/group_li.html +0 -0
  164. /zrb/runner/{web_app → web_controller}/home_page/partial/task_info.html +0 -0
  165. /zrb/runner/{web_app → web_controller}/home_page/partial/task_li.html +0 -0
  166. /zrb/runner/{web_app → web_controller}/static/favicon-32x32.png +0 -0
  167. /zrb/runner/{web_app → web_controller}/static/pico.min.css +0 -0
  168. /zrb/runner/{web_app → web_controller}/task_ui/partial/common-util.js +0 -0
  169. /zrb/runner/{web_app → web_controller}/task_ui/partial/input.html +0 -0
  170. /zrb/runner/{web_app → web_controller}/task_ui/partial/main.js +0 -0
  171. /zrb/runner/{web_app → web_controller}/task_ui/partial/show-existing-session.js +0 -0
  172. /zrb/runner/{web_app → web_controller}/task_ui/partial/visualize-history.js +0 -0
  173. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/WHEEL +0 -0
  174. {zrb-1.0.0a2.dist-info → zrb-1.0.0a4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,189 @@
1
+ import json
2
+ import os
3
+ import sys
4
+ from collections.abc import Callable, Iterable
5
+
6
+ import litellm
7
+
8
+ from zrb.config import (
9
+ RAG_CHUNK_SIZE,
10
+ RAG_EMBEDDING_MODEL,
11
+ RAG_MAX_RESULT_COUNT,
12
+ RAG_OVERLAP,
13
+ )
14
+ from zrb.util.cli.style import stylize_error, stylize_faint
15
+ from zrb.util.run import run_async
16
+
17
+ Document = str | Callable[[], str]
18
+ Documents = Callable[[], Iterable[Document]] | Iterable[Document]
19
+
20
+
21
+ def create_rag_from_directory(
22
+ tool_name: str,
23
+ tool_description: str,
24
+ document_dir_path: str = "./documents",
25
+ model: str = RAG_EMBEDDING_MODEL,
26
+ vector_db_path: str = "./chroma",
27
+ vector_db_collection: str = "documents",
28
+ chunk_size: int = RAG_CHUNK_SIZE,
29
+ overlap: int = RAG_OVERLAP,
30
+ max_result_count: int = RAG_MAX_RESULT_COUNT,
31
+ ):
32
+ return create_rag(
33
+ tool_name=tool_name,
34
+ tool_description=tool_description,
35
+ documents=get_rag_documents(os.path.expanduser(document_dir_path)),
36
+ model=model,
37
+ vector_db_path=vector_db_path,
38
+ vector_db_collection=vector_db_collection,
39
+ reset_db=get_rag_reset_db(
40
+ document_dir_path=os.path.expanduser(document_dir_path),
41
+ vector_db_path=os.path.expanduser(vector_db_path),
42
+ ),
43
+ chunk_size=chunk_size,
44
+ overlap=overlap,
45
+ max_result_count=max_result_count,
46
+ )
47
+
48
+
49
+ def create_rag(
50
+ tool_name: str,
51
+ tool_description: str,
52
+ documents: Documents = [],
53
+ model: str = RAG_EMBEDDING_MODEL,
54
+ vector_db_path: str = "./chroma",
55
+ vector_db_collection: str = "documents",
56
+ reset_db: Callable[[], bool] | bool = False,
57
+ chunk_size: int = RAG_CHUNK_SIZE,
58
+ overlap: int = RAG_OVERLAP,
59
+ max_result_count: int = RAG_MAX_RESULT_COUNT,
60
+ ) -> Callable[[str], str]:
61
+ async def retrieve(query: str) -> str:
62
+ import chromadb
63
+ from chromadb.config import Settings
64
+
65
+ is_db_exist = os.path.isdir(vector_db_path)
66
+ client = chromadb.PersistentClient(
67
+ path=vector_db_path, settings=Settings(allow_reset=True)
68
+ )
69
+ should_reset_db = (
70
+ await run_async(reset_db()) if callable(reset_db) else reset_db
71
+ )
72
+ if (not is_db_exist) or should_reset_db:
73
+ client.reset()
74
+ collection = client.get_or_create_collection(vector_db_collection)
75
+ chunk_index = 0
76
+ print(stylize_faint("Scanning documents"), file=sys.stderr)
77
+ docs = await run_async(documents()) if callable(documents) else documents
78
+ for document in docs:
79
+ if callable(document):
80
+ try:
81
+ document = await run_async(document())
82
+ except Exception as error:
83
+ print(stylize_error(f"Error: {error}"), file=sys.stderr)
84
+ continue
85
+ for i in range(0, len(document), chunk_size - overlap):
86
+ chunk = document[i : i + chunk_size]
87
+ if len(chunk) > 0:
88
+ print(
89
+ stylize_faint(f"Vectorize chunk {chunk_index}"),
90
+ file=sys.stderr,
91
+ )
92
+ response = await litellm.aembedding(model=model, input=[chunk])
93
+ vector = response["data"][0]["embedding"]
94
+ print(
95
+ stylize_faint(f"Adding chunk {chunk_index} to db"),
96
+ file=sys.stderr,
97
+ )
98
+ collection.upsert(
99
+ ids=[f"id{chunk_index}"],
100
+ embeddings=[vector],
101
+ documents=[chunk],
102
+ )
103
+ chunk_index += 1
104
+ collection = client.get_or_create_collection(vector_db_collection)
105
+ # Generate embedding for the query
106
+ print(stylize_faint("Vectorize query"), file=sys.stderr)
107
+ query_response = await litellm.aembedding(model=model, input=[query])
108
+ print(stylize_faint("Search documents"), file=sys.stderr)
109
+ # Search for the top_k most similar documents
110
+ results = collection.query(
111
+ query_embeddings=query_response["data"][0]["embedding"],
112
+ n_results=max_result_count,
113
+ )
114
+ return json.dumps(results)
115
+
116
+ retrieve.__name__ = tool_name
117
+ retrieve.__doc__ = tool_description
118
+ return retrieve
119
+
120
+
121
+ def get_rag_documents(document_dir_path: str) -> Callable[[], list[Callable[[], str]]]:
122
+ def get_documents() -> list[Callable[[], str]]:
123
+ # Walk through the directory
124
+ readers = []
125
+ for root, _, files in os.walk(document_dir_path):
126
+ for file in files:
127
+ file_path = os.path.join(root, file)
128
+ if file_path.lower().endswith(".pdf"):
129
+ readers.append(_get_pdf_reader(file_path))
130
+ continue
131
+ readers.append(_get_text_reader(file_path))
132
+ return readers
133
+
134
+ return get_documents
135
+
136
+
137
+ def _get_text_reader(file_path: str):
138
+ def read():
139
+ print(stylize_faint(f"Start reading {file_path}"), file=sys.stderr)
140
+ with open(file_path, "r", encoding="utf-8") as f:
141
+ content = f.read()
142
+ print(stylize_faint(f"Complete reading {file_path}"), file=sys.stderr)
143
+ return content
144
+
145
+ return read
146
+
147
+
148
+ def _get_pdf_reader(file_path):
149
+ def read():
150
+ import pdfplumber
151
+
152
+ print(stylize_faint(f"Start reading {file_path}"), file=sys.stderr)
153
+ contents = []
154
+ with pdfplumber.open(file_path) as pdf:
155
+ for page in pdf.pages:
156
+ contents.append(page.extract_text())
157
+ print(stylize_faint(f"Complete reading {file_path}"), file=sys.stderr)
158
+ return "\n".join(contents)
159
+
160
+ return read
161
+
162
+
163
+ def get_rag_reset_db(
164
+ document_dir_path: str, vector_db_path: str = "./chroma"
165
+ ) -> Callable[[], bool]:
166
+ def should_reset_db() -> bool:
167
+ document_exist = os.path.isdir(document_dir_path)
168
+ if not document_exist:
169
+ raise ValueError(f"Document directory not exists: {document_dir_path}")
170
+ vector_db_exist = os.path.isdir(vector_db_path)
171
+ if not vector_db_exist:
172
+ return True
173
+ document_mtime = _get_most_recent_mtime(document_dir_path)
174
+ vector_db_mtime = _get_most_recent_mtime(vector_db_path)
175
+ return document_mtime > vector_db_mtime
176
+
177
+ return should_reset_db
178
+
179
+
180
+ def _get_most_recent_mtime(directory):
181
+ most_recent_mtime = 0
182
+ for root, dirs, files in os.walk(directory):
183
+ # Check mtime for directories
184
+ for name in dirs + files:
185
+ file_path = os.path.join(root, name)
186
+ mtime = os.path.getmtime(file_path)
187
+ if mtime > most_recent_mtime:
188
+ most_recent_mtime = mtime
189
+ return most_recent_mtime
@@ -0,0 +1,74 @@
1
+ import json
2
+ from typing import Annotated
3
+
4
+
5
+ def open_web_page(url: str) -> str:
6
+ """Get content from a web page."""
7
+ import requests
8
+
9
+ response = requests.get(
10
+ url,
11
+ headers={
12
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa
13
+ },
14
+ )
15
+ if response.status_code != 200:
16
+ raise Exception(
17
+ f"Error: Unable to retrieve search results (status code: {response.status_code})" # noqa
18
+ )
19
+ return json.dumps(parse_html_text(response.text))
20
+
21
+
22
+ def query_internet(
23
+ query: Annotated[str, "Search query"],
24
+ num_results: Annotated[int, "Search result count, by default 10"] = 10,
25
+ ) -> str:
26
+ """Search factual information from the internet by using Google."""
27
+ import requests
28
+
29
+ response = requests.get(
30
+ "https://google.com/search",
31
+ headers={
32
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" # noqa
33
+ },
34
+ params={
35
+ "q": query,
36
+ "num": num_results,
37
+ "hl": "en",
38
+ "safe": "off",
39
+ },
40
+ )
41
+ if response.status_code != 200:
42
+ raise Exception(
43
+ f"Error: Unable to retrieve search results (status code: {response.status_code})" # noqa
44
+ )
45
+ return json.dumps(parse_html_text(response.text))
46
+
47
+
48
+ def parse_html_text(html_text: str) -> dict[str, str]:
49
+ from bs4 import BeautifulSoup
50
+
51
+ ignored_tags = [
52
+ "script",
53
+ "link",
54
+ "meta",
55
+ "style",
56
+ "code",
57
+ "footer",
58
+ "nav",
59
+ "header",
60
+ "aside",
61
+ ]
62
+ soup = BeautifulSoup(html_text, "html.parser")
63
+ links = []
64
+ for anchor in soup.find_all("a"):
65
+ if not anchor or "href" not in anchor.attrs:
66
+ continue
67
+ link: str = anchor["href"]
68
+ if link.startswith("#") or link.startswith("/"):
69
+ continue
70
+ links.append(link)
71
+ for tag in soup(ignored_tags):
72
+ tag.decompose()
73
+ html_text = soup.get_text(separator=" ", strip=True)
74
+ return {"content": html_text, "links_on_page": links}
zrb/builtin/md5.py ADDED
@@ -0,0 +1,36 @@
1
+ from zrb.builtin.group import md5_group
2
+ from zrb.context.any_context import AnyContext
3
+ from zrb.input.str_input import StrInput
4
+ from zrb.task.make_task import make_task
5
+
6
+
7
+ @make_task(
8
+ name="hash-md5",
9
+ input=StrInput(name="text", description="Text", prompt="Text to encode"),
10
+ description="🧩 Hash text to md5",
11
+ group=md5_group,
12
+ alias="hash",
13
+ )
14
+ def hash_md5(ctx: AnyContext) -> str:
15
+ import hashlib
16
+
17
+ result = hashlib.md5(ctx.input.text.encode()).hexdigest()
18
+ ctx.print(result)
19
+ return result
20
+
21
+
22
+ @make_task(
23
+ name="sum-md5",
24
+ input=StrInput(name="file", description="File name", prompt="File name"),
25
+ description="➕ Get md5 checksum of a file",
26
+ group=md5_group,
27
+ alias="sum",
28
+ )
29
+ def sum_md5(ctx: AnyContext) -> str:
30
+ import hashlib
31
+
32
+ with open(ctx.input.file, mode="rb") as file:
33
+ content = file.read()
34
+ result = hashlib.md5(content).hexdigest()
35
+ ctx.print(result)
36
+ return result
@@ -0,0 +1,72 @@
1
+ import os
2
+
3
+ from zrb.builtin.group import add_to_project_group
4
+ from zrb.context.any_context import AnyContext
5
+ from zrb.input.str_input import StrInput
6
+ from zrb.task.make_task import make_task
7
+ from zrb.task.scaffolder import Scaffolder
8
+ from zrb.task.task import Task
9
+ from zrb.util.string.conversion import double_quote
10
+ from zrb.util.string.name import get_random_name
11
+
12
+ _DIR = os.path.dirname(__file__)
13
+
14
+
15
+ scaffold_fastapp = Scaffolder(
16
+ name="scaffold-fastapp",
17
+ input=[
18
+ StrInput(
19
+ name="project-dir",
20
+ description="Project directory",
21
+ prompt="Project directory",
22
+ default_str=lambda _: os.getcwd(),
23
+ ),
24
+ StrInput(
25
+ name="app-name",
26
+ description="App name",
27
+ prompt="App name",
28
+ default_str=lambda _: get_random_name(separator="_"),
29
+ ),
30
+ ],
31
+ source_path=os.path.join(_DIR, "fastapp_template"),
32
+ render_source_path=False,
33
+ destination_path=lambda ctx: os.path.join(
34
+ ctx.input["project-dir"], ctx.input["app-name"]
35
+ ),
36
+ transform_content={
37
+ "fastapp_template": "{to_snake_case(ctx.input['app-name'])}",
38
+ "App Name": "{ctx.input['app-name'].title()}",
39
+ "App name": "{ctx.input['app-name'].capitalize()}",
40
+ "app-name": "{to_kebab_case(ctx.input['app-name'])}",
41
+ "app_name": "{to_snake_case(ctx.input['app-name'])}",
42
+ "APP_NAME": "{to_snake_case(ctx.input['app-name']).upper()}",
43
+ "secure-password": lambda _: get_random_name(),
44
+ },
45
+ retries=0,
46
+ )
47
+
48
+
49
+ @make_task(
50
+ name="register-fastapp-automation",
51
+ )
52
+ def register_fastapp_automation(ctx: AnyContext):
53
+ project_dir_path = ctx.input["project-dir"]
54
+ zrb_init_path = os.path.join(project_dir_path, "zrb_init.py")
55
+ app_dir_path = ctx.input["app-name"]
56
+ app_automation_file_part = ", ".join(
57
+ [double_quote(part) for part in [app_dir_path, "_zrb", "main.py"]]
58
+ )
59
+ with open(zrb_init_path, "+a") as f:
60
+ f.write(f"load_file(os.path.join(_DIR, {app_automation_file_part}))\n")
61
+
62
+
63
+ scaffold_fastapp >> register_fastapp_automation
64
+
65
+ add_fastapp_to_project = add_to_project_group.add_task(
66
+ Task(
67
+ name="add-fastapp",
68
+ description="🚀 Add FastApp to project",
69
+ ),
70
+ alias="fastapp",
71
+ )
72
+ add_fastapp_to_project << [register_fastapp_automation]
@@ -0,0 +1,4 @@
1
+ __pycache__
2
+ .venv
3
+ .env
4
+ *.db
@@ -0,0 +1,7 @@
1
+ # App Name
2
+
3
+ # Principle
4
+
5
+ - Developer should be able to override everything with sane amount of code.
6
+ - Simple tasks should only require small amount of code.
7
+ - A bit of magic is okay as long as transparent and documented.
@@ -0,0 +1,17 @@
1
+ import os
2
+ import platform
3
+
4
+ DIR = os.path.dirname(__file__)
5
+ APP_DIR = os.path.dirname(DIR)
6
+ APP_MODULE_NAME = os.path.basename(APP_DIR)
7
+
8
+ MICROSERVICES_ENV_VARS = {
9
+ "APP_NAME_MODE": "microservices",
10
+ "APP_NAME_AUTH_BASE_URL": "http://localhost:3002",
11
+ }
12
+ MONOLITH_ENV_VARS = {"APP_NAME_MODE": "monolith"}
13
+
14
+ if platform.system() == "Windows":
15
+ ACTIVATE_VENV_SCRIPT = "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser; . .venv\Scripts\Activate" # noqa
16
+ else:
17
+ ACTIVATE_VENV_SCRIPT = "source .venv/bin/activate"
@@ -0,0 +1,16 @@
1
+ from zrb import Group
2
+ from zrb.builtin import project_group
3
+
4
+ app_group = project_group.add_group(
5
+ Group(name="app-name", description="🚀 Managing App Name")
6
+ )
7
+
8
+ app_run_group = app_group.add_group(Group(name="run", description="🟢 Run App Name"))
9
+
10
+ app_migrate_group = app_group.add_group(
11
+ Group(name="migrate", description="📦 Run App Name DB migration")
12
+ )
13
+
14
+ app_create_group = app_group.add_group(
15
+ Group(name="create", description="✨ Create resources for App Name")
16
+ )
@@ -0,0 +1,97 @@
1
+ import os
2
+
3
+ from fastapp_template._zrb.config import (
4
+ ACTIVATE_VENV_SCRIPT,
5
+ APP_DIR,
6
+ MICROSERVICES_ENV_VARS,
7
+ MONOLITH_ENV_VARS,
8
+ )
9
+
10
+ from zrb import Cmd, CmdTask, EnvFile, EnvMap, StrInput, Task
11
+
12
+
13
+ def create_migration(name: str, module: str) -> Task:
14
+ return CmdTask(
15
+ name=f"create-app-name-{name}-migration",
16
+ description=f"🧩 Create App Name {name.capitalize()} DB migration",
17
+ input=StrInput(
18
+ name="message",
19
+ description="Migration message",
20
+ prompt="Migration message",
21
+ allow_empty=False,
22
+ ),
23
+ env=[
24
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
25
+ EnvMap(
26
+ vars={
27
+ "APP_DB_URL": f"sqlite:///{APP_DIR}/.migration.{module}.db",
28
+ "APP_NAME_MODULES": f"{module}",
29
+ }
30
+ ),
31
+ ],
32
+ cwd=APP_DIR,
33
+ cmd=[
34
+ ACTIVATE_VENV_SCRIPT,
35
+ f"cd {os.path.join(APP_DIR, 'module', module)}",
36
+ "alembic upgrade head",
37
+ Cmd(
38
+ "alembic revision --autogenerate -m {double_quote(ctx.input.message)}",
39
+ auto_render=True,
40
+ ),
41
+ ],
42
+ render_cmd=False,
43
+ retries=2,
44
+ )
45
+
46
+
47
+ def migrate_module(name: str, module: str, as_microservices: bool) -> Task:
48
+ env_vars = MICROSERVICES_ENV_VARS if as_microservices else MONOLITH_ENV_VARS
49
+ return CmdTask(
50
+ name=(
51
+ f"migrate-app-name-{name}"
52
+ if as_microservices
53
+ else f"migrate-{name}-on-monolith"
54
+ ),
55
+ description=f"🧩 Run App Name {name.capitalize()} DB migration",
56
+ env=[
57
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
58
+ EnvMap(
59
+ vars={
60
+ **env_vars,
61
+ "APP_NAME_MODULES": f"{module}",
62
+ }
63
+ ),
64
+ ],
65
+ cwd=APP_DIR,
66
+ cmd=[
67
+ ACTIVATE_VENV_SCRIPT,
68
+ f"cd {os.path.join(APP_DIR, 'module', module)}",
69
+ "alembic upgrade head",
70
+ ],
71
+ render_cmd=False,
72
+ retries=2,
73
+ )
74
+
75
+
76
+ def run_microservice(name: str, port: int, module: str) -> Task:
77
+ return CmdTask(
78
+ name=f"run-app-name-{name}",
79
+ description=f"🧩 Run App Name {name.capitalize()}",
80
+ env=[
81
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
82
+ EnvMap(
83
+ vars={
84
+ **MICROSERVICES_ENV_VARS,
85
+ "APP_NAME_PORT": f"{port}",
86
+ "APP_NAME_MODULES": f"{module}",
87
+ }
88
+ ),
89
+ ],
90
+ cwd=APP_DIR,
91
+ cmd=[
92
+ ACTIVATE_VENV_SCRIPT,
93
+ 'fastapi dev main.py --port "${APP_NAME_PORT}"',
94
+ ],
95
+ render_cmd=False,
96
+ retries=2,
97
+ )
@@ -0,0 +1,132 @@
1
+ import os
2
+
3
+ from fastapp_template._zrb.config import ACTIVATE_VENV_SCRIPT, APP_DIR
4
+ from fastapp_template._zrb.group import (
5
+ app_create_group,
6
+ app_migrate_group,
7
+ app_run_group,
8
+ )
9
+ from fastapp_template._zrb.helper import (
10
+ create_migration,
11
+ migrate_module,
12
+ run_microservice,
13
+ )
14
+ from fastapp_template._zrb.venv_task import prepare_venv
15
+
16
+ from zrb import CmdTask, Env, EnvFile, Task
17
+
18
+ # 🚀 Run/Migrate All ===========================================================
19
+
20
+ run_all = app_run_group.add_task(
21
+ Task(
22
+ name="run-app-name", description="🟢 Run App Name as monolith and microservices"
23
+ ),
24
+ alias="all",
25
+ )
26
+
27
+ migrate_all = app_migrate_group.add_task(
28
+ Task(
29
+ name="migrate-app-name",
30
+ description="📦 Run App Name DB migration for monolith and microservices",
31
+ ),
32
+ alias="all",
33
+ )
34
+
35
+ create_all_migration = app_create_group.add_task(
36
+ Task(
37
+ name="create-app-name-migration", description="📦 Create App Name DB migration"
38
+ ),
39
+ alias="migration",
40
+ )
41
+
42
+ # 🗿 Run/Migrate Monolith =====================================================
43
+
44
+ run_monolith = app_run_group.add_task(
45
+ CmdTask(
46
+ name="run-monolith-app-name",
47
+ description="🗿 Run App Name as a monolith",
48
+ env=[
49
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
50
+ Env(name="APP_NAME_MODE", default="monolith"),
51
+ ],
52
+ cwd=APP_DIR,
53
+ cmd=[
54
+ ACTIVATE_VENV_SCRIPT,
55
+ 'fastapi dev main.py --port "${APP_NAME_PORT}"',
56
+ ],
57
+ render_cmd=False,
58
+ retries=2,
59
+ ),
60
+ alias="monolith",
61
+ )
62
+ prepare_venv >> run_monolith >> run_all
63
+
64
+ migrate_monolith = app_migrate_group.add_task(
65
+ Task(
66
+ name="migrate-monolith-app-name",
67
+ description="🗿 Run App Name DB migration for monolith",
68
+ ),
69
+ alias="monolith",
70
+ )
71
+ migrate_monolith >> migrate_all
72
+
73
+ # 🌐 Run/Migrate Microsevices ==================================================
74
+
75
+ run_microservices = app_run_group.add_task(
76
+ Task(
77
+ name="run-microservices-app-name",
78
+ description="🌐 Run App Name as microservices",
79
+ ),
80
+ alias="microservices",
81
+ )
82
+ run_microservices >> run_all
83
+
84
+ migrate_microservices = app_migrate_group.add_task(
85
+ Task(
86
+ name="migrate-microservices-app-name",
87
+ description="🌐 Run App Name DB migration for microservices",
88
+ ),
89
+ alias="microservices",
90
+ )
91
+ migrate_microservices >> migrate_all
92
+
93
+ # 📡 Run/Migrate Gateway =======================================================
94
+
95
+ run_gateway = app_run_group.add_task(
96
+ run_microservice("gateway", 3001, "gateway"), alias="microservices-gateway"
97
+ )
98
+ prepare_venv >> run_gateway >> run_microservices
99
+
100
+ create_gateway_migration = app_create_group.add_task(
101
+ create_migration("gateway", "gateway"), alias="gateway-migration"
102
+ )
103
+ prepare_venv >> create_gateway_migration >> create_all_migration
104
+
105
+ migrate_monolith_gateway = migrate_module("gateway", "gateway", as_microservices=False)
106
+ prepare_venv >> migrate_monolith_gateway >> [migrate_monolith, run_monolith]
107
+
108
+ migrate_microservices_gateway = app_migrate_group.add_task(
109
+ migrate_module("gateway", "gateway", as_microservices=True),
110
+ alias="microservices-gateway",
111
+ )
112
+ prepare_venv >> migrate_microservices_gateway >> [migrate_microservices, run_gateway]
113
+
114
+ # 🔐 Run/Migrate Auth ==========================================================
115
+
116
+ run_auth = app_run_group.add_task(
117
+ run_microservice("auth", 3002, "auth"), alias="microservices-auth"
118
+ )
119
+ prepare_venv >> run_auth >> run_microservices
120
+
121
+ create_auth_migration = app_create_group.add_task(
122
+ create_migration("auth", "auth"), alias="auth-migration"
123
+ )
124
+ prepare_venv >> create_auth_migration >> create_all_migration
125
+
126
+ migrate_monolith_auth = migrate_module("auth", "auth", as_microservices=False)
127
+ prepare_venv >> migrate_monolith_auth >> [migrate_monolith, run_monolith]
128
+
129
+ migrate_microservices_auth = app_migrate_group.add_task(
130
+ migrate_module("auth", "auth", as_microservices=True), alias="microservices-auth"
131
+ )
132
+ prepare_venv >> migrate_microservices_auth >> [migrate_microservices, run_auth]
@@ -0,0 +1,22 @@
1
+ import os
2
+
3
+ from fastapp_template._zrb.config import ACTIVATE_VENV_SCRIPT, APP_DIR
4
+ from fastapp_template._zrb.group import app_group
5
+
6
+ from zrb import CmdTask
7
+
8
+ create_venv = CmdTask(
9
+ name="create-app-name-venv",
10
+ cwd=APP_DIR,
11
+ cmd="python -m venv .venv",
12
+ execute_condition=lambda _: not os.path.isdir(os.path.join(APP_DIR, ".venv")),
13
+ )
14
+
15
+ prepare_venv = CmdTask(
16
+ name="prepare-app-name-venv",
17
+ cmd=[ACTIVATE_VENV_SCRIPT, "pip install -r requirements.txt"],
18
+ cwd=APP_DIR,
19
+ )
20
+ create_venv >> prepare_venv
21
+
22
+ app_group.add_task(create_venv, alias="prepare")
@@ -0,0 +1,18 @@
1
+ from contextlib import asynccontextmanager
2
+
3
+ from fastapi import FastAPI
4
+ from fastapp_template.common.db_engine import engine
5
+ from fastapp_template.config import APP_MODE, APP_MODULES
6
+ from sqlmodel import SQLModel
7
+
8
+
9
+ @asynccontextmanager
10
+ async def lifespan(app: FastAPI):
11
+ SQLModel.metadata.create_all(engine)
12
+ yield
13
+
14
+
15
+ app_title = (
16
+ "App Name" if APP_MODE == "monolith" else f"App Name - {', '.join(APP_MODULES)}"
17
+ )
18
+ app = FastAPI(title=app_title, lifespan=lifespan)