FastAPI-fastkit 0.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.
- fastapi_fastkit/__init__.py +10 -0
- fastapi_fastkit/__main__.py +8 -0
- fastapi_fastkit/backend.py +112 -0
- fastapi_fastkit/cli.py +432 -0
- fastapi_fastkit/core/__init__.py +0 -0
- fastapi_fastkit/core/exceptions.py +27 -0
- fastapi_fastkit/core/settings.py +107 -0
- fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md +66 -0
- fastapi_fastkit/fastapi_project_template/README.md +20 -0
- fastapi_fastkit/fastapi_project_template/__init__.py +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/.env.test-tpl +3 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/.gitignore-tpl +191 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/main.py-tpl +21 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/requirements.txt-tpl +47 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/script/run-server.sh-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/script/run-test.sh-tpl +2 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.cfg-tpl +13 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/setup.py-tpl +35 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/.DS_Store +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/__init__.py-tpl +88 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/core/settings.py-tpl +96 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/_base.py-tpl +92 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/crud/user.py-tpl +44 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/helper/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/helper/exceptions.py-tpl +94 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/helper/global_data.py-tpl +33 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/helper/logging.py-tpl +25 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/helper/pagination.py-tpl +71 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/mocks/mock_users.json-tpl +17 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/router/__init__.py-tpl +48 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/router/user.py-tpl +126 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/schemas/__init__.py-tpl +48 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/schemas/user.py-tpl +81 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/templates/index.html-tpl +27 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/utils/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/src/utils/documents.py-tpl +20 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/test/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/test/conftest.py-tpl +22 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/test/routes/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-default/test/routes/test_user.py-tpl +112 -0
- fastapi_fastkit/fastapi_project_template/fastapi-dockerized/__init__.py-tpl +0 -0
- fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/__init__.py-tpl +0 -0
- fastapi_fastkit/py.typed +0 -0
- fastapi_fastkit/utils/__init__.py +0 -0
- fastapi_fastkit/utils/inspector.py +15 -0
- fastapi_fastkit/utils/logging.py +33 -0
- fastapi_fastkit/utils/transducer.py +70 -0
- fastapi_fastkit-0.1.0.dist-info/METADATA +46 -0
- fastapi_fastkit-0.1.0.dist-info/RECORD +56 -0
- fastapi_fastkit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_fastkit-0.1.0.dist-info/entry_points.txt +5 -0
- fastapi_fastkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines basic Model CRUD methods.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from typing import Any, Type, List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from sqlalchemy import select, text
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_json(file_path: str):
|
|
16
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
17
|
+
return json.load(f)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_object(
|
|
21
|
+
db: AsyncSession, model: Any, model_id: int | str, response_model: Type[BaseModel]
|
|
22
|
+
) -> Optional[Any]:
|
|
23
|
+
result = await db.get(model, model_id)
|
|
24
|
+
return response_model.model_validate(result.__dict__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_object_with_uuid(
|
|
28
|
+
db: AsyncSession, model: Any, model_uid: str, response_model: Type[BaseModel]
|
|
29
|
+
) -> Optional[Any]:
|
|
30
|
+
result = await db.get(model, model_uid)
|
|
31
|
+
return response_model.model_validate(result.__dict__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def get_objects(
|
|
35
|
+
db: AsyncSession,
|
|
36
|
+
model: Any,
|
|
37
|
+
response_model: Type[BaseModel],
|
|
38
|
+
condition: Optional[Any] = None,
|
|
39
|
+
skip: int = 0,
|
|
40
|
+
limit: int = 100,
|
|
41
|
+
) -> List[Any]:
|
|
42
|
+
query = select(model).offset(skip).limit(limit)
|
|
43
|
+
if condition is not None:
|
|
44
|
+
query = query.where(text(condition))
|
|
45
|
+
result = await db.execute(query)
|
|
46
|
+
result_list = result.scalars().all()
|
|
47
|
+
return [response_model.model_validate(item.__dict__) for item in result_list]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def create_object(
|
|
51
|
+
db: AsyncSession, model: Any, obj: BaseModel, response_model: Type[BaseModel]
|
|
52
|
+
) -> Any:
|
|
53
|
+
obj_data = obj.model_dump()
|
|
54
|
+
db_obj = model(**obj_data)
|
|
55
|
+
|
|
56
|
+
db.add(db_obj)
|
|
57
|
+
await db.commit()
|
|
58
|
+
await db.refresh(db_obj)
|
|
59
|
+
return response_model.model_validate(db_obj.__dict__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def update_object(
|
|
63
|
+
db: AsyncSession,
|
|
64
|
+
model: Any,
|
|
65
|
+
model_id: int | str,
|
|
66
|
+
obj: BaseModel,
|
|
67
|
+
response_model: Type[BaseModel],
|
|
68
|
+
) -> Optional[Any]:
|
|
69
|
+
query = select(model).filter(model.id == model_id)
|
|
70
|
+
db_obj = (await db.execute(query)).scalar_one_or_none()
|
|
71
|
+
if db_obj is None:
|
|
72
|
+
return None
|
|
73
|
+
update_data = obj.model_dump(exclude_unset=True)
|
|
74
|
+
for key, value in update_data.items():
|
|
75
|
+
setattr(db_obj, key, value)
|
|
76
|
+
db.add(db_obj)
|
|
77
|
+
await db.commit()
|
|
78
|
+
await db.refresh(db_obj)
|
|
79
|
+
return response_model.model_validate(db_obj.__dict__)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def delete_object(
|
|
83
|
+
db: AsyncSession, model: Any, model_id: int | str
|
|
84
|
+
) -> Optional[int]:
|
|
85
|
+
query = select(model).filter(model.id == model_id)
|
|
86
|
+
db_obj = (await db.execute(query)).scalar_one_or_none()
|
|
87
|
+
if db_obj:
|
|
88
|
+
await db.delete(db_obj)
|
|
89
|
+
await db.commit()
|
|
90
|
+
else:
|
|
91
|
+
return None
|
|
92
|
+
return model_id
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines User model's CRUD methods.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from typing import Type
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from ._base import load_json
|
|
10
|
+
from src.helper.exceptions import InternalException, ErrorCode
|
|
11
|
+
from src.schemas.user import UserSchema, UserCreate, UserUpdate
|
|
12
|
+
from src.helper.global_data import mock_user_data
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_mock_user_data(id: UUID) -> dict:
|
|
16
|
+
user_data = next((user for user in mock_user_data if user["id"] == str(id)), None)
|
|
17
|
+
if user_data is None:
|
|
18
|
+
raise InternalException(
|
|
19
|
+
message="User not found.", error_code=ErrorCode.NOT_FOUND
|
|
20
|
+
)
|
|
21
|
+
return user_data
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_mock_user(user: UserCreate) -> dict:
|
|
25
|
+
new_user = user.model_dump()
|
|
26
|
+
if any(u["email"] == new_user["email"] for u in mock_user_data):
|
|
27
|
+
raise InternalException(
|
|
28
|
+
error_code=ErrorCode.CONFLICT,
|
|
29
|
+
message="This user is already exist.",
|
|
30
|
+
)
|
|
31
|
+
mock_user_data.append(new_user)
|
|
32
|
+
return new_user
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def update_mock_user(id: UUID, user: UserUpdate) -> dict:
|
|
36
|
+
user_data = get_mock_user_data(id)
|
|
37
|
+
update_data = user.model_dump(exclude_unset=True)
|
|
38
|
+
user_data.update(update_data)
|
|
39
|
+
return user_data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def delete_mock_user(id: UUID) -> None:
|
|
43
|
+
user_data = get_mock_user_data(id)
|
|
44
|
+
mock_user_data.remove(user_data)
|
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines custom Backend Application Exception class, overrides basic Exception.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from src.schemas import ResponseSchema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ErrorCode(Enum):
|
|
13
|
+
"""
|
|
14
|
+
ErrorCode is designed to easily categorize errors into code in the log of the server application.
|
|
15
|
+
|
|
16
|
+
It is designed for custom code-based log filtering in third-party log monitoring systems
|
|
17
|
+
by matching with key HTTP status codes.
|
|
18
|
+
"""
|
|
19
|
+
# HTTP
|
|
20
|
+
BAD_REQUEST = ("BAD_REQUEST", "HTTP-001", 400)
|
|
21
|
+
NOT_FOUND = ("NOT_FOUND", "HTTP-002", 404)
|
|
22
|
+
METHOD_NOT_ALLOWED = ("METHOD_NOT_ALLOWED", "HTTP-003", 405)
|
|
23
|
+
NOT_ACCESSABLE = ("NOT_ACCESSABLE", "HTTP-004", 406)
|
|
24
|
+
TIMEOUT = ("TIMEOUT", "HTTP-005", 408)
|
|
25
|
+
UNPROCESSABLE = ("UNPROCESSABLE", "HTTP-006", 422)
|
|
26
|
+
TOO_MANY_REQUEST = ("TOO_MANY_REQUEST", "HTTP-007", 429)
|
|
27
|
+
|
|
28
|
+
# DATA
|
|
29
|
+
CONFLICT = ("CONFLICT", "DATA-001", 409)
|
|
30
|
+
|
|
31
|
+
# AUTH
|
|
32
|
+
UNAUTHORIZED = ("UNAUTHORIZED", "AUTH-001", 401)
|
|
33
|
+
FORBIDDEN = ("FORBIDDEN", "AUTH-002", 403)
|
|
34
|
+
|
|
35
|
+
# SERVER
|
|
36
|
+
UNKNOWN_ERROR = ("UNKNOWN_ERROR", "SEVR-001", 500)
|
|
37
|
+
BAD_GATEWAY = ("BAD_GATEWAY", "SEVR-002", 502)
|
|
38
|
+
SERVICE_UNAVAILABLE = ("SERVICE_UNAVAILABLE", "SEVR-003", 503)
|
|
39
|
+
GATEWAY_TIMEOUT = ("GATEWAY_TIMEOUT", "SEVR-004", 504)
|
|
40
|
+
|
|
41
|
+
def __init__(self, error: str, code: str, status_code: int):
|
|
42
|
+
self.error = error
|
|
43
|
+
self.code = code
|
|
44
|
+
self.status_code = status_code
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExceptionSchema(BaseModel):
|
|
48
|
+
timestamp: str = Field(
|
|
49
|
+
...,
|
|
50
|
+
description="Timestamp of the error occurred.",
|
|
51
|
+
)
|
|
52
|
+
status: int = Field(..., description="HTTP status code of the error")
|
|
53
|
+
code: str = Field(
|
|
54
|
+
...,
|
|
55
|
+
description="Server identification code of the error.",
|
|
56
|
+
)
|
|
57
|
+
message: str = Field(
|
|
58
|
+
...,
|
|
59
|
+
description="Message content of the error.",
|
|
60
|
+
)
|
|
61
|
+
path: str = Field(
|
|
62
|
+
...,
|
|
63
|
+
description="Path where the error occurred.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
class ConfigDict:
|
|
67
|
+
json_schema_extra = {
|
|
68
|
+
"example": {
|
|
69
|
+
"default": {
|
|
70
|
+
"timestamp": "2023-02-10T01:00:00.000Z",
|
|
71
|
+
"status": 500,
|
|
72
|
+
"code": "SEVR-000",
|
|
73
|
+
"path": "/v1/<some/endpoint>",
|
|
74
|
+
"message": "ERROR : Unknown error occurred in server logic.",
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InternalException(Exception):
|
|
81
|
+
def __init__(self, message: str, error_code: ErrorCode):
|
|
82
|
+
self.timestamp = datetime.utcnow().isoformat() + "Z"
|
|
83
|
+
self.status = error_code.status_code
|
|
84
|
+
self.error_code = error_code.code
|
|
85
|
+
self.message = f"ERROR : {message}"
|
|
86
|
+
|
|
87
|
+
def to_response(self, path: str) -> ResponseSchema[str]:
|
|
88
|
+
return ResponseSchema(
|
|
89
|
+
timestamp=self.timestamp,
|
|
90
|
+
status=self.status,
|
|
91
|
+
code=self.error_code,
|
|
92
|
+
path=path,
|
|
93
|
+
message=self.message,
|
|
94
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module initialize Mocking Data storage at FastAPI main coroutine's
|
|
3
|
+
# memory space.
|
|
4
|
+
# --------------------------------------------------------------------------
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
# Global Mock Data storage (Stores : FastAPI app coroutine - In FastAPI thread memory)
|
|
9
|
+
mock_user_data = []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def initialize_mock_data() -> None:
|
|
13
|
+
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
|
14
|
+
user_file_path = os.path.join(base_path, "src", "mocks", "mock_users.json")
|
|
15
|
+
|
|
16
|
+
with open(user_file_path, "r", encoding="utf-8") as f:
|
|
17
|
+
user_data = json.load(f)
|
|
18
|
+
|
|
19
|
+
# If the model has relations with other model,
|
|
20
|
+
# you can map it their respective columns like below:
|
|
21
|
+
item_data = [{'some': 'item', "user_id": "123e4567-e89b-12d3-a456-426614174000"}]
|
|
22
|
+
item_map = {}
|
|
23
|
+
for item in item_data:
|
|
24
|
+
user_id = item["user_id"]
|
|
25
|
+
if user_id not in item_map:
|
|
26
|
+
item_map[user_id] = []
|
|
27
|
+
item_map[user_id].append(item)
|
|
28
|
+
|
|
29
|
+
for user in user_data:
|
|
30
|
+
user_id = user["id"]
|
|
31
|
+
user["items"] = item_map.get(user_id, [])
|
|
32
|
+
|
|
33
|
+
mock_user_data.extend(user_data)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines Backend Application's logger.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
import logging
|
|
5
|
+
import logging.handlers
|
|
6
|
+
|
|
7
|
+
from src.core.settings import AppSettings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
LOGGING_FORMAT = (
|
|
11
|
+
"[%(levelname)1.1s "
|
|
12
|
+
"%(asctime)s "
|
|
13
|
+
"P%(process)d "
|
|
14
|
+
"%(threadName)s "
|
|
15
|
+
"%(module)s:%(lineno)d] "
|
|
16
|
+
"%(message)s"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init_logger(root_logger_name: str, app_settings: AppSettings) -> logging.Logger:
|
|
21
|
+
app_logger_level = (
|
|
22
|
+
logging.DEBUG if app_settings.LOGGING_DEBUG_LEVEL else logging.INFO
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logging.basicConfig(level=app_logger_level, format=LOGGING_FORMAT)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines paginatable object schemas' helper methods.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from typing import TypeVar, Generic, List, Optional
|
|
5
|
+
from pydantic import BaseModel, Field, AnyHttpUrl
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
12
|
+
count: int = Field(
|
|
13
|
+
..., title="Count", description="Indicates the total number of imported items."
|
|
14
|
+
)
|
|
15
|
+
next: Optional[AnyHttpUrl] = Field(
|
|
16
|
+
None,
|
|
17
|
+
title="Next",
|
|
18
|
+
description="Indicates the URL of the next page to import the data.",
|
|
19
|
+
)
|
|
20
|
+
previous: Optional[AnyHttpUrl] = Field(
|
|
21
|
+
None,
|
|
22
|
+
title="Previous",
|
|
23
|
+
description="Indicates the URL of the previous page from which the data was imported.",
|
|
24
|
+
)
|
|
25
|
+
results: List[T] = Field(
|
|
26
|
+
...,
|
|
27
|
+
title="Results",
|
|
28
|
+
description="Lists the information of the imported items.",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Paginator:
|
|
33
|
+
def __init__(self, data: List[T], page: int, per_page: int, request: Request):
|
|
34
|
+
self.data = data
|
|
35
|
+
self.page = page
|
|
36
|
+
self.page_size = per_page
|
|
37
|
+
self.limit = per_page
|
|
38
|
+
self.offset = (page - 1) * per_page
|
|
39
|
+
self.request = request
|
|
40
|
+
self.number_of_pages = self._get_number_of_pages(len(data))
|
|
41
|
+
|
|
42
|
+
def _get_next_page(self) -> Optional[str]:
|
|
43
|
+
if self.page >= self.number_of_pages:
|
|
44
|
+
return None
|
|
45
|
+
url = self.request.url.include_query_params(page=self.page + 1)
|
|
46
|
+
return str(url)
|
|
47
|
+
|
|
48
|
+
def _get_previous_page(self) -> Optional[str]:
|
|
49
|
+
if self.page == 1:
|
|
50
|
+
return None
|
|
51
|
+
url = self.request.url.include_query_params(page=self.page - 1)
|
|
52
|
+
return str(url)
|
|
53
|
+
|
|
54
|
+
def get_response(self) -> dict:
|
|
55
|
+
paginated_data = self.data[self.offset : self.offset + self.limit]
|
|
56
|
+
return {
|
|
57
|
+
"count": len(self.data),
|
|
58
|
+
"next": self._get_next_page(),
|
|
59
|
+
"previous": self._get_previous_page(),
|
|
60
|
+
"results": paginated_data,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def _get_number_of_pages(self, count: int) -> int:
|
|
64
|
+
rest = count % self.page_size
|
|
65
|
+
quotient = count // self.page_size
|
|
66
|
+
return quotient if not rest else quotient + 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def paginate(data: List[T], page: int, page_size: int, request: Request) -> dict:
|
|
70
|
+
paginator = Paginator(data, page, page_size, request)
|
|
71
|
+
return paginator.get_response()
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
4
|
+
"userId": "bnbong",
|
|
5
|
+
"email": "bbbong9@gmail.com",
|
|
6
|
+
"roles": ["NORMAL_USER"],
|
|
7
|
+
"profileImageUrl": null,
|
|
8
|
+
"bio": "hello, My name is JunHyeok Lee.",
|
|
9
|
+
"firstName": "JunHyeok",
|
|
10
|
+
"lastName": "Lee",
|
|
11
|
+
"gender": "M",
|
|
12
|
+
"country": "KOREA",
|
|
13
|
+
"isActive": true,
|
|
14
|
+
"createdDate": "2000-02-10T01:53:16.707426",
|
|
15
|
+
"password": "password123"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module connect routers to Backend Application.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, status, Request
|
|
11
|
+
from fastapi.responses import JSONResponse, HTMLResponse
|
|
12
|
+
from fastapi.templating import Jinja2Templates
|
|
13
|
+
|
|
14
|
+
from src.helper.exceptions import InternalException
|
|
15
|
+
|
|
16
|
+
from .user import router as user_router
|
|
17
|
+
|
|
18
|
+
router = APIRouter(prefix="/v1")
|
|
19
|
+
|
|
20
|
+
router.include_router(user_router, tags=["user"])
|
|
21
|
+
|
|
22
|
+
templates = Jinja2Templates(os.path.join(os.path.dirname(__file__), "../templates"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
|
26
|
+
async def root(request: Request):
|
|
27
|
+
return templates.TemplateResponse("index.html", {"request": request})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get(
|
|
31
|
+
"/ping",
|
|
32
|
+
summary="Server health check",
|
|
33
|
+
description="Checking FastAPI server's health.",
|
|
34
|
+
response_model=dict,
|
|
35
|
+
responses={
|
|
36
|
+
200: {
|
|
37
|
+
"description": "Ping Success",
|
|
38
|
+
"content": {"application/json": {"example": {"ping": "pong"}}},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
async def ping():
|
|
43
|
+
return {"ping": "pong"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_json(file_path: str):
|
|
47
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
48
|
+
return json.load(f)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines User router.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from . import *
|
|
5
|
+
|
|
6
|
+
from src.helper.global_data import mock_user_data
|
|
7
|
+
from src.crud.user import get_mock_user_data
|
|
8
|
+
from src.schemas import ResponseSchema
|
|
9
|
+
from src.schemas.user import UserSchema, UserCreate, UserUpdate
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
router = APIRouter(
|
|
13
|
+
prefix="/user",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_new_user_id():
|
|
18
|
+
if mock_user_data:
|
|
19
|
+
return max(_user["id"] for _user in mock_user_data) + 1
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post(
|
|
24
|
+
"/",
|
|
25
|
+
summary="Create a new user.",
|
|
26
|
+
status_code=status.HTTP_201_CREATED,
|
|
27
|
+
response_model=UserSchema,
|
|
28
|
+
)
|
|
29
|
+
async def create_user_route(
|
|
30
|
+
data: UserCreate,
|
|
31
|
+
request: Request,
|
|
32
|
+
):
|
|
33
|
+
try:
|
|
34
|
+
new_user = create_mock_user(data)
|
|
35
|
+
response = ResponseSchema(
|
|
36
|
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
|
37
|
+
status=201,
|
|
38
|
+
code="HTTP-201",
|
|
39
|
+
path=str(request.url),
|
|
40
|
+
message=UserSchema(**new_user),
|
|
41
|
+
)
|
|
42
|
+
return response
|
|
43
|
+
except InternalException as e:
|
|
44
|
+
return JSONResponse(
|
|
45
|
+
status_code=e.status,
|
|
46
|
+
content=e.to_response(path=str(request.url)).model_dump(),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get(
|
|
51
|
+
"/{id}",
|
|
52
|
+
summary="Get a user info",
|
|
53
|
+
description="Inquires information about a specific user.",
|
|
54
|
+
response_model=ResponseSchema[UserSchema],
|
|
55
|
+
)
|
|
56
|
+
async def get_user_route(id: UUID, request: Request):
|
|
57
|
+
try:
|
|
58
|
+
user = get_mock_user_data(id)
|
|
59
|
+
response = ResponseSchema(
|
|
60
|
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
|
61
|
+
status=200,
|
|
62
|
+
code="HTTP-200",
|
|
63
|
+
path=str(request.url),
|
|
64
|
+
message=UserSchema(**user),
|
|
65
|
+
)
|
|
66
|
+
return response
|
|
67
|
+
except InternalException as e:
|
|
68
|
+
return JSONResponse(
|
|
69
|
+
status_code=e.status,
|
|
70
|
+
content=e.to_response(path=str(request.url)).model_dump(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.patch(
|
|
75
|
+
"/{id}",
|
|
76
|
+
summary="Update a user.",
|
|
77
|
+
status_code=status.HTTP_200_OK,
|
|
78
|
+
response_model=UserSchema,
|
|
79
|
+
)
|
|
80
|
+
async def update_user_route(
|
|
81
|
+
id: UUID,
|
|
82
|
+
data: UserUpdate,
|
|
83
|
+
):
|
|
84
|
+
try:
|
|
85
|
+
user = get_mock_user_data(id)
|
|
86
|
+
if user["username"] != request_user:
|
|
87
|
+
raise InternalException(
|
|
88
|
+
message="ERROR : You do not have permission to modify.", error_code=ErrorCode.UNAUTHORIZED
|
|
89
|
+
)
|
|
90
|
+
user.update(data.dict(exclude_unset=True))
|
|
91
|
+
user["modifiedAt"] = datetime.utcnow().isoformat() + "Z"
|
|
92
|
+
mock_user_data[mock_user_data.index(user)] = user
|
|
93
|
+
response = ResponseSchema(
|
|
94
|
+
timestamp=datetime.utcnow().isoformat() + "Z",
|
|
95
|
+
status=200,
|
|
96
|
+
code="HTTP-200",
|
|
97
|
+
path=str(request.url),
|
|
98
|
+
message=UserSchema(**user),
|
|
99
|
+
)
|
|
100
|
+
return response
|
|
101
|
+
except InternalException as e:
|
|
102
|
+
return JSONResponse(
|
|
103
|
+
status_code=e.status,
|
|
104
|
+
content=e.to_response(path=str(request.url)).model_dump(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.delete(
|
|
109
|
+
"/{id}",
|
|
110
|
+
summary="Delete a user.",
|
|
111
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
112
|
+
)
|
|
113
|
+
async def delete_user_route(id: UUID):
|
|
114
|
+
try:
|
|
115
|
+
user = get_mock_user_data(id)
|
|
116
|
+
if user["username"] != request_user:
|
|
117
|
+
raise InternalException(
|
|
118
|
+
message="ERROR : You do not have permission to delete.", error_code=ErrorCode.UNAUTHORIZED
|
|
119
|
+
)
|
|
120
|
+
mock_user_data.remove(user)
|
|
121
|
+
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content="")
|
|
122
|
+
except InternalException as e:
|
|
123
|
+
return JSONResponse(
|
|
124
|
+
status_code=e.status,
|
|
125
|
+
content=e.to_response(path=str(request.url)).model_dump(),
|
|
126
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines Base response schemas.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponseSchema(BaseModel, Generic[T]):
|
|
11
|
+
timestamp: str = Field(
|
|
12
|
+
...,
|
|
13
|
+
description="The timestamp when the response was generated.",
|
|
14
|
+
)
|
|
15
|
+
status: int = Field(..., description="HTTP status code.")
|
|
16
|
+
code: str = Field(
|
|
17
|
+
...,
|
|
18
|
+
description="Server identification code.",
|
|
19
|
+
)
|
|
20
|
+
path: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="Request path.",
|
|
23
|
+
)
|
|
24
|
+
message: T = Field(
|
|
25
|
+
...,
|
|
26
|
+
description="Data details or error messages requested.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class ConfigDict:
|
|
30
|
+
json_schema_extra = {
|
|
31
|
+
"example": {
|
|
32
|
+
"timestamp": "2023-02-10T01:00:00.000Z",
|
|
33
|
+
"status": 200,
|
|
34
|
+
"code": "HTTP-200",
|
|
35
|
+
"path": "/v1/<some/endpoint>",
|
|
36
|
+
"message": {
|
|
37
|
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
38
|
+
"email": "example@example.com",
|
|
39
|
+
"password": "secret",
|
|
40
|
+
"nickname": "example_nick",
|
|
41
|
+
"create_at": "2023-02-10T01:00:00.000Z",
|
|
42
|
+
"bio": "example bio",
|
|
43
|
+
"profile_img": "https://example.com/profile.jpg",
|
|
44
|
+
"first_name": "John",
|
|
45
|
+
"last_name": "Doe",
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# --------------------------------------------------------------------------
|
|
2
|
+
# The module defines User schemas.
|
|
3
|
+
# --------------------------------------------------------------------------
|
|
4
|
+
from datetime import datetime, date
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, EmailStr, SecretStr, field_serializer
|
|
9
|
+
from typing import Optional, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserBase(BaseModel):
|
|
13
|
+
email: EmailStr = Field(
|
|
14
|
+
..., title="User's Email", description="The email address of the user."
|
|
15
|
+
)
|
|
16
|
+
bio: str = Field(None, title="User's bio", description="Personal introduction of the user.")
|
|
17
|
+
firstName: str = Field(
|
|
18
|
+
..., title="User's first name", description="The real name of the user."
|
|
19
|
+
)
|
|
20
|
+
lastName: str = Field(
|
|
21
|
+
..., title="User's last name", description="The last name of the user."
|
|
22
|
+
)
|
|
23
|
+
gender: str = Field(..., title="User's gender", description="The gender of the user.")
|
|
24
|
+
country: str = Field(..., title="User's country", description="Country of the user.")
|
|
25
|
+
isActive: bool = Field(
|
|
26
|
+
..., title="User's active status", description="The active state of the user."
|
|
27
|
+
)
|
|
28
|
+
createdDate: datetime = Field(
|
|
29
|
+
...,
|
|
30
|
+
title="User's account created date",
|
|
31
|
+
description="Date of creation of the user account.",
|
|
32
|
+
)
|
|
33
|
+
password: Optional[SecretStr] = Field(
|
|
34
|
+
None, title="User's password", description="Password of the user account."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# items: Optional[List] = List[ItemSchema]
|
|
38
|
+
|
|
39
|
+
@field_serializer("password", when_used="json")
|
|
40
|
+
def dump_secret(self, v):
|
|
41
|
+
return v.get_secret_value()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UserSchema(UserBase):
|
|
45
|
+
id: UUID = Field(
|
|
46
|
+
..., title="User's ID (pk)", description="The unique database identifier for the user."
|
|
47
|
+
)
|
|
48
|
+
userId: str = Field(..., title="User's ID", description="ID of the user account.")
|
|
49
|
+
roles: List[str] = Field(
|
|
50
|
+
..., title="User's roles", description="List of roles for the user."
|
|
51
|
+
)
|
|
52
|
+
profileImageUrl: Optional[str] = Field(
|
|
53
|
+
None,
|
|
54
|
+
title="User's profile image URL",
|
|
55
|
+
description="The URL of the user's profile image.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class ConfigDict:
|
|
59
|
+
from_attributes = True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UserCreate(UserBase):
|
|
63
|
+
firstName: str = Field(
|
|
64
|
+
..., title="User's first name", description="The real name of the user."
|
|
65
|
+
)
|
|
66
|
+
lastName: str = Field(
|
|
67
|
+
..., title="User's last name", description="The last name of the user."
|
|
68
|
+
)
|
|
69
|
+
email: EmailStr = Field(
|
|
70
|
+
..., title="User's Email", description="The email address of the user."
|
|
71
|
+
)
|
|
72
|
+
password: str = Field(
|
|
73
|
+
..., title="User's password", description="Password of the user account."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class UserUpdate(BaseModel):
|
|
78
|
+
bio: str = Field(None, title="User's bio", description="Personal introduction of the user.")
|
|
79
|
+
profileImg: str = Field(
|
|
80
|
+
None, title="User's profile image", description="The URL of the user's profile image."
|
|
81
|
+
)
|