Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ restart:

format:
isort . && ruff format $(pwd)
.PHONY: format
.PHONY: format

gen-proto:
uv run -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./internal/controllers/grpc/protos/*.proto
.PHONY: gen-proto
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ The **Unit of Work pattern** ensures that multiple repository operations are **e

```
internal/
│── app/ # Defines servers and middlewares (protocols)
│── controllers/ # Handles requests and responses (endpoints)
│── domains/ # Core business logic (services, use cases, entities)
│── infrastructures/ # External dependencies (databases, caches, queues)
│── patterns/ # Dependency injection
└── main.py # Application entry point
```

- **App**: Define servers (protocols) and middlewares.
- **Controllers**: Define the API endpoints and route requests to services.
- **Domains**: Contains core business logic, including services and use cases.
- **Infrastructures**: Houses repositories and database interactions.
Expand Down Expand Up @@ -183,15 +185,28 @@ Ensure you have the following installed:
Note: Our internal/infrastructures/config_manager including the auto-reload configs function.
```

7. **Start the Application**
7. **Manage proto files**

```sh
This section is optional if you want to develop service with gRPC.

Every proto files are stored at "internal/controllers/grpc/protos"

Use the following command if you want to generate new proto:

# gen-proto
make gen-proto
```

8. **Start the Application**

```sh
python main.py
# or
./run.sh
```

8. **Alternative run with docker**
9. **Alternative run with docker**

```sh
# if you do not want to start from scratch, just run with docker
Expand All @@ -211,7 +226,7 @@ Ensure you have the following installed:

```

9. **Access the API**
10. **Access the API**

- Use `http://127.0.0.1:8082/docs` for Swagger UI.
- Use `http://127.0.0.1:8082/redoc` for Redoc documentation.
Expand Down
1 change: 1 addition & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class AppConfig(BaseSettings):
# === General App Settings ===
main_http_port: Optional[int] = 8080
health_check_http_port: Optional[int] = 5000
main_grpc_port: Optional[int] = 9090
log_level: Optional[str] = "INFO"
uvicorn_workers: Optional[int] = 1

Expand Down
1 change: 1 addition & 0 deletions config/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# === General App Settings ===
MAIN_HTTP_PORT=8082
HEALTH_CHECK_HTTP_PORT=5000
MAIN_GRPC_PORT=9090
LOG_LEVEL=INFO
UVICORN_WORKERS=1

Expand Down
2 changes: 1 addition & 1 deletion internal/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .middlewares import JWTAuthMiddleware
from .servers import init_health_check_server, init_http_server
from .servers import init_grpc_server, init_health_check_server, init_http_server
76 changes: 25 additions & 51 deletions internal/app/servers.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,23 @@
from contextlib import asynccontextmanager

import grpc
from fastapi import FastAPI, HTTPException
from fastapi.encoders import jsonable_encoder
from fastapi.responses import ORJSONResponse
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request

from config import app_config
from internal.app import JWTAuthMiddleware
from internal.controllers.grpc.protos import post_v1_pb2_grpc
from internal.controllers.grpc.v1.endpoints import PostPRouter as PostPRouterV1
from internal.controllers.http.v1.routes import api_router as api_router_v1
from internal.controllers.responses import DataResponse, MessageResponse
from internal.infrastructures.config_manager import ConfigManager
from internal.patterns import Container, initialize_relational_db
from internal.patterns.dependency_injection import close_relational_db
from utils.logger_utils import get_shared_logger
from internal.patterns import Container
from utils.logger_utils import GRPCLoggingInterceptor, get_shared_logger

logger = get_shared_logger()

app_status = {"alive": True, "status_code": 200, "message": "I'm fine"}


def init_http_server() -> FastAPI:
@asynccontextmanager
async def lifespan(app: FastAPI):
try:
# Get the container instance
container = Container()

# Load config from the config manager
if app_config.cfg_manager_service.enable:
cfg_manager = ConfigManager(
address=app_config.cfg_manager_service.url,
token=app_config.cfg_manager_service.token,
env=app_config.cfg_manager_service.env,
app_config=app_config,
di_container=container,
)
await cfg_manager.load()
await cfg_manager.update_app_config()
logger.info(f"Load config from server successfully")
else:
container.config.from_dict(app_config.model_dump())
logger.info(f"Load config from local successfully")

# Initialize relational database
await initialize_relational_db(container=container)
logger.info("Relational database initialized")

yield

# Close relational database
await close_relational_db(container=container)
logger.info("Relational database closed")
except Exception as exc:
logger.error(f"Main HTTP server crashed due to: {exc}")
app_status["alive"] = False
app_status["status_code"] = 500
app_status["message"] = str(exc)

server_ = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan)
server_ = FastAPI(default_response_class=ORJSONResponse)

server_.add_middleware(
middleware_class=CORSMiddleware,
Expand Down Expand Up @@ -95,13 +54,28 @@ async def http_exception_handler(_: Request, exc: HTTPException):
return server_


def init_health_check_server() -> FastAPI:
def init_health_check_server(app_status: DataResponse) -> FastAPI:
health_check_app = FastAPI()

@health_check_app.get("/health-check")
async def health_check():
if app_status["status_code"] != 200:
logger.info(app_status)
return ORJSONResponse(content=app_status, status_code=app_status["status_code"])
return ORJSONResponse(
content=jsonable_encoder(app_status),
status_code=app_status.message.status_code,
)

return health_check_app


def init_grpc_server(container: Container) -> grpc.aio.Server:
logging_interceptor = GRPCLoggingInterceptor()
server = grpc.aio.server(interceptors=[logging_interceptor])

# Instantiate your service
post_p_router_v1 = PostPRouterV1(container=container)

# Add your service to the gRPC server
post_v1_pb2_grpc.add_PostV1Servicer_to_server(post_p_router_v1, server)

logger.info("gRPC server initialized")
return server
29 changes: 29 additions & 0 deletions internal/controllers/grpc/protos/post_v1.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
syntax = "proto3";

// Defines a package for the protocol buffer messages to prevent name clashes.
// It's good practice to include a version number (e.g., v1).

service PostV1 {
rpc Create (CreatePostRequest) returns (CreatePostResponse) {}
rpc GetById (GetPostByIdRequest) returns (GetPostByIdResponse) {}
}

message CreatePostRequest {
string text_content = 1;
string owner_id = 2;
}

message CreatePostResponse {
string id = 1;
}

message GetPostByIdRequest {
string id = 1;
}

message GetPostByIdResponse {
string id = 1;
string text_content = 2;
string created_at = 3;
string updated_at = 4;
}
48 changes: 48 additions & 0 deletions internal/controllers/grpc/protos/post_v1_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading