FastAPI-fastkit 1.0.2__py3-none-any.whl → 1.1.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 (174) hide show
  1. fastapi_fastkit/__init__.py +1 -10
  2. fastapi_fastkit/backend/inspector.py +933 -83
  3. fastapi_fastkit/backend/main.py +640 -482
  4. fastapi_fastkit/backend/package_managers/__init__.py +22 -0
  5. fastapi_fastkit/backend/package_managers/base.py +150 -0
  6. fastapi_fastkit/backend/package_managers/factory.py +151 -0
  7. fastapi_fastkit/backend/package_managers/pdm_manager.py +275 -0
  8. fastapi_fastkit/backend/package_managers/pip_manager.py +205 -0
  9. fastapi_fastkit/backend/package_managers/poetry_manager.py +378 -0
  10. fastapi_fastkit/backend/package_managers/uv_manager.py +327 -0
  11. fastapi_fastkit/backend/transducer.py +210 -106
  12. fastapi_fastkit/cli.py +702 -664
  13. fastapi_fastkit/core/exceptions.py +14 -3
  14. fastapi_fastkit/core/settings.py +181 -155
  15. fastapi_fastkit/fastapi_project_template/README.md +90 -90
  16. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.env-tpl +1 -1
  17. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/.gitignore-tpl +30 -30
  18. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl +107 -107
  19. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +36 -36
  20. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/format.sh-tpl +5 -5
  21. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/lint.sh-tpl +5 -5
  22. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/run-server.sh-tpl +8 -8
  23. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/scripts/test.sh-tpl +6 -6
  24. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.cfg-tpl +14 -14
  25. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/setup.py-tpl +25 -25
  26. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/api.py-tpl +9 -9
  27. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/api/routes/items.py-tpl +57 -57
  28. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +64 -64
  29. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/crud/items.py-tpl +21 -21
  30. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/main.py-tpl +26 -26
  31. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/mocks/mock_items.json-tpl +8 -8
  32. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/schemas/items.py-tpl +22 -22
  33. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/conftest.py-tpl +42 -42
  34. fastapi_fastkit/fastapi_project_template/fastapi-async-crud/tests/test_items.py-tpl +48 -48
  35. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.env-tpl +1 -1
  36. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/.gitignore-tpl +191 -191
  37. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl +156 -156
  38. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/requirements.txt-tpl +36 -36
  39. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/format.sh-tpl +5 -5
  40. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/lint.sh-tpl +5 -5
  41. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/run-server.sh-tpl +8 -8
  42. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/scripts/test.sh-tpl +6 -6
  43. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.cfg-tpl +12 -12
  44. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/setup.py-tpl +27 -27
  45. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/api.py-tpl +9 -9
  46. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +172 -172
  47. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +64 -64
  48. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/crud/items.py-tpl +21 -21
  49. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +103 -103
  50. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/pagination.py-tpl +72 -72
  51. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/main.py-tpl +39 -39
  52. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/mocks/mock_items.json-tpl +8 -8
  53. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/base.py-tpl +49 -49
  54. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/schemas/items.py-tpl +22 -22
  55. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/utils/documents.py-tpl +20 -20
  56. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/conftest.py-tpl +42 -42
  57. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/tests/test_items.py-tpl +61 -61
  58. fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl +101 -101
  59. fastapi_fastkit/fastapi_project_template/fastapi-default/scripts/run-server.sh-tpl +8 -8
  60. fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +22 -22
  61. fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/mock_items.json-tpl +8 -8
  62. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/conftest.py-tpl +30 -30
  63. fastapi_fastkit/fastapi_project_template/fastapi-default/tests/test_items.py-tpl +44 -44
  64. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.env-tpl +1 -1
  65. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/.gitignore-tpl +30 -30
  66. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/Dockerfile-tpl +23 -23
  67. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl +119 -119
  68. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/requirements.txt-tpl +36 -36
  69. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/format.sh-tpl +5 -5
  70. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/lint.sh-tpl +5 -5
  71. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/run-server.sh-tpl +8 -8
  72. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/scripts/test.sh-tpl +6 -6
  73. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.cfg-tpl +13 -13
  74. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/setup.py-tpl +22 -22
  75. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/api.py-tpl +6 -6
  76. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/api/routes/items.py-tpl +54 -54
  77. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +64 -64
  78. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/crud/items.py-tpl +15 -15
  79. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/main.py-tpl +26 -26
  80. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/mocks/mock_items.json-tpl +8 -8
  81. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/schemas/items.py-tpl +19 -19
  82. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/conftest.py-tpl +30 -30
  83. fastapi_fastkit/fastapi_project_template/fastapi-dockerized/tests/test_items.py-tpl +44 -44
  84. fastapi_fastkit/fastapi_project_template/fastapi-empty/.env-tpl +1 -1
  85. fastapi_fastkit/fastapi_project_template/fastapi-empty/.gitignore-tpl +30 -0
  86. fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl +85 -0
  87. fastapi_fastkit/fastapi_project_template/fastapi-empty/requirements.txt-tpl +9 -0
  88. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.cfg-tpl +13 -0
  89. fastapi_fastkit/fastapi_project_template/fastapi-empty/setup.py-tpl +22 -22
  90. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +75 -75
  91. fastapi_fastkit/fastapi_project_template/fastapi-empty/src/main.py-tpl +48 -36
  92. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/__init__.py-tpl +0 -0
  93. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/conftest.py-tpl +16 -0
  94. fastapi_fastkit/fastapi_project_template/fastapi-empty/tests/test_main.py-tpl +37 -0
  95. fastapi_fastkit/fastapi_project_template/fastapi-mcp/.env-tpl +29 -0
  96. fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl +249 -0
  97. fastapi_fastkit/fastapi_project_template/fastapi-mcp/requirements.txt-tpl +40 -0
  98. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/format.sh-tpl +12 -0
  99. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/lint.sh-tpl +18 -0
  100. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/run-server.sh-tpl +9 -0
  101. fastapi_fastkit/fastapi_project_template/fastapi-mcp/scripts/test.sh-tpl +9 -0
  102. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.cfg-tpl +90 -0
  103. fastapi_fastkit/fastapi_project_template/fastapi-mcp/setup.py-tpl +51 -0
  104. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/__init__.py-tpl +4 -0
  105. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/__init__.py-tpl +3 -0
  106. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/api.py-tpl +14 -0
  107. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/__init__.py-tpl +3 -0
  108. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/auth.py-tpl +134 -0
  109. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/api/routes/items.py-tpl +189 -0
  110. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/__init__.py-tpl +3 -0
  111. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/auth/dependencies.py-tpl +88 -0
  112. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/__init__.py-tpl +3 -0
  113. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/core/config.py-tpl +60 -0
  114. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/main.py-tpl +66 -0
  115. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/__init__.py-tpl +3 -0
  116. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/mcp/router.py-tpl +100 -0
  117. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/__init__.py-tpl +3 -0
  118. fastapi_fastkit/fastapi_project_template/fastapi-mcp/src/schemas/items.py-tpl +67 -0
  119. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/__init__.py-tpl +3 -0
  120. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/conftest.py-tpl +49 -0
  121. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_auth.py-tpl +101 -0
  122. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_items.py-tpl +96 -0
  123. fastapi_fastkit/fastapi_project_template/fastapi-mcp/tests/test_mcp.py-tpl +81 -0
  124. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.env-tpl +9 -9
  125. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/.gitignore-tpl +30 -30
  126. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/Dockerfile-tpl +23 -23
  127. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl +144 -144
  128. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/alembic.ini-tpl +119 -119
  129. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/docker-compose.yml-tpl +44 -44
  130. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/requirements.txt-tpl +41 -41
  131. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/format.sh-tpl +5 -5
  132. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/lint.sh-tpl +5 -5
  133. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/pre-start.sh-tpl +16 -16
  134. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/run-server.sh-tpl +8 -8
  135. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/scripts/test.sh-tpl +10 -10
  136. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.cfg-tpl +13 -13
  137. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/setup.py-tpl +25 -25
  138. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/README-tpl +1 -1
  139. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/env.py-tpl +85 -85
  140. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/script.py.mako-tpl +26 -26
  141. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/alembic/versions/bedcdc35b64a_first_alembic.py-tpl +37 -37
  142. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/api.py-tpl +11 -11
  143. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/deps.py-tpl +19 -19
  144. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/api/routes/items.py-tpl +109 -109
  145. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/config.py-tpl +88 -88
  146. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/core/db.py-tpl +53 -53
  147. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/crud/items.py-tpl +102 -102
  148. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/main.py-tpl +45 -45
  149. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/schemas/items.py-tpl +54 -54
  150. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/backend_pre_start.py-tpl +39 -39
  151. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/init_data.py-tpl +23 -23
  152. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/src/utils/tests_pre_start.py-tpl +39 -39
  153. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/template-config.yml-tpl +24 -0
  154. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/conftest.py-tpl +63 -63
  155. fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/tests/test_items.py-tpl +44 -44
  156. fastapi_fastkit/fastapi_project_template/modules/api/routes/new_route.py-tpl +64 -64
  157. fastapi_fastkit/fastapi_project_template/modules/crud/new_route.py-tpl +29 -29
  158. fastapi_fastkit/fastapi_project_template/modules/schemas/new_route.py-tpl +23 -23
  159. fastapi_fastkit/utils/logging.py +64 -28
  160. fastapi_fastkit/utils/main.py +180 -140
  161. fastapi_fastkit-1.1.0.dist-info/METADATA +182 -0
  162. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/RECORD +165 -129
  163. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/WHEEL +1 -1
  164. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/licenses/LICENSE +21 -21
  165. fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/.DS_Store +0 -0
  166. fastapi_fastkit/logs/fastkit_debug_20250702_161423.log +0 -21
  167. fastapi_fastkit/logs/fastkit_debug_20250702_162706.log +0 -20
  168. fastapi_fastkit/logs/fastkit_debug_20250702_163010.log +0 -21
  169. fastapi_fastkit/logs/fastkit_debug_20250702_163526.log +0 -21
  170. fastapi_fastkit/logs/fastkit_debug_20250702_164053.log +0 -21
  171. fastapi_fastkit/logs/fastkit_debug_20250702_165220.log +0 -21
  172. fastapi_fastkit/logs/fastkit_debug_20250702_165647.log +0 -21
  173. fastapi_fastkit-1.0.2.dist-info/METADATA +0 -320
  174. {fastapi_fastkit-1.0.2.dist-info → fastapi_fastkit-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,482 +1,640 @@
1
- # --------------------------------------------------------------------------
2
- # The Module defines backend operations for FastAPI-fastkit CLI.
3
- #
4
- # @author bnbong bbbong9@gmail.com
5
- # --------------------------------------------------------------------------
6
- import os
7
- import subprocess
8
- from typing import Dict, List
9
-
10
- from fastapi_fastkit import console
11
- from fastapi_fastkit.backend.transducer import copy_and_convert_template_file
12
- from fastapi_fastkit.core.exceptions import BackendExceptions, TemplateExceptions
13
- from fastapi_fastkit.core.settings import settings
14
- from fastapi_fastkit.utils.main import (
15
- handle_exception,
16
- print_error,
17
- print_info,
18
- print_success,
19
- print_warning,
20
- )
21
-
22
- # ------------------------------------------------------------
23
- # Template Discovery Functions
24
- # ------------------------------------------------------------
25
-
26
-
27
- def find_template_core_modules(project_dir: str) -> Dict[str, str]:
28
- """
29
- Find core module files in the template project structure.
30
- Returns a dictionary with paths to main.py, setup.py, and config files.
31
-
32
- :param project_dir: Path to the project directory
33
- :return: Dictionary with paths to core modules
34
- """
35
- core_modules = {"main": "", "setup": "", "config": ""}
36
- template_config = settings.TEMPLATE_PATHS["config"]
37
-
38
- # Find main.py
39
- for path in settings.TEMPLATE_PATHS["main"]:
40
- full_path = os.path.join(project_dir, path)
41
- if os.path.exists(full_path):
42
- core_modules["main"] = full_path
43
- break
44
-
45
- # Find setup.py
46
- for path in settings.TEMPLATE_PATHS["setup"]:
47
- full_path = os.path.join(project_dir, path)
48
- if os.path.exists(full_path):
49
- core_modules["setup"] = full_path
50
- break
51
-
52
- # Find config files
53
- if isinstance(template_config, dict):
54
- for config_file in template_config.get("files", []):
55
- for base_path in template_config.get("paths", []):
56
- full_path = os.path.join(project_dir, base_path, config_file)
57
- if os.path.exists(full_path):
58
- core_modules["config"] = full_path
59
- break
60
- if core_modules["config"]:
61
- break
62
-
63
- return core_modules
64
-
65
-
66
- def read_template_stack(template_path: str) -> List[str]:
67
- """
68
- Read the install_requires from setup.py-tpl in the template directory.
69
- Returns a list of required packages.
70
-
71
- :param template_path: Path to the template directory
72
- :return: List of required packages
73
- """
74
- setup_path = os.path.join(template_path, "setup.py-tpl")
75
- if not os.path.exists(setup_path):
76
- return []
77
-
78
- try:
79
- with open(setup_path, "r") as f:
80
- content = f.read()
81
- # Find the install_requires section using proper string matching
82
- if "install_requires: list[str] = [" in content:
83
- start_idx = content.find("install_requires: list[str] = [") + len(
84
- "install_requires: list[str] = ["
85
- )
86
- end_idx = content.find("]", start_idx)
87
- if start_idx != -1 and end_idx != -1:
88
- deps_str = content[start_idx:end_idx]
89
- # Clean and parse the dependencies
90
- deps = [
91
- dep.strip().strip("'").strip('"')
92
- for dep in deps_str.split(",")
93
- if dep.strip() and not dep.isspace()
94
- ]
95
- return [dep for dep in deps if dep] # Remove empty strings
96
- except Exception as e:
97
- handle_exception(e, "Error reading template dependencies")
98
- return []
99
-
100
- return []
101
-
102
-
103
- # ------------------------------------------------------------
104
- # Project Setup Functions
105
- # ------------------------------------------------------------
106
-
107
-
108
- def inject_project_metadata(
109
- target_dir: str,
110
- project_name: str,
111
- author: str,
112
- author_email: str,
113
- description: str,
114
- ) -> None:
115
- """
116
- Inject project metadata into template files after template conversion.
117
- """
118
- try:
119
- core_modules = find_template_core_modules(target_dir)
120
- setup_py = core_modules.get("setup", "")
121
- config_py = core_modules.get("config", "")
122
-
123
- if setup_py and os.path.exists(setup_py):
124
- with open(setup_py, "r") as f:
125
- content = f.read()
126
-
127
- # Replace placeholders
128
- content = content.replace("<project_name>", project_name)
129
- content = content.replace("<author>", author)
130
- content = content.replace("<author_email>", author_email)
131
- content = content.replace("<description>", description)
132
-
133
- with open(setup_py, "w") as f:
134
- f.write(content)
135
- print_info("Injected metadata into setup.py")
136
-
137
- if config_py and os.path.exists(config_py):
138
- with open(config_py, "r") as f:
139
- content = f.read()
140
-
141
- content = content.replace("<project_name>", project_name)
142
-
143
- with open(config_py, "w") as f:
144
- f.write(content)
145
- print_info("Injected metadata into config file")
146
-
147
- # Try to find the readme file
148
- readme_files = ["README.md", "Readme.md", "readme.md"]
149
- for readme in readme_files:
150
- readme_path = os.path.join(target_dir, readme)
151
- if os.path.exists(readme_path):
152
- with open(readme_path, "r") as f:
153
- content = f.read()
154
-
155
- content = content.replace(
156
- "{FASTAPI TEMPLATE PROJECT NAME}", project_name
157
- )
158
- content = content.replace("{fill here}", author)
159
-
160
- with open(readme_path, "w") as f:
161
- f.write(content)
162
- print_info("Injected metadata into README.md")
163
- break
164
-
165
- except Exception as e:
166
- handle_exception(e, "Failed to inject project metadata")
167
- raise BackendExceptions("Failed to inject project metadata")
168
-
169
-
170
- def create_venv(project_dir: str) -> str:
171
- """Create a virtual environment."""
172
- try:
173
- with console.status("[bold green]Setting up project environment..."):
174
- console.print("[yellow]Creating virtual environment...[/yellow]")
175
- venv_path = os.path.join(project_dir, ".venv")
176
- subprocess.run(["python", "-m", "venv", venv_path], check=True)
177
-
178
- if os.name == "nt":
179
- activate_venv = f" {os.path.join(venv_path, 'Scripts', 'activate.bat')}"
180
- else:
181
- activate_venv = f" source {os.path.join(venv_path, 'bin', 'activate')}"
182
-
183
- print_info(
184
- "venv created at "
185
- + venv_path
186
- + "\nTo activate the virtual environment, run:\n\n"
187
- + activate_venv,
188
- )
189
- return venv_path
190
-
191
- except Exception as e:
192
- print_error(f"Error during venv creation: {e}")
193
- raise BackendExceptions("Failed to create venv")
194
-
195
-
196
- def install_dependencies(project_dir: str, venv_path: str) -> None:
197
- """
198
- Install project dependencies into the virtual environment.
199
-
200
- :param project_dir: Path to the project directory
201
- :param venv_path: Path to the virtual environment
202
- :return: None
203
- """
204
- try:
205
- if not os.path.exists(venv_path):
206
- print_error("Virtual environment does not exist. Creating it first.")
207
- venv_path = create_venv(project_dir)
208
- if not venv_path:
209
- raise BackendExceptions("Failed to create virtual environment")
210
-
211
- requirements_path = os.path.join(project_dir, "requirements.txt")
212
- if not os.path.exists(requirements_path):
213
- print_error(f"Requirements file not found at {requirements_path}")
214
- raise BackendExceptions("Requirements file not found")
215
-
216
- if os.name == "nt": # Windows
217
- pip_path = os.path.join(venv_path, "Scripts", "pip")
218
- else: # Unix-based
219
- pip_path = os.path.join(venv_path, "bin", "pip")
220
-
221
- # Upgrade pip
222
- subprocess.run(
223
- [pip_path, "install", "--upgrade", "pip"],
224
- check=True,
225
- capture_output=True,
226
- text=True,
227
- )
228
-
229
- with console.status("[bold green]Installing dependencies..."):
230
- subprocess.run(
231
- [pip_path, "install", "-r", "requirements.txt"],
232
- cwd=project_dir,
233
- check=True,
234
- )
235
-
236
- print_success("Dependencies installed successfully")
237
-
238
- except subprocess.CalledProcessError as e:
239
- handle_exception(e, f"Error during dependency installation: {str(e)}")
240
- if hasattr(e, "stderr"):
241
- print_error(f"Error details: {e.stderr}")
242
- raise BackendExceptions("Failed to install dependencies")
243
- except Exception as e:
244
- handle_exception(e, f"Error during dependency installation: {str(e)}")
245
- raise BackendExceptions(f"Failed to install dependencies: {str(e)}")
246
-
247
-
248
- # ------------------------------------------------------------
249
- # Add Route Functions
250
- # ------------------------------------------------------------
251
-
252
-
253
- def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
254
- """
255
- Ensure the project structure exists for adding a new route.
256
- Creates necessary directories if they don't exist.
257
-
258
- :param src_dir: Source directory of the project
259
- :return: Dictionary with paths to target directories
260
- """
261
- if not os.path.exists(src_dir):
262
- raise BackendExceptions(f"Source directory not found at {src_dir}")
263
-
264
- # Define target directories
265
- target_dirs = {
266
- "api": os.path.join(src_dir, "api"),
267
- "api_routes": os.path.join(src_dir, "api", "routes"),
268
- "crud": os.path.join(src_dir, "crud"),
269
- "schemas": os.path.join(src_dir, "schemas"),
270
- }
271
-
272
- # Create directories and __init__.py files if needed
273
- for dir_name, dir_path in target_dirs.items():
274
- if not os.path.exists(dir_path):
275
- os.makedirs(dir_path)
276
-
277
- # Create __init__.py if it doesn't exist
278
- init_file = os.path.join(dir_path, "__init__.py")
279
- if not os.path.exists(init_file):
280
- with open(init_file, "w") as f:
281
- pass
282
-
283
- return target_dirs
284
-
285
-
286
- def _create_route_files(
287
- modules_dir: str, target_dirs: Dict[str, str], route_name: str
288
- ) -> None:
289
- """
290
- Create route files from templates.
291
-
292
- :param modules_dir: Path to the modules directory
293
- :param target_dirs: Dictionary with paths to target directories
294
- :param route_name: Name of the route to create
295
- """
296
- replacements = {"<new_route>": route_name}
297
- module_types = ["api/routes", "crud", "schemas"]
298
-
299
- for module_type in module_types:
300
- # Source template
301
- source = os.path.join(modules_dir, module_type, "new_route.py-tpl")
302
-
303
- # Target directory
304
- if module_type == "api/routes":
305
- target_dir = target_dirs["api_routes"]
306
- else:
307
- target_dir = target_dirs[module_type.split("/")[-1]]
308
-
309
- # Target file
310
- target = os.path.join(target_dir, f"{route_name}.py")
311
-
312
- if os.path.exists(target):
313
- print_warning(f"File {target} already exists, skipping...")
314
- continue
315
-
316
- if not copy_and_convert_template_file(source, target, replacements):
317
- print_warning(f"Failed to copy template file {source} to {target}")
318
-
319
-
320
- def _handle_api_router_file(
321
- target_dirs: Dict[str, str], modules_dir: str, route_name: str
322
- ) -> None:
323
- """
324
- Handle the API router file - create or update.
325
-
326
- :param target_dirs: Dictionary with paths to target directories
327
- :param modules_dir: Path to the modules directory
328
- :param route_name: Name of the route to add
329
- """
330
- api_py_target = os.path.join(target_dirs["api"], "api.py")
331
-
332
- if os.path.exists(api_py_target):
333
- # Update existing api.py
334
- with open(api_py_target, "r") as f:
335
- api_content = f.read()
336
-
337
- # Check if router is already included
338
- router_import = f"from src.api.routes import {route_name}"
339
- if router_import not in api_content:
340
- with open(api_py_target, "a") as f:
341
- f.write(f"\n{router_import}\n")
342
- f.write(f"api_router.include_router({route_name}.router)\n")
343
- print_info(f"Added route {route_name} to existing api.py")
344
- else:
345
- # Create new api.py
346
- router_code = f"""
347
- # --------------------------------------------------------------------------
348
- # API router connector module
349
- # --------------------------------------------------------------------------
350
- from fastapi import APIRouter
351
-
352
- from src.api.routes import {route_name}
353
-
354
- api_router = APIRouter()
355
- api_router.include_router({route_name}.router)
356
-
357
- """
358
- with open(api_py_target, "w") as tgt_file:
359
- tgt_file.write(router_code)
360
-
361
-
362
- def _process_init_files(
363
- modules_dir: str, target_dirs: Dict[str, str], module_types: List[str]
364
- ) -> None:
365
- """
366
- Process __init__.py files if they don't exist.
367
-
368
- :param modules_dir: Path to the modules directory
369
- :param target_dirs: Dictionary with paths to target directories
370
- :param module_types: List of module types to process
371
- """
372
- for module_type in module_types:
373
- module_base = module_type.split("/")[0]
374
- init_source = os.path.join(modules_dir, module_base, "__init__.py-tpl")
375
-
376
- if os.path.exists(init_source):
377
- init_target_dir = target_dirs.get(module_base, None) or target_dirs.get(
378
- f"{module_base}_routes"
379
- )
380
- if init_target_dir:
381
- init_target = os.path.join(init_target_dir, "__init__.py")
382
- if not os.path.exists(init_target):
383
- copy_and_convert_template_file(init_source, init_target)
384
-
385
-
386
- def _update_main_app(src_dir: str, route_name: str) -> None:
387
- """
388
- Update the main application file to include the API router.
389
- Adds the router import and include statement at the end of the file.
390
-
391
- :param src_dir: Source directory of the project
392
- :param route_name: Name of the route to add
393
- """
394
- main_py_path = os.path.join(src_dir, "main.py")
395
- if not os.path.exists(main_py_path):
396
- print_warning("main.py not found. Please manually add the API router.")
397
- return
398
-
399
- with open(main_py_path, "r") as f:
400
- main_content = f.read()
401
-
402
- # Check if router is already imported or included
403
- router_import = "from src.api.api import api_router"
404
- router_include = "app.include_router(api_router"
405
-
406
- if router_import in main_content and router_include in main_content:
407
- # Router already fully configured, nothing to do
408
- return
409
-
410
- # Check if FastAPI app is defined
411
- if "app = FastAPI" not in main_content:
412
- print_warning(
413
- "FastAPI app instance not found in main.py. Please manually add router."
414
- )
415
- return
416
-
417
- # Add the router import if needed
418
- if router_import not in main_content:
419
- # Add import at the end of imports section or beginning of file
420
- lines = main_content.split("\n")
421
-
422
- # Find last import line
423
- last_import_idx = -1
424
- for i, line in enumerate(lines):
425
- if line.startswith("import ") or line.startswith("from "):
426
- last_import_idx = i
427
-
428
- if last_import_idx >= 0:
429
- # Insert after the last import
430
- lines.insert(last_import_idx + 1, router_import)
431
- else:
432
- # Insert at the beginning
433
- lines.insert(0, router_import)
434
-
435
- main_content = "\n".join(lines)
436
-
437
- # Add the router include
438
- if router_include not in main_content:
439
- if not main_content.endswith("\n"):
440
- main_content += "\n"
441
-
442
- main_content += f"\n# Include API router\napp.include_router(api_router)\n"
443
-
444
- # Write the updated content back to the file
445
- with open(main_py_path, "w") as f:
446
- f.write(main_content)
447
-
448
- print_info("Updated main.py to include the API router")
449
-
450
-
451
- def add_new_route(project_dir: str, route_name: str) -> None:
452
- """
453
- Add a new API route to an existing FastAPI project.
454
-
455
- :param project_dir: Path to the project directory
456
- :param route_name: Name of the new route to add
457
- :return: None
458
- """
459
- try:
460
- # Setup paths
461
- modules_dir = os.path.join(settings.FASTKIT_TEMPLATE_ROOT, "modules")
462
- src_dir = os.path.join(project_dir, "src")
463
-
464
- # Ensure project structure exists
465
- target_dirs = _ensure_project_structure(src_dir)
466
-
467
- # Create route files
468
- _create_route_files(modules_dir, target_dirs, route_name)
469
-
470
- # Handle API router file
471
- _handle_api_router_file(target_dirs, modules_dir, route_name)
472
-
473
- # Process init files
474
- module_types = ["api/routes", "crud", "schemas"]
475
- _process_init_files(modules_dir, target_dirs, module_types)
476
-
477
- # Update main application
478
- _update_main_app(src_dir, route_name)
479
-
480
- except Exception as e:
481
- handle_exception(e, f"Error adding new route: {str(e)}")
482
- raise BackendExceptions(f"Failed to add new route: {str(e)}")
1
+ # --------------------------------------------------------------------------
2
+ # The Module defines backend operations for FastAPI-fastkit CLI.
3
+ #
4
+ # @author bnbong bbbong9@gmail.com
5
+ # --------------------------------------------------------------------------
6
+ import os
7
+ import re
8
+ from typing import Dict, List
9
+
10
+ from fastapi_fastkit import console
11
+ from fastapi_fastkit.backend.package_managers import PackageManagerFactory
12
+ from fastapi_fastkit.backend.transducer import copy_and_convert_template_file
13
+ from fastapi_fastkit.core.exceptions import BackendExceptions, TemplateExceptions
14
+ from fastapi_fastkit.core.settings import settings
15
+ from fastapi_fastkit.utils.logging import debug_log, get_logger
16
+ from fastapi_fastkit.utils.main import (
17
+ handle_exception,
18
+ print_error,
19
+ print_info,
20
+ print_success,
21
+ print_warning,
22
+ )
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ # ------------------------------------------------------------
28
+ # Template Discovery Functions
29
+ # ------------------------------------------------------------
30
+
31
+
32
+ def find_template_core_modules(project_dir: str) -> Dict[str, str]:
33
+ """
34
+ Find core module files in the template project structure.
35
+ Returns a dictionary with paths to main.py, setup.py, and config files.
36
+
37
+ :param project_dir: Path to the project directory
38
+ :return: Dictionary with paths to core modules
39
+ """
40
+ core_modules = {"main": "", "setup": "", "config": ""}
41
+ template_paths = settings.TEMPLATE_PATHS
42
+
43
+ # Find main.py
44
+ for main_path in template_paths["main"]:
45
+ full_path = os.path.join(project_dir, main_path)
46
+ if os.path.exists(full_path):
47
+ core_modules["main"] = full_path
48
+ break
49
+
50
+ # Find setup.py
51
+ for setup_path in template_paths["setup"]:
52
+ full_path = os.path.join(project_dir, setup_path)
53
+ if os.path.exists(full_path):
54
+ core_modules["setup"] = full_path
55
+ break
56
+
57
+ # Find config file
58
+ config_info = template_paths["config"]
59
+ if isinstance(config_info, dict):
60
+ for config_path in config_info["paths"]:
61
+ for config_file in config_info["files"]:
62
+ full_path = os.path.join(project_dir, config_path, config_file)
63
+ if os.path.exists(full_path):
64
+ core_modules["config"] = full_path
65
+ break
66
+ if core_modules["config"]:
67
+ break
68
+
69
+ return core_modules
70
+
71
+
72
+ def read_template_stack(template_path: str) -> List[str]:
73
+ """
74
+ Read dependencies from template requirements.txt-tpl file or setup.py-tpl file.
75
+
76
+ :param template_path: Path to the template directory
77
+ :return: List of dependencies
78
+ """
79
+ # First try requirements.txt-tpl
80
+ req_file = os.path.join(template_path, "requirements.txt-tpl")
81
+ if os.path.exists(req_file):
82
+ try:
83
+ with open(req_file, "r", encoding="utf-8") as f:
84
+ deps = [dep.strip() for dep in f.readlines() if dep.strip()]
85
+ return deps
86
+ except (OSError, UnicodeDecodeError) as e:
87
+ debug_log(
88
+ f"Error reading template dependencies from {req_file}: {e}", "error"
89
+ )
90
+ return []
91
+
92
+ # Fallback: try to read from setup.py-tpl for legacy templates
93
+ setup_file = os.path.join(template_path, "setup.py-tpl")
94
+ if os.path.exists(setup_file):
95
+ try:
96
+ with open(setup_file, "r", encoding="utf-8") as f:
97
+ content = f.read()
98
+ return _parse_setup_dependencies(content)
99
+ except (OSError, UnicodeDecodeError) as e:
100
+ debug_log(f"Error reading setup.py template: {e}", "error")
101
+
102
+ return []
103
+
104
+
105
+ def _parse_setup_dependencies(content: str) -> List[str]:
106
+ """
107
+ Parse dependencies from setup.py content.
108
+
109
+ :param content: setup.py file content
110
+ :return: List of dependencies
111
+ """
112
+ # First try to find install_requires with list[str] type annotation (new format)
113
+ list_match = re.search(
114
+ r"install_requires:\s*list\[str\]\s*=\s*\[(.*?)\]",
115
+ content,
116
+ re.DOTALL,
117
+ )
118
+ if list_match:
119
+ deps_str = list_match.group(1)
120
+ # Split by lines and commas, clean up quotes
121
+ deps = []
122
+ for line in deps_str.split("\n"):
123
+ line = line.strip()
124
+ if line and not line.startswith("#"):
125
+ # Remove quotes and trailing commas
126
+ line = line.strip(" \",'")
127
+ if line:
128
+ deps.append(line)
129
+ return [dep for dep in deps if dep] # Remove empty strings
130
+
131
+ # Fallback to original install_requires parsing
132
+ match = re.search(r"install_requires\s*=\s*\[(.*?)\]", content, re.DOTALL)
133
+ if match:
134
+ deps_str = match.group(1).replace("\n", "").replace(" ", "")
135
+ deps = [
136
+ dep.strip().strip("'").strip('"')
137
+ for dep in deps_str.split(",")
138
+ if dep.strip() and not dep.isspace()
139
+ ]
140
+ return [dep for dep in deps if dep] # Remove empty strings
141
+
142
+ return []
143
+
144
+
145
+ # ------------------------------------------------------------
146
+ # Project Setup Functions
147
+ # ------------------------------------------------------------
148
+
149
+
150
+ def inject_project_metadata(
151
+ target_dir: str,
152
+ project_name: str,
153
+ author: str,
154
+ author_email: str,
155
+ description: str,
156
+ ) -> None:
157
+ """
158
+ Inject project metadata into template files after template conversion.
159
+ """
160
+ try:
161
+ core_modules = find_template_core_modules(target_dir)
162
+ _process_setup_file(
163
+ core_modules.get("setup", ""),
164
+ project_name,
165
+ author,
166
+ author_email,
167
+ description,
168
+ )
169
+ _process_config_file(core_modules.get("config", ""), project_name)
170
+
171
+ print_success("Project metadata injected successfully")
172
+
173
+ except Exception as e:
174
+ debug_log(f"Failed to inject project metadata: {e}", "error")
175
+ handle_exception(e, "Failed to inject project metadata")
176
+ raise BackendExceptions("Failed to inject project metadata")
177
+
178
+
179
+ def _process_setup_file(
180
+ setup_py: str, project_name: str, author: str, author_email: str, description: str
181
+ ) -> None:
182
+ """
183
+ Process setup.py file and inject metadata.
184
+
185
+ :param setup_py: Path to setup.py file
186
+ :param project_name: Project name
187
+ :param author: Author name
188
+ :param author_email: Author email
189
+ :param description: Project description
190
+ """
191
+ if not setup_py or not os.path.exists(setup_py):
192
+ return
193
+
194
+ try:
195
+ with open(setup_py, "r", encoding="utf-8") as f:
196
+ content = f.read()
197
+
198
+ # Replace placeholders
199
+ replacements = {
200
+ "<project_name>": project_name,
201
+ "<author>": author,
202
+ "<author_email>": author_email,
203
+ "<description>": description,
204
+ }
205
+
206
+ for placeholder, value in replacements.items():
207
+ content = content.replace(placeholder, value)
208
+
209
+ with open(setup_py, "w", encoding="utf-8") as f:
210
+ f.write(content)
211
+
212
+ debug_log("Injected metadata into setup.py", "info")
213
+ print_info("Injected metadata into setup.py")
214
+
215
+ except (OSError, UnicodeDecodeError) as e:
216
+ debug_log(f"Error processing setup.py: {e}", "error")
217
+ raise BackendExceptions(f"Failed to process setup.py: {e}")
218
+
219
+
220
+ def _process_config_file(config_py: str, project_name: str) -> None:
221
+ """
222
+ Process config file and inject project name.
223
+
224
+ :param config_py: Path to config file
225
+ :param project_name: Project name
226
+ """
227
+ if not config_py or not os.path.exists(config_py):
228
+ return
229
+
230
+ try:
231
+ with open(config_py, "r", encoding="utf-8") as f:
232
+ content = f.read()
233
+
234
+ # Replace project name placeholder
235
+ content = content.replace("<project_name>", project_name)
236
+
237
+ with open(config_py, "w", encoding="utf-8") as f:
238
+ f.write(content)
239
+
240
+ debug_log("Injected project name into config file", "info")
241
+ print_info("Injected project name into config file")
242
+
243
+ except (OSError, UnicodeDecodeError) as e:
244
+ debug_log(f"Error processing config file: {e}", "error")
245
+ raise BackendExceptions(f"Failed to process config file: {e}")
246
+
247
+
248
+ def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str:
249
+ """
250
+ Create a virtual environment using the specified package manager.
251
+
252
+ :param project_dir: Path to the project directory
253
+ :param manager_type: Type of package manager to use
254
+ :return: Path to the virtual environment
255
+ :raises: BackendExceptions if virtual environment creation fails
256
+ """
257
+ try:
258
+ package_manager = PackageManagerFactory.create_manager(
259
+ manager_type, project_dir, auto_detect=True
260
+ )
261
+ return package_manager.create_virtual_environment()
262
+ except Exception as e:
263
+ debug_log(
264
+ f"Error creating virtual environment with {manager_type}: {e}", "error"
265
+ )
266
+ raise BackendExceptions(f"Failed to create virtual environment: {str(e)}")
267
+
268
+
269
+ def create_venv(project_dir: str) -> str:
270
+ """
271
+ Create a Python virtual environment in the project directory.
272
+
273
+ This is a backward compatibility wrapper that uses pip by default.
274
+
275
+ :param project_dir: Path to the project directory
276
+ :return: Path to the virtual environment
277
+ """
278
+ return create_venv_with_manager(project_dir, "pip")
279
+
280
+
281
+ def install_dependencies_with_manager(
282
+ project_dir: str, venv_path: str, manager_type: str = "pip"
283
+ ) -> None:
284
+ """
285
+ Install dependencies using the specified package manager.
286
+
287
+ :param project_dir: Path to the project directory
288
+ :param venv_path: Path to the virtual environment
289
+ :param manager_type: Type of package manager to use
290
+ :return: None
291
+ :raises: BackendExceptions if dependency installation fails
292
+ """
293
+ try:
294
+ package_manager = PackageManagerFactory.create_manager(
295
+ manager_type, project_dir, auto_detect=True
296
+ )
297
+ package_manager.install_dependencies(venv_path)
298
+ except Exception as e:
299
+ debug_log(f"Error installing dependencies with {manager_type}: {e}", "error")
300
+ raise BackendExceptions(f"Failed to install dependencies: {str(e)}")
301
+
302
+
303
+ def install_dependencies(project_dir: str, venv_path: str) -> None:
304
+ """
305
+ Install dependencies in the virtual environment.
306
+
307
+ This is a backward compatibility wrapper that uses pip by default.
308
+
309
+ :param project_dir: Path to the project directory
310
+ :param venv_path: Path to the virtual environment
311
+ :return: None
312
+ """
313
+ install_dependencies_with_manager(project_dir, venv_path, "pip")
314
+
315
+
316
+ def generate_dependency_file_with_manager(
317
+ project_dir: str,
318
+ dependencies: List[str],
319
+ manager_type: str = "pip",
320
+ project_name: str = "",
321
+ author: str = "",
322
+ author_email: str = "",
323
+ description: str = "",
324
+ ) -> None:
325
+ """
326
+ Generate a dependency file using the specified package manager.
327
+
328
+ :param project_dir: Path to the project directory
329
+ :param dependencies: List of dependency specifications
330
+ :param manager_type: Type of package manager to use
331
+ :param project_name: Name of the project
332
+ :param author: Author name
333
+ :param author_email: Author email
334
+ :param description: Project description
335
+ :return: None
336
+ :raises: BackendExceptions if dependency file generation fails
337
+ """
338
+ try:
339
+ package_manager = PackageManagerFactory.create_manager(
340
+ manager_type, project_dir, auto_detect=True
341
+ )
342
+ package_manager.generate_dependency_file(
343
+ dependencies, project_name, author, author_email, description
344
+ )
345
+ except Exception as e:
346
+ debug_log(f"Error generating dependency file with {manager_type}: {e}", "error")
347
+ raise BackendExceptions(f"Failed to generate dependency file: {str(e)}")
348
+
349
+
350
+ # ------------------------------------------------------------
351
+ # Add Route Functions
352
+ # ------------------------------------------------------------
353
+
354
+
355
+ def _ensure_project_structure(src_dir: str) -> Dict[str, str]:
356
+ """
357
+ Ensure the project structure exists for adding a new route.
358
+ Creates necessary directories if they don't exist.
359
+
360
+ :param src_dir: Source directory of the project
361
+ :return: Dictionary with paths to target directories
362
+ """
363
+ if not os.path.exists(src_dir):
364
+ raise BackendExceptions(f"Source directory not found at {src_dir}")
365
+
366
+ # Define target directories
367
+ target_dirs = {
368
+ "api": os.path.join(src_dir, "api"),
369
+ "api_routes": os.path.join(src_dir, "api", "routes"),
370
+ "crud": os.path.join(src_dir, "crud"),
371
+ "schemas": os.path.join(src_dir, "schemas"),
372
+ }
373
+
374
+ # Create directories and __init__.py files if needed
375
+ for dir_name, dir_path in target_dirs.items():
376
+ if not os.path.exists(dir_path):
377
+ os.makedirs(dir_path)
378
+
379
+ # Create __init__.py if it doesn't exist
380
+ init_file = os.path.join(dir_path, "__init__.py")
381
+ if not os.path.exists(init_file):
382
+ with open(init_file, "w") as f:
383
+ pass
384
+
385
+ return target_dirs
386
+
387
+
388
+ def _create_route_files(
389
+ modules_dir: str, target_dirs: Dict[str, str], route_name: str
390
+ ) -> None:
391
+ """
392
+ Create route files from templates.
393
+
394
+ :param modules_dir: Path to the modules directory
395
+ :param target_dirs: Dictionary with paths to target directories
396
+ :param route_name: Name of the route to create
397
+ """
398
+ replacements = {"<new_route>": route_name}
399
+ module_types = ["api/routes", "crud", "schemas"]
400
+
401
+ for module_type in module_types:
402
+ # Source template
403
+ source = os.path.join(modules_dir, module_type, "new_route.py-tpl")
404
+
405
+ # Target directory
406
+ if module_type == "api/routes":
407
+ target_dir = target_dirs["api_routes"]
408
+ else:
409
+ target_dir = target_dirs[module_type.split("/")[-1]]
410
+
411
+ # Target file
412
+ target = os.path.join(target_dir, f"{route_name}.py")
413
+
414
+ if os.path.exists(target):
415
+ print_warning(f"File {target} already exists, skipping...")
416
+ continue
417
+
418
+ if not copy_and_convert_template_file(source, target, replacements):
419
+ print_warning(f"Failed to copy template file {source} to {target}")
420
+
421
+
422
+ def _handle_api_router_file(
423
+ target_dirs: Dict[str, str], modules_dir: str, route_name: str
424
+ ) -> None:
425
+ """
426
+ Handle API router file creation or update.
427
+
428
+ :param target_dirs: Dictionary with paths to target directories
429
+ :param modules_dir: Path to the modules directory
430
+ :param route_name: Name of the route
431
+ """
432
+ api_dir = target_dirs["api"]
433
+ api_router_file = os.path.join(api_dir, "api.py")
434
+
435
+ if not os.path.exists(api_router_file):
436
+ # Create api.py if it doesn't exist
437
+ api_source = os.path.join(modules_dir, "api", "__init__.py-tpl")
438
+ if os.path.exists(api_source):
439
+ copy_and_convert_template_file(api_source, api_router_file)
440
+
441
+ # Update API router to include new route
442
+ if os.path.exists(api_router_file):
443
+ _update_api_router(api_router_file, route_name)
444
+
445
+
446
+ def _update_api_router(api_router_file: str, route_name: str) -> None:
447
+ """
448
+ Update API router file to include new route.
449
+
450
+ :param api_router_file: Path to API router file
451
+ :param route_name: Name of the route
452
+ """
453
+ try:
454
+ with open(api_router_file, "r", encoding="utf-8") as f:
455
+ content = f.read()
456
+
457
+ # Check if route is already included
458
+ route_import = f"from .routes import {route_name}"
459
+ route_include = f"api_router.include_router({route_name}.router"
460
+
461
+ if route_import in content and route_include in content:
462
+ return # Already included
463
+
464
+ # Add import if not present
465
+ if route_import not in content:
466
+ # Find where to add the import
467
+ lines = content.split("\n")
468
+ last_import_idx = -1
469
+ for i, line in enumerate(lines):
470
+ if line.startswith("from .routes import") or line.startswith(
471
+ "from routes import"
472
+ ):
473
+ last_import_idx = i
474
+
475
+ if last_import_idx >= 0:
476
+ lines.insert(last_import_idx + 1, route_import)
477
+ else:
478
+ # Add at the end of imports section
479
+ for i, line in enumerate(lines):
480
+ if (
481
+ not line.startswith("from")
482
+ and not line.startswith("import")
483
+ and line.strip()
484
+ ):
485
+ lines.insert(i, route_import)
486
+ break
487
+
488
+ content = "\n".join(lines)
489
+
490
+ # Add router inclusion if not present
491
+ if route_include not in content:
492
+ # Add before the last line or at the end
493
+ if content.strip():
494
+ content += f'\napi_router.include_router({route_name}.router, prefix="/{route_name}", tags=["{route_name}"])\n'
495
+ else:
496
+ content = f'api_router.include_router({route_name}.router, prefix="/{route_name}", tags=["{route_name}"])\n'
497
+
498
+ with open(api_router_file, "w", encoding="utf-8") as f:
499
+ f.write(content)
500
+
501
+ debug_log(f"Updated API router to include {route_name}", "info")
502
+
503
+ except (OSError, UnicodeDecodeError) as e:
504
+ debug_log(f"Error updating API router: {e}", "error")
505
+
506
+
507
+ def _process_init_files(
508
+ modules_dir: str, target_dirs: Dict[str, str], module_types: List[str]
509
+ ) -> None:
510
+ """
511
+ Process __init__.py files if they don't exist.
512
+
513
+ :param modules_dir: Path to the modules directory
514
+ :param target_dirs: Dictionary with paths to target directories
515
+ :param module_types: List of module types to process
516
+ """
517
+ for module_type in module_types:
518
+ module_base = module_type.split("/")[0]
519
+ init_source = os.path.join(modules_dir, module_base, "__init__.py-tpl")
520
+
521
+ if os.path.exists(init_source):
522
+ init_target_dir = target_dirs.get(module_base, None) or target_dirs.get(
523
+ f"{module_base}_routes"
524
+ )
525
+ if init_target_dir:
526
+ init_target = os.path.join(init_target_dir, "__init__.py")
527
+ if not os.path.exists(init_target):
528
+ copy_and_convert_template_file(init_source, init_target)
529
+
530
+
531
+ def _update_main_app(src_dir: str, route_name: str) -> None:
532
+ """
533
+ Update the main application file to include the API router.
534
+ Adds the router import and include statement at the end of the file.
535
+
536
+ :param src_dir: Source directory of the project
537
+ :param route_name: Name of the route to add
538
+ """
539
+ main_py_path = os.path.join(src_dir, "main.py")
540
+ if not os.path.exists(main_py_path):
541
+ print_warning("main.py not found. Please manually add the API router.")
542
+ return
543
+
544
+ try:
545
+ with open(main_py_path, "r", encoding="utf-8") as f:
546
+ main_content = f.read()
547
+
548
+ # Check if router is already imported or included
549
+ router_import = "from src.api.api import api_router"
550
+ router_include = "app.include_router(api_router"
551
+
552
+ if router_import in main_content and router_include in main_content:
553
+ # Router already fully configured, nothing to do
554
+ return
555
+
556
+ # Check if FastAPI app is defined
557
+ if "app = FastAPI" not in main_content:
558
+ print_warning(
559
+ "FastAPI app instance not found in main.py. Please manually add router."
560
+ )
561
+ return
562
+
563
+ # Add the router import if needed
564
+ if router_import not in main_content:
565
+ # Add import at the end of imports section or beginning of file
566
+ lines = main_content.split("\n")
567
+
568
+ # Find last import line
569
+ last_import_idx = -1
570
+ for i, line in enumerate(lines):
571
+ if line.startswith("import ") or line.startswith("from "):
572
+ last_import_idx = i
573
+
574
+ if last_import_idx >= 0:
575
+ # Insert after the last import
576
+ lines.insert(last_import_idx + 1, router_import)
577
+ else:
578
+ # Insert at the beginning
579
+ lines.insert(0, router_import)
580
+
581
+ main_content = "\n".join(lines)
582
+
583
+ # Add router inclusion if needed
584
+ if router_include not in main_content:
585
+ # Add at the end of the file
586
+ main_content += "\napp.include_router(api_router)\n"
587
+
588
+ with open(main_py_path, "w", encoding="utf-8") as f:
589
+ f.write(main_content)
590
+
591
+ debug_log(f"Updated main.py to include router for {route_name}", "info")
592
+
593
+ except (OSError, UnicodeDecodeError) as e:
594
+ debug_log(f"Error updating main.py: {e}", "error")
595
+ print_warning(f"Failed to update main.py: {e}")
596
+
597
+
598
+ def add_new_route(project_dir: str, route_name: str) -> None:
599
+ """
600
+ Add a new API route to an existing FastAPI project.
601
+
602
+ :param project_dir: Path to the project directory
603
+ :param route_name: Name of the new route to add
604
+ :raises BackendExceptions: If route addition fails
605
+ """
606
+ try:
607
+ # Setup paths
608
+ modules_dir = os.path.join(settings.FASTKIT_TEMPLATE_ROOT, "modules")
609
+ src_dir = os.path.join(project_dir, "src")
610
+
611
+ # Ensure project structure exists
612
+ target_dirs = _ensure_project_structure(src_dir)
613
+
614
+ # Create route files
615
+ _create_route_files(modules_dir, target_dirs, route_name)
616
+
617
+ # Handle API router file
618
+ _handle_api_router_file(target_dirs, modules_dir, route_name)
619
+
620
+ # Process init files
621
+ module_types = ["api/routes", "crud", "schemas"]
622
+ _process_init_files(modules_dir, target_dirs, module_types)
623
+
624
+ # Update main application
625
+ _update_main_app(src_dir, route_name)
626
+
627
+ debug_log(f"Successfully added new route: {route_name}", "info")
628
+ print_success(f"Route '{route_name}' has been added successfully!")
629
+
630
+ except (OSError, PermissionError) as e:
631
+ debug_log(f"File system error while adding route {route_name}: {e}", "error")
632
+ handle_exception(e, f"Error adding new route: {str(e)}")
633
+ raise BackendExceptions(f"Failed to add new route: {str(e)}")
634
+ except BackendExceptions:
635
+ # Re-raise our own exceptions
636
+ raise
637
+ except Exception as e:
638
+ debug_log(f"Unexpected error while adding route {route_name}: {e}", "error")
639
+ handle_exception(e, f"Error adding new route: {str(e)}")
640
+ raise BackendExceptions(f"Failed to add new route: {str(e)}")