FastAPI-fastkit 1.1.0__py3-none-any.whl → 1.1.1__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.
- fastapi_fastkit/__init__.py +1 -1
- fastapi_fastkit/backend/inspector.py +86 -2
- fastapi_fastkit/backend/main.py +61 -2
- fastapi_fastkit/backend/package_managers/uv_manager.py +14 -3
- fastapi_fastkit/core/settings.py +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/pyproject.toml-tpl +85 -0
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/requirements.txt-tpl +1 -1
- fastapi_fastkit/fastapi_project_template/fastapi-async-crud/src/core/config.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/pyproject.toml-tpl +85 -0
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/core/config.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl +2 -2
- fastapi_fastkit/fastapi_project_template/fastapi-default/pyproject.toml-tpl +81 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/config.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/pyproject.toml-tpl +84 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/src/core/config.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-empty/pyproject.toml-tpl +77 -0
- fastapi_fastkit/fastapi_project_template/fastapi-empty/src/core/config.py-tpl +6 -6
- fastapi_fastkit/fastapi_project_template/fastapi-mcp/pyproject.toml-tpl +88 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/pyproject.toml-tpl +90 -0
- {fastapi_fastkit-1.1.0.dist-info → fastapi_fastkit-1.1.1.dist-info}/METADATA +1 -1
- {fastapi_fastkit-1.1.0.dist-info → fastapi_fastkit-1.1.1.dist-info}/RECORD +25 -18
- {fastapi_fastkit-1.1.0.dist-info → fastapi_fastkit-1.1.1.dist-info}/WHEEL +0 -0
- {fastapi_fastkit-1.1.0.dist-info → fastapi_fastkit-1.1.1.dist-info}/entry_points.txt +0 -0
- {fastapi_fastkit-1.1.0.dist-info → fastapi_fastkit-1.1.1.dist-info}/licenses/LICENSE +0 -0
fastapi_fastkit/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = 'v1.1.
|
|
1
|
+
__version__ = 'v1.1.1'
|
|
@@ -31,7 +31,9 @@ import yaml # type: ignore
|
|
|
31
31
|
from fastapi_fastkit.backend.main import (
|
|
32
32
|
create_venv,
|
|
33
33
|
find_template_core_modules,
|
|
34
|
+
inject_project_metadata,
|
|
34
35
|
install_dependencies,
|
|
36
|
+
install_dependencies_with_manager,
|
|
35
37
|
)
|
|
36
38
|
from fastapi_fastkit.backend.transducer import copy_and_convert_template
|
|
37
39
|
from fastapi_fastkit.core.settings import settings
|
|
@@ -61,6 +63,10 @@ class TemplateInspector:
|
|
|
61
63
|
try:
|
|
62
64
|
os.makedirs(self.temp_dir, exist_ok=True)
|
|
63
65
|
copy_and_convert_template(str(self.template_path), self.temp_dir)
|
|
66
|
+
|
|
67
|
+
# Inject dummy metadata for inspection
|
|
68
|
+
self._inject_dummy_metadata()
|
|
69
|
+
|
|
64
70
|
self._cleanup_needed = True
|
|
65
71
|
self.template_config = self._load_template_config()
|
|
66
72
|
debug_log(f"Created temporary directory at {self.temp_dir}", "debug")
|
|
@@ -112,6 +118,43 @@ class TemplateInspector:
|
|
|
112
118
|
debug_log(f"Failed to load template configuration: {e}", "warning")
|
|
113
119
|
return None
|
|
114
120
|
|
|
121
|
+
def _inject_dummy_metadata(self) -> None:
|
|
122
|
+
"""Inject dummy metadata for template inspection."""
|
|
123
|
+
try:
|
|
124
|
+
# Use dummy metadata for inspection
|
|
125
|
+
dummy_metadata = {
|
|
126
|
+
"project_name": "test-template",
|
|
127
|
+
"author": "Template Inspector",
|
|
128
|
+
"author_email": "inspector@fastapi-fastkit.dev",
|
|
129
|
+
"description": "Test project for template inspection",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
inject_project_metadata(
|
|
133
|
+
self.temp_dir,
|
|
134
|
+
dummy_metadata["project_name"],
|
|
135
|
+
dummy_metadata["author"],
|
|
136
|
+
dummy_metadata["author_email"],
|
|
137
|
+
dummy_metadata["description"],
|
|
138
|
+
)
|
|
139
|
+
debug_log("Injected dummy metadata for template inspection", "info")
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
debug_log(f"Failed to inject dummy metadata: {e}", "warning")
|
|
143
|
+
self.warnings.append(f"Failed to inject metadata: {str(e)}")
|
|
144
|
+
|
|
145
|
+
def _detect_package_manager(self) -> str:
|
|
146
|
+
"""Detect the appropriate package manager for the template."""
|
|
147
|
+
# Check for pyproject.toml (modern Python packaging)
|
|
148
|
+
if os.path.exists(os.path.join(self.temp_dir, "pyproject.toml")):
|
|
149
|
+
return "uv" # Use UV for pyproject.toml based projects
|
|
150
|
+
|
|
151
|
+
# Check for requirements.txt (traditional pip)
|
|
152
|
+
if os.path.exists(os.path.join(self.temp_dir, "requirements.txt")):
|
|
153
|
+
return "pip"
|
|
154
|
+
|
|
155
|
+
# Default to pip if no dependency file found
|
|
156
|
+
return "pip"
|
|
157
|
+
|
|
115
158
|
def _check_docker_available(self) -> bool:
|
|
116
159
|
"""Check if Docker and Docker Compose are available."""
|
|
117
160
|
try:
|
|
@@ -393,7 +436,21 @@ class TemplateInspector:
|
|
|
393
436
|
try:
|
|
394
437
|
# Create virtual environment for testing
|
|
395
438
|
venv_path = create_venv(self.temp_dir)
|
|
396
|
-
|
|
439
|
+
|
|
440
|
+
# Detect and use appropriate package manager
|
|
441
|
+
package_manager = self._detect_package_manager()
|
|
442
|
+
debug_log(f"Using package manager: {package_manager}", "info")
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
install_dependencies_with_manager(
|
|
446
|
+
self.temp_dir, venv_path, package_manager
|
|
447
|
+
)
|
|
448
|
+
except Exception as dep_error:
|
|
449
|
+
# Capture detailed dependency installation error
|
|
450
|
+
error_msg = f"Failed to install dependencies: {str(dep_error)}"
|
|
451
|
+
debug_log(f"Dependency installation failed: {dep_error}", "error")
|
|
452
|
+
self.errors.append(error_msg)
|
|
453
|
+
return False
|
|
397
454
|
|
|
398
455
|
# Check if scripts/test.sh exists
|
|
399
456
|
test_script_path = os.path.join(self.temp_dir, "scripts", "test.sh")
|
|
@@ -557,7 +614,21 @@ class TemplateInspector:
|
|
|
557
614
|
try:
|
|
558
615
|
# Create virtual environment for testing
|
|
559
616
|
venv_path = create_venv(self.temp_dir)
|
|
560
|
-
|
|
617
|
+
|
|
618
|
+
# Detect and use appropriate package manager
|
|
619
|
+
package_manager = self._detect_package_manager()
|
|
620
|
+
debug_log(f"Using package manager: {package_manager}", "info")
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
install_dependencies_with_manager(
|
|
624
|
+
self.temp_dir, venv_path, package_manager
|
|
625
|
+
)
|
|
626
|
+
except Exception as dep_error:
|
|
627
|
+
# Capture detailed dependency installation error
|
|
628
|
+
error_msg = f"Failed to install dependencies: {str(dep_error)}"
|
|
629
|
+
debug_log(f"Dependency installation failed: {dep_error}", "error")
|
|
630
|
+
self.errors.append(error_msg)
|
|
631
|
+
return False
|
|
561
632
|
|
|
562
633
|
# Set up fallback environment (e.g., SQLite database)
|
|
563
634
|
fallback_config = self.template_config["fallback_testing"]
|
|
@@ -683,6 +754,14 @@ class TemplateInspector:
|
|
|
683
754
|
# Check if all services are running
|
|
684
755
|
all_running = True
|
|
685
756
|
for service in services:
|
|
757
|
+
# Ensure service is a dictionary before calling .get()
|
|
758
|
+
if not isinstance(service, dict):
|
|
759
|
+
debug_log(
|
|
760
|
+
f"Service info is not a dictionary: {service}",
|
|
761
|
+
"warning",
|
|
762
|
+
)
|
|
763
|
+
continue
|
|
764
|
+
|
|
686
765
|
if service.get("State") != "running":
|
|
687
766
|
all_running = False
|
|
688
767
|
debug_log(
|
|
@@ -738,6 +817,11 @@ class TemplateInspector:
|
|
|
738
817
|
app_running = False
|
|
739
818
|
|
|
740
819
|
for service in services:
|
|
820
|
+
# Ensure service is a dictionary before calling .get()
|
|
821
|
+
if not isinstance(service, dict):
|
|
822
|
+
debug_log(f"Service info is not a dictionary: {service}", "warning")
|
|
823
|
+
continue
|
|
824
|
+
|
|
741
825
|
service_name = service.get("Name", "")
|
|
742
826
|
service_state = service.get("State", "")
|
|
743
827
|
|
fastapi_fastkit/backend/main.py
CHANGED
|
@@ -32,12 +32,12 @@ logger = get_logger(__name__)
|
|
|
32
32
|
def find_template_core_modules(project_dir: str) -> Dict[str, str]:
|
|
33
33
|
"""
|
|
34
34
|
Find core module files in the template project structure.
|
|
35
|
-
Returns a dictionary with paths to main.py, setup.py, and config files.
|
|
35
|
+
Returns a dictionary with paths to main.py, setup.py, pyproject.toml and config files.
|
|
36
36
|
|
|
37
37
|
:param project_dir: Path to the project directory
|
|
38
38
|
:return: Dictionary with paths to core modules
|
|
39
39
|
"""
|
|
40
|
-
core_modules = {"main": "", "setup": "", "config": ""}
|
|
40
|
+
core_modules = {"main": "", "setup": "", "pyproject": "", "config": ""}
|
|
41
41
|
template_paths = settings.TEMPLATE_PATHS
|
|
42
42
|
|
|
43
43
|
# Find main.py
|
|
@@ -54,6 +54,13 @@ def find_template_core_modules(project_dir: str) -> Dict[str, str]:
|
|
|
54
54
|
core_modules["setup"] = full_path
|
|
55
55
|
break
|
|
56
56
|
|
|
57
|
+
# Find pyproject.toml
|
|
58
|
+
for pyproject_path in template_paths["pyproject"]:
|
|
59
|
+
full_path = os.path.join(project_dir, pyproject_path)
|
|
60
|
+
if os.path.exists(full_path):
|
|
61
|
+
core_modules["pyproject"] = full_path
|
|
62
|
+
break
|
|
63
|
+
|
|
57
64
|
# Find config file
|
|
58
65
|
config_info = template_paths["config"]
|
|
59
66
|
if isinstance(config_info, dict):
|
|
@@ -166,6 +173,13 @@ def inject_project_metadata(
|
|
|
166
173
|
author_email,
|
|
167
174
|
description,
|
|
168
175
|
)
|
|
176
|
+
_process_pyproject_file(
|
|
177
|
+
core_modules.get("pyproject", ""),
|
|
178
|
+
project_name,
|
|
179
|
+
author,
|
|
180
|
+
author_email,
|
|
181
|
+
description,
|
|
182
|
+
)
|
|
169
183
|
_process_config_file(core_modules.get("config", ""), project_name)
|
|
170
184
|
|
|
171
185
|
print_success("Project metadata injected successfully")
|
|
@@ -245,6 +259,51 @@ def _process_config_file(config_py: str, project_name: str) -> None:
|
|
|
245
259
|
raise BackendExceptions(f"Failed to process config file: {e}")
|
|
246
260
|
|
|
247
261
|
|
|
262
|
+
def _process_pyproject_file(
|
|
263
|
+
pyproject_toml: str,
|
|
264
|
+
project_name: str,
|
|
265
|
+
author: str,
|
|
266
|
+
author_email: str,
|
|
267
|
+
description: str,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Process pyproject.toml file and inject metadata.
|
|
271
|
+
|
|
272
|
+
:param pyproject_toml: Path to pyproject.toml file
|
|
273
|
+
:param project_name: Project name
|
|
274
|
+
:param author: Author name
|
|
275
|
+
:param author_email: Author email
|
|
276
|
+
:param description: Project description
|
|
277
|
+
"""
|
|
278
|
+
if not pyproject_toml or not os.path.exists(pyproject_toml):
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
with open(pyproject_toml, "r", encoding="utf-8") as f:
|
|
283
|
+
content = f.read()
|
|
284
|
+
|
|
285
|
+
# Replace placeholders
|
|
286
|
+
replacements = {
|
|
287
|
+
"<project_name>": project_name,
|
|
288
|
+
"<author>": author,
|
|
289
|
+
"<author_email>": author_email,
|
|
290
|
+
"<description>": description,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for placeholder, value in replacements.items():
|
|
294
|
+
content = content.replace(placeholder, value)
|
|
295
|
+
|
|
296
|
+
with open(pyproject_toml, "w", encoding="utf-8") as f:
|
|
297
|
+
f.write(content)
|
|
298
|
+
|
|
299
|
+
debug_log("Injected metadata into pyproject.toml", "info")
|
|
300
|
+
print_info("Injected metadata into pyproject.toml")
|
|
301
|
+
|
|
302
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
303
|
+
debug_log(f"Error processing pyproject.toml: {e}", "error")
|
|
304
|
+
raise BackendExceptions(f"Failed to process pyproject.toml: {e}")
|
|
305
|
+
|
|
306
|
+
|
|
248
307
|
def create_venv_with_manager(project_dir: str, manager_type: str = "pip") -> str:
|
|
249
308
|
"""
|
|
250
309
|
Create a virtual environment using the specified package manager.
|
|
@@ -93,16 +93,27 @@ class UvManager(BasePackageManager):
|
|
|
93
93
|
print_error(f"pyproject.toml file not found at {pyproject_path}")
|
|
94
94
|
raise BackendExceptions("pyproject.toml file not found")
|
|
95
95
|
|
|
96
|
-
# Install dependencies using UV sync
|
|
96
|
+
# Install dependencies using UV sync (including dev dependencies)
|
|
97
|
+
cmd = ["uv", "sync", "--group", "dev"]
|
|
98
|
+
debug_log(
|
|
99
|
+
f"Running UV command: {' '.join(cmd)} in {self.project_dir}", "info"
|
|
100
|
+
)
|
|
101
|
+
|
|
97
102
|
with console.status("[bold green]Installing dependencies with UV..."):
|
|
98
|
-
subprocess.run(
|
|
99
|
-
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
cmd,
|
|
100
105
|
cwd=str(self.project_dir),
|
|
101
106
|
check=True,
|
|
102
107
|
capture_output=True,
|
|
103
108
|
text=True,
|
|
104
109
|
)
|
|
105
110
|
|
|
111
|
+
# Log UV output for debugging
|
|
112
|
+
if result.stdout:
|
|
113
|
+
debug_log(f"UV stdout: {result.stdout}", "debug")
|
|
114
|
+
if result.stderr:
|
|
115
|
+
debug_log(f"UV stderr: {result.stderr}", "debug")
|
|
116
|
+
|
|
106
117
|
debug_log("Dependencies installed successfully with UV", "info")
|
|
107
118
|
print_success("Dependencies installed successfully with UV")
|
|
108
119
|
|
fastapi_fastkit/core/settings.py
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "<project_name>"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "<description>"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "<author>", email = "<author_email>"},
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.115.8",
|
|
13
|
+
"uvicorn[standard]>=0.34.0",
|
|
14
|
+
"pydantic>=2.10.6",
|
|
15
|
+
"pydantic-settings>=2.7.1",
|
|
16
|
+
"SQLAlchemy>=2.0.38",
|
|
17
|
+
"python-dotenv>=1.0.1",
|
|
18
|
+
"aiofiles>=24.1.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.3.4",
|
|
24
|
+
"pytest-asyncio>=0.25.3",
|
|
25
|
+
"httpx>=0.28.1",
|
|
26
|
+
"black>=25.1.0",
|
|
27
|
+
"isort>=6.0.0",
|
|
28
|
+
"mypy>=1.15.0",
|
|
29
|
+
"PyYAML>=6.0.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3.4",
|
|
35
|
+
"pytest-asyncio>=0.25.3",
|
|
36
|
+
"httpx>=0.28.1",
|
|
37
|
+
"black>=25.1.0",
|
|
38
|
+
"isort>=6.0.0",
|
|
39
|
+
"mypy>=1.15.0",
|
|
40
|
+
"PyYAML>=6.0.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.black]
|
|
51
|
+
line-length = 88
|
|
52
|
+
target-version = ["py39"]
|
|
53
|
+
include = '\.pyi?$'
|
|
54
|
+
exclude = '''
|
|
55
|
+
/(
|
|
56
|
+
\.git
|
|
57
|
+
| \.venv
|
|
58
|
+
| \.mypy_cache
|
|
59
|
+
| \.pytest_cache
|
|
60
|
+
| __pycache__
|
|
61
|
+
| build
|
|
62
|
+
| dist
|
|
63
|
+
)/
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
[tool.isort]
|
|
67
|
+
profile = "black"
|
|
68
|
+
multi_line_output = 3
|
|
69
|
+
line_length = 88
|
|
70
|
+
known_first_party = ["src"]
|
|
71
|
+
|
|
72
|
+
[tool.mypy]
|
|
73
|
+
python_version = "3.9"
|
|
74
|
+
strict = true
|
|
75
|
+
warn_return_any = true
|
|
76
|
+
warn_unused_configs = true
|
|
77
|
+
disallow_untyped_defs = true
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
python_files = ["test_*.py"]
|
|
82
|
+
python_classes = ["Test*"]
|
|
83
|
+
python_functions = ["test_*"]
|
|
84
|
+
addopts = "-v --tb=short"
|
|
85
|
+
asyncio_mode = "auto"
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
# --------------------------------------------------------------------------
|
|
4
4
|
import secrets
|
|
5
5
|
import warnings
|
|
6
|
-
from typing import Annotated, Any, Literal
|
|
6
|
+
from typing import Annotated, Any, List, Literal, Union
|
|
7
7
|
|
|
8
8
|
from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator
|
|
9
9
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def parse_cors(v: Any) ->
|
|
13
|
+
def parse_cors(v: Any) -> Union[List[str], str]:
|
|
14
14
|
if isinstance(v, str) and not v.startswith("["):
|
|
15
15
|
return [i.strip() for i in v.split(",")]
|
|
16
|
-
elif isinstance(v, list
|
|
16
|
+
elif isinstance(v, (list, str)):
|
|
17
17
|
return v
|
|
18
18
|
raise ValueError(v)
|
|
19
19
|
|
|
@@ -30,20 +30,20 @@ class Settings(BaseSettings):
|
|
|
30
30
|
|
|
31
31
|
CLIENT_ORIGIN: str = ""
|
|
32
32
|
|
|
33
|
-
BACKEND_CORS_ORIGINS: Annotated[
|
|
33
|
+
BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = (
|
|
34
34
|
[]
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
@computed_field # type: ignore[prop-decorator]
|
|
38
38
|
@property
|
|
39
|
-
def all_cors_origins(self) ->
|
|
39
|
+
def all_cors_origins(self) -> List[str]:
|
|
40
40
|
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
|
|
41
41
|
self.CLIENT_ORIGIN
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
PROJECT_NAME: str = "<project_name>"
|
|
45
45
|
|
|
46
|
-
def _check_default_secret(self, var_name: str, value: str
|
|
46
|
+
def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None:
|
|
47
47
|
if value == "changethis":
|
|
48
48
|
message = (
|
|
49
49
|
f'The value of {var_name} is "changethis", '
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "<project_name>"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "<description>"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "<author>", email = "<author_email>"},
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"fastapi>=0.115.8",
|
|
13
|
+
"uvicorn[standard]>=0.34.0",
|
|
14
|
+
"pydantic>=2.10.6",
|
|
15
|
+
"pydantic-settings>=2.7.1",
|
|
16
|
+
"SQLAlchemy>=2.0.38",
|
|
17
|
+
"python-dotenv>=1.0.1",
|
|
18
|
+
"aiofiles>=24.1.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8.3.4",
|
|
24
|
+
"pytest-asyncio>=0.25.3",
|
|
25
|
+
"httpx>=0.28.1",
|
|
26
|
+
"black>=25.1.0",
|
|
27
|
+
"isort>=6.0.0",
|
|
28
|
+
"mypy>=1.15.0",
|
|
29
|
+
"PyYAML>=6.0.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.3.4",
|
|
35
|
+
"pytest-asyncio>=0.25.3",
|
|
36
|
+
"httpx>=0.28.1",
|
|
37
|
+
"black>=25.1.0",
|
|
38
|
+
"isort>=6.0.0",
|
|
39
|
+
"mypy>=1.15.0",
|
|
40
|
+
"PyYAML>=6.0.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src"]
|
|
49
|
+
|
|
50
|
+
[tool.black]
|
|
51
|
+
line-length = 88
|
|
52
|
+
target-version = ["py39"]
|
|
53
|
+
include = '\.pyi?$'
|
|
54
|
+
exclude = '''
|
|
55
|
+
/(
|
|
56
|
+
\.git
|
|
57
|
+
| \.venv
|
|
58
|
+
| \.mypy_cache
|
|
59
|
+
| \.pytest_cache
|
|
60
|
+
| __pycache__
|
|
61
|
+
| build
|
|
62
|
+
| dist
|
|
63
|
+
)/
|
|
64
|
+
'''
|
|
65
|
+
|
|
66
|
+
[tool.isort]
|
|
67
|
+
profile = "black"
|
|
68
|
+
multi_line_output = 3
|
|
69
|
+
line_length = 88
|
|
70
|
+
known_first_party = ["src"]
|
|
71
|
+
|
|
72
|
+
[tool.mypy]
|
|
73
|
+
python_version = "3.9"
|
|
74
|
+
strict = true
|
|
75
|
+
warn_return_any = true
|
|
76
|
+
warn_unused_configs = true
|
|
77
|
+
disallow_untyped_defs = true
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
testpaths = ["tests"]
|
|
81
|
+
python_files = ["test_*.py"]
|
|
82
|
+
python_classes = ["Test*"]
|
|
83
|
+
python_functions = ["test_*"]
|
|
84
|
+
addopts = "-v --tb=short"
|
|
85
|
+
asyncio_mode = "auto"
|
fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/api/routes/items.py-tpl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# --------------------------------------------------------------------------
|
|
2
2
|
# Item CRUD Endpoint
|
|
3
3
|
# --------------------------------------------------------------------------
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from typing import Any, List, Union
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, Request
|
|
@@ -29,7 +29,7 @@ async def read_all_items(request: Request):
|
|
|
29
29
|
try:
|
|
30
30
|
items = await read_items()
|
|
31
31
|
return ResponseSchema(
|
|
32
|
-
timestamp=datetime.now(
|
|
32
|
+
timestamp=datetime.now(timezone.utc)
|
|
33
33
|
.isoformat(timespec="milliseconds")
|
|
34
34
|
.replace("+00:00", "Z"),
|
|
35
35
|
status=200,
|
|
@@ -60,7 +60,7 @@ async def read_item(item_id: int, request: Request):
|
|
|
60
60
|
message="Item not found", error_code=ErrorCode.NOT_FOUND
|
|
61
61
|
)
|
|
62
62
|
return ResponseSchema(
|
|
63
|
-
timestamp=datetime.now(
|
|
63
|
+
timestamp=datetime.now(timezone.utc)
|
|
64
64
|
.isoformat(timespec="milliseconds")
|
|
65
65
|
.replace("+00:00", "Z"),
|
|
66
66
|
status=200,
|
|
@@ -92,7 +92,7 @@ async def create_item(item: ItemCreate, request: Request):
|
|
|
92
92
|
items.append(new_item)
|
|
93
93
|
await write_items(items)
|
|
94
94
|
return ResponseSchema(
|
|
95
|
-
timestamp=datetime.now(
|
|
95
|
+
timestamp=datetime.now(timezone.utc)
|
|
96
96
|
.isoformat(timespec="milliseconds")
|
|
97
97
|
.replace("+00:00", "Z"),
|
|
98
98
|
status=201,
|
|
@@ -124,7 +124,7 @@ async def update_item(item_id: int, item: ItemCreate, request: Request):
|
|
|
124
124
|
items[index] = updated_item
|
|
125
125
|
await write_items(items)
|
|
126
126
|
return ResponseSchema(
|
|
127
|
-
timestamp=datetime.now(
|
|
127
|
+
timestamp=datetime.now(timezone.utc)
|
|
128
128
|
.isoformat(timespec="milliseconds")
|
|
129
129
|
.replace("+00:00", "Z"),
|
|
130
130
|
status=200,
|
|
@@ -156,7 +156,7 @@ async def delete_item(item_id: int, request: Request):
|
|
|
156
156
|
)
|
|
157
157
|
await write_items(new_items)
|
|
158
158
|
return ResponseSchema(
|
|
159
|
-
timestamp=datetime.now(
|
|
159
|
+
timestamp=datetime.now(timezone.utc)
|
|
160
160
|
.isoformat(timespec="milliseconds")
|
|
161
161
|
.replace("+00:00", "Z"),
|
|
162
162
|
status=200,
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
# --------------------------------------------------------------------------
|
|
4
4
|
import secrets
|
|
5
5
|
import warnings
|
|
6
|
-
from typing import Annotated, Any, Literal
|
|
6
|
+
from typing import Annotated, Any, List, Literal, Union
|
|
7
7
|
|
|
8
8
|
from pydantic import AnyUrl, BeforeValidator, computed_field, model_validator
|
|
9
9
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
10
|
from typing_extensions import Self
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def parse_cors(v: Any) ->
|
|
13
|
+
def parse_cors(v: Any) -> Union[List[str], str]:
|
|
14
14
|
if isinstance(v, str) and not v.startswith("["):
|
|
15
15
|
return [i.strip() for i in v.split(",")]
|
|
16
|
-
elif isinstance(v, list
|
|
16
|
+
elif isinstance(v, (list, str)):
|
|
17
17
|
return v
|
|
18
18
|
raise ValueError(v)
|
|
19
19
|
|
|
@@ -30,20 +30,20 @@ class Settings(BaseSettings):
|
|
|
30
30
|
|
|
31
31
|
CLIENT_ORIGIN: str = ""
|
|
32
32
|
|
|
33
|
-
BACKEND_CORS_ORIGINS: Annotated[
|
|
33
|
+
BACKEND_CORS_ORIGINS: Annotated[Union[List[AnyUrl], str], BeforeValidator(parse_cors)] = (
|
|
34
34
|
[]
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
@computed_field # type: ignore[prop-decorator]
|
|
38
38
|
@property
|
|
39
|
-
def all_cors_origins(self) ->
|
|
39
|
+
def all_cors_origins(self) -> List[str]:
|
|
40
40
|
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [
|
|
41
41
|
self.CLIENT_ORIGIN
|
|
42
42
|
]
|
|
43
43
|
|
|
44
44
|
PROJECT_NAME: str = "<project_name>"
|
|
45
45
|
|
|
46
|
-
def _check_default_secret(self, var_name: str, value: str
|
|
46
|
+
def _check_default_secret(self, var_name: str, value: Union[str, None]) -> None:
|
|
47
47
|
if value == "changethis":
|
|
48
48
|
message = (
|
|
49
49
|
f'The value of {var_name} is "changethis", '
|
fastapi_fastkit/fastapi_project_template/fastapi-custom-response/src/helper/exceptions.py-tpl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# --------------------------------------------------------------------------
|
|
2
2
|
# The module defines custom Backend Application Exception class, overrides basic Exception.
|
|
3
3
|
# --------------------------------------------------------------------------
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
@@ -87,7 +87,7 @@ class ExceptionSchema(BaseModel):
|
|
|
87
87
|
class InternalException(Exception):
|
|
88
88
|
def __init__(self, message: str, error_code: ErrorCode):
|
|
89
89
|
self.timestamp = (
|
|
90
|
-
datetime.now(
|
|
90
|
+
datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
91
91
|
)
|
|
92
92
|
self.status = error_code.status_code
|
|
93
93
|
self.error_code = error_code.code
|