From 486fadbfb3c90f77fadf6911ec74701f57dbd076 Mon Sep 17 00:00:00 2001 From: berkay Date: Tue, 14 Jan 2025 19:16:24 +0300 Subject: [PATCH] auth service completed and tested --- DockerApiServices/AllApiNeeds/app.py | 37 ++++ DockerApiServices/AllApiNeeds/app_handler.py | 121 ++++++++++ .../AllApiNeeds/application/__init__.py | 1 + .../AllApiNeeds/application/app.py | 21 ++ DockerApiServices/AllApiNeeds/create_file.py | 152 +++++++++++++ .../AllApiNeeds/middleware/__init__.py | 1 + .../AllApiNeeds/middleware/auth_middleware.py | 179 +++++++++++++++ .../AllApiNeeds/open_api_creator.py | 209 ++++++++++++++++++ .../AllApiNeeds/routers/__init__.py | 5 + .../AllApiNeeds/routers/base_router.py | 22 ++ DockerApiServices/AuthServiceApi/Dockerfile | 30 +++ .../AuthServiceApi/requirements.txt | 15 ++ DockerApiServices/EventServiceApi/Dockerfile | 23 ++ DockerApiServices/README.md | 59 +++++ .../ValidationServiceApi/Dockerfile | 23 ++ DockerApiServices/pyproject.toml | 76 +++++++ DockerApiServices/requirements.txt | 17 ++ DockerApiServices/steps.txt | 31 ++- README.md | 85 ++++++- Services/MongoDb/Models/action_models/base.py | 48 ++-- .../MongoDb/Models/action_models/domain.py | 40 ++-- .../MongoDb/Models/action_models/password.py | 13 +- Services/MongoDb/Models/actions.py | 39 +--- Services/MongoDb/Models/exception_handlers.py | 81 ++++--- Services/MongoDb/Models/exceptions.py | 68 +++--- Services/MongoDb/Models/mixins.py | 19 +- Services/MongoDb/database.py | 16 +- Services/PostgresDb/database.py | 3 +- Ztest/fixtures.py | 13 ++ Ztest/models.py | 13 ++ Ztest/test_mongo.py | 40 ++-- Ztest/test_postgres.py | 51 ++--- docker-compose-services.yml | 22 ++ 33 files changed, 1325 insertions(+), 248 deletions(-) create mode 100644 DockerApiServices/AllApiNeeds/app.py create mode 100644 DockerApiServices/AllApiNeeds/app_handler.py create mode 100644 DockerApiServices/AllApiNeeds/application/__init__.py create mode 100644 DockerApiServices/AllApiNeeds/application/app.py create mode 100644 DockerApiServices/AllApiNeeds/create_file.py create mode 100644 DockerApiServices/AllApiNeeds/middleware/__init__.py create mode 100644 DockerApiServices/AllApiNeeds/middleware/auth_middleware.py create mode 100644 DockerApiServices/AllApiNeeds/open_api_creator.py create mode 100644 DockerApiServices/AllApiNeeds/routers/__init__.py create mode 100644 DockerApiServices/AllApiNeeds/routers/base_router.py create mode 100644 DockerApiServices/AuthServiceApi/Dockerfile create mode 100644 DockerApiServices/AuthServiceApi/requirements.txt create mode 100644 DockerApiServices/EventServiceApi/Dockerfile create mode 100644 DockerApiServices/README.md create mode 100644 DockerApiServices/ValidationServiceApi/Dockerfile create mode 100644 DockerApiServices/pyproject.toml create mode 100644 DockerApiServices/requirements.txt create mode 100644 Ztest/fixtures.py create mode 100644 Ztest/models.py create mode 100644 docker-compose-services.yml diff --git a/DockerApiServices/AllApiNeeds/app.py b/DockerApiServices/AllApiNeeds/app.py new file mode 100644 index 0000000..0e06c49 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/app.py @@ -0,0 +1,37 @@ +""" +FastAPI Application Entry Point + +This module initializes and configures the FastAPI application with: +- CORS middleware for cross-origin requests +- Request timing middleware for performance monitoring +- Custom exception handlers for consistent error responses +- Prometheus instrumentation for metrics +- API routers for endpoint organization +""" + +import uvicorn +import routers + +from create_file import create_app +from prometheus_fastapi_instrumentator import Instrumentator +from app_handler import setup_middleware, get_uvicorn_config + + +print("Loading app.py module...") + +# Initialize FastAPI application +app = create_app(routers=routers) + +# Setup Prometheus metrics +Instrumentator().instrument(app=app).expose(app=app) + +# Configure middleware and exception handlers +setup_middleware(app) + + +if __name__ == "__main__": + print("Starting server from __main__...") + # Run the application with Uvicorn + uvicorn_config = get_uvicorn_config() + print(f"Using config: {uvicorn_config}") + uvicorn.Server(uvicorn.Config(**uvicorn_config)).run() diff --git a/DockerApiServices/AllApiNeeds/app_handler.py b/DockerApiServices/AllApiNeeds/app_handler.py new file mode 100644 index 0000000..9a5c290 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/app_handler.py @@ -0,0 +1,121 @@ +""" +FastAPI Application Handler Module + +This module contains all the handler functions for configuring and setting up the FastAPI application: +- CORS middleware configuration +- Exception handlers setup +- Uvicorn server configuration +""" + +from typing import Dict, Any + +from fastapi.middleware.cors import CORSMiddleware +from fastapi import FastAPI, Request, HTTPException, status +from fastapi.responses import JSONResponse + +from ErrorHandlers.bases import ( + BaseErrorModelClass, + StatusesModelClass, + LanguageModelClass, +) +from ErrorHandlers import statuses +from middleware.auth_middleware import MiddlewareModule + + +def setup_cors_middleware(app: FastAPI) -> None: + """ + Configure CORS middleware for the application. + + Args: + app: FastAPI application instance + """ + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + """ + Handle HTTP exceptions and return formatted error responses. + + Args: + request: FastAPI request object + exc: HTTP exception instance + + Returns: + JSONResponse: Formatted error response + """ + error_code = getattr(exc, "error_code", None) + if error_code: + status_code = StatusesModelClass.retrieve_error_by_code(error_code) + error_message = LanguageModelClass.retrieve_error_by_code( + error_code, request.headers.get("accept-language", "en") + ) + else: + status_code = exc.status_code + error_message = str(exc.detail) + + return JSONResponse( + status_code=status_code, + content={"detail": error_message, "error_code": error_code}, + ) + + +async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """ + Handle generic exceptions and return formatted error responses. + + Args: + request: FastAPI request object + exc: Exception instance + + Returns: + JSONResponse: Formatted error response + """ + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={"detail": "Internal server error", "error_code": "INTERNAL_ERROR"}, + ) + + +def setup_exception_handlers(app: FastAPI) -> None: + """ + Configure custom exception handlers for the application. + + Args: + app: FastAPI application instance + """ + app.add_exception_handler(HTTPException, http_exception_handler) + app.add_exception_handler(Exception, generic_exception_handler) + + +def setup_middleware(app: FastAPI) -> None: + """ + Configure all middleware for the application. + + Args: + app: FastAPI application instance + """ + setup_cors_middleware(app) + app.add_middleware(MiddlewareModule.RequestTimingMiddleware) + setup_exception_handlers(app) + + +def get_uvicorn_config() -> Dict[str, Any]: + """ + Get Uvicorn server configuration. + + Returns: + Dict[str, Any]: Uvicorn configuration dictionary + """ + return { + "app": "app:app", + "host": "0.0.0.0", + "port": 41575, + "log_level": "info", + "reload": True, + } \ No newline at end of file diff --git a/DockerApiServices/AllApiNeeds/application/__init__.py b/DockerApiServices/AllApiNeeds/application/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/application/__init__.py @@ -0,0 +1 @@ + diff --git a/DockerApiServices/AllApiNeeds/application/app.py b/DockerApiServices/AllApiNeeds/application/app.py new file mode 100644 index 0000000..257add1 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/application/app.py @@ -0,0 +1,21 @@ +""" +Base FastAPI application configuration. +""" + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + + +def create_app() -> FastAPI: + app = FastAPI(title="API Service") + + # Configure CORS + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + return app diff --git a/DockerApiServices/AllApiNeeds/create_file.py b/DockerApiServices/AllApiNeeds/create_file.py new file mode 100644 index 0000000..b5a3cb3 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/create_file.py @@ -0,0 +1,152 @@ +""" +FastAPI Application Factory Module + +This module provides functionality to create and configure a FastAPI application with: +- Custom OpenAPI schema configuration +- Security scheme configuration for Bearer authentication +- Automatic router registration +- Response class configuration +- Security requirements for protected endpoints +""" +from types import ModuleType +from typing import Any, Dict, List, Optional, Union +from fastapi import FastAPI, APIRouter +from fastapi.responses import JSONResponse, RedirectResponse +from fastapi.openapi.utils import get_openapi +from fastapi.routing import APIRoute +from AllConfigs.main import MainConfig as Config +from middleware.auth_middleware import MiddlewareModule + + +def setup_security_schema() -> Dict[str, Any]: + """ + Configure security schema for the OpenAPI documentation. + + Returns: + Dict[str, Any]: Security schema configuration + """ + return { + "Bearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Enter the token", + } + } + + +def configure_route_security( + path: str, + method: str, + schema: Dict[str, Any], + protected_paths: List[str] +) -> None: + """ + Configure security requirements for a specific route. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + protected_paths: List of paths that require authentication + """ + if path in protected_paths: + if "paths" not in schema: + schema["paths"] = {} + + if path not in schema["paths"]: + schema["paths"][path] = {} + + if method not in schema["paths"][path]: + schema["paths"][path][method] = {} + + schema["paths"][path][method]["security"] = [{"Bearer": []}] + + +def get_routers(routers_module: ModuleType) -> List[APIRouter]: + """ + Extract all routers from the routers module. + + Args: + routers_module: Module containing router definitions + + Returns: + List[APIRouter]: List of router instances + """ + routers = [] + for attr_name in dir(routers_module): + attr = getattr(routers_module, attr_name) + if isinstance(attr, APIRouter): + routers.append(attr) + return routers + + +def create_app(routers: ModuleType) -> FastAPI: + """ + Create and configure a FastAPI application. + + Args: + routers: Module containing router definitions + + Returns: + FastAPI: Configured FastAPI application instance + """ + # Initialize FastAPI app + app = FastAPI( + title=Config.TITLE, + description=Config.DESCRIPTION, + default_response_class=JSONResponse, + ) + + # Add home route that redirects to API documentation + @app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION)) + async def home() -> RedirectResponse: + """Redirect root path to API documentation.""" + return RedirectResponse(url="/docs") + + # Get all routers + router_instances = get_routers(routers) + + # Find protected paths + protected_paths = [] + for router in router_instances: + for route in router.routes: + if isinstance(route, APIRoute): + # Check if the route has auth_required decorator + if any(d.__name__ == 'auth_required' for d in route.dependencies): + protected_paths.append(route.path) + + # Include routers + for router in router_instances: + app.include_router(router) + + # Configure custom OpenAPI schema + def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=Config.TITLE, + version="1.0.0", + description=Config.DESCRIPTION, + routes=app.routes, + ) + + # Add security schemes + openapi_schema["components"] = {"securitySchemes": setup_security_schema()} + + # Configure security for each route + for route in app.routes: + if isinstance(route, APIRoute): + configure_route_security( + route.path, + route.methods.pop().lower(), + openapi_schema, + protected_paths + ) + + app.openapi_schema = openapi_schema + return app.openapi_schema + + app.openapi = custom_openapi + return app diff --git a/DockerApiServices/AllApiNeeds/middleware/__init__.py b/DockerApiServices/AllApiNeeds/middleware/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/middleware/__init__.py @@ -0,0 +1 @@ + diff --git a/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py new file mode 100644 index 0000000..b21b054 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/middleware/auth_middleware.py @@ -0,0 +1,179 @@ +""" +Authentication and Authorization middleware for FastAPI applications. + +This module provides authentication decorator for protecting endpoints +and a middleware for request timing measurements. +""" + +from time import perf_counter +from typing import Callable, Optional, Dict, Any, Tuple +from functools import wraps +from fastapi import HTTPException, Request, Response, status +from starlette.middleware.base import BaseHTTPMiddleware +from AllConfigs.Token.config import Auth +from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi + + +class MiddlewareModule: + """ + Module containing authentication and middleware functionality. + + This class provides: + - Token extraction and validation + - Authentication decorator for endpoints + - Request timing middleware + """ + + @staticmethod + def get_access_token(request: Request) -> Tuple[str, str]: + """ + Extract access token from request headers. + + Args: + request: FastAPI request object + + Returns: + Tuple[str, str]: A tuple containing (scheme, token) + + Raises: + HTTPExceptionApi: If token is missing or malformed + """ + auth_header = request.headers.get(Auth.ACCESS_TOKEN_TAG) + if not auth_header: + raise HTTPExceptionApi( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="No authorization header", + ) + + try: + scheme, token = auth_header.split() + if scheme.lower() != "bearer": + raise HTTPExceptionApi( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication scheme", + ) + return scheme, token + except ValueError: + raise HTTPExceptionApi( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token format" + ) + + @staticmethod + async def validate_token(token: str) -> Dict[str, Any]: + """ + Validate the authentication token. + + Args: + token: JWT token to validate + + Returns: + Dict[str, Any]: User data extracted from token + + Raises: + HTTPExceptionApi: If token is invalid + """ + try: + # TODO: Implement your token validation logic + # Example: + # return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) + return {"user_id": "test", "role": "user"} # Placeholder + except Exception as e: + raise HTTPExceptionApi( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Token validation failed: {str(e)}", + ) + + @classmethod + def auth_required(cls, func: Callable) -> Callable: + """ + Decorator for protecting FastAPI endpoints with authentication. + + Usage: + @router.get("/protected") + @MiddlewareModule.auth_required + async def protected_endpoint(request: Request): + user = request.state.user # Access authenticated user data + return {"message": "Protected content"} + + @router.get("/public") # No decorator = public endpoint + async def public_endpoint(): + return {"message": "Public content"} + + Args: + func: The FastAPI route handler function to protect + + Returns: + Callable: Wrapped function that checks authentication before execution + """ + + @wraps(func) + async def wrapper(request: Request, *args, **kwargs): + try: + # Get token from header + _, token = cls.get_access_token(request) + + # Validate token and get user data + token_data = await cls.validate_token(token) + + # Add user data to request state for use in endpoint + request.state.user = token_data + + # Call the original endpoint function + return await func(request, *args, **kwargs) + + except HTTPExceptionApi: + raise + except Exception as e: + raise HTTPExceptionApi( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Authentication failed: {str(e)}", + ) + + return wrapper + + class RequestTimingMiddleware(BaseHTTPMiddleware): + """ + Middleware for measuring and logging request timing. + Only handles timing, no authentication. + """ + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """ + Process each request through the middleware. + + Args: + request: FastAPI request object + call_next: Next middleware in the chain + + Returns: + Response: Processed response with timing headers + """ + start_time = perf_counter() + + # Process the request + response = await call_next(request) + + # Add timing information to response headers + self._add_timing_headers(response, start_time) + + return response + + @staticmethod + def _add_timing_headers(response: Response, start_time: float) -> None: + """ + Add request timing information to response headers. + + Args: + response: FastAPI response object + start_time: Time when request processing started + """ + end_time = perf_counter() + elapsed = (end_time - start_time) * 1000 # Convert to milliseconds + + response.headers.update( + { + "request-start": f"{start_time:.6f}", + "request-end": f"{end_time:.6f}", + "request-duration": f"{elapsed:.2f}ms", + } + ) diff --git a/DockerApiServices/AllApiNeeds/open_api_creator.py b/DockerApiServices/AllApiNeeds/open_api_creator.py new file mode 100644 index 0000000..e129066 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/open_api_creator.py @@ -0,0 +1,209 @@ +""" +OpenAPI Schema Creator Module + +This module provides functionality to create and customize OpenAPI documentation: +- Custom security schemes (Bearer Auth, API Key) +- Response schemas and examples +- Tag management and descriptions +- Error responses and validation +- Custom documentation extensions +""" + +from typing import Any, Dict, List, Optional, Set +from fastapi import FastAPI, APIRouter +from fastapi.openapi.utils import get_openapi +from AllConfigs.main import MainConfig as Config + + +class OpenAPISchemaCreator: + """ + OpenAPI schema creator and customizer for FastAPI applications. + """ + + def __init__(self, app: FastAPI): + """ + Initialize the OpenAPI schema creator. + + Args: + app: FastAPI application instance + """ + self.app = app + self.protected_paths: Set[str] = set() + self.tags_metadata = self._create_tags_metadata() + + @staticmethod + def _create_tags_metadata() -> List[Dict[str, str]]: + """ + Create metadata for API tags. + + Returns: + List[Dict[str, str]]: List of tag metadata + """ + return [ + { + "name": "Authentication", + "description": "Operations related to user authentication and authorization", + }, + { + "name": "Users", + "description": "User management and profile operations", + }, + # Add more tags as needed + ] + + def _create_security_schemes(self) -> Dict[str, Any]: + """ + Create security scheme definitions. + + Returns: + Dict[str, Any]: Security scheme configurations + """ + return { + "Bearer Auth": { + "type": "apiKey", + "in": "header", + "name": "evyos-session-key", + "description": "Enter: **'Bearer '**, where JWT is the access token", + }, + "API Key": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + "description": "API key for service authentication", + }, + } + + def _create_common_responses(self) -> Dict[str, Any]: + """ + Create common response schemas. + + Returns: + Dict[str, Any]: Common response configurations + """ + return { + "401": { + "description": "Unauthorized - Authentication failed or not provided", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": {"type": "string"}, + "error_code": {"type": "string"}, + }, + }, + "example": { + "detail": "Invalid authentication credentials", + "error_code": "INVALID_CREDENTIALS", + }, + } + }, + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/HTTPValidationError"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/HTTPValidationError"} + } + }, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "detail": {"type": "string"}, + "error_code": {"type": "string"}, + }, + }, + "example": { + "detail": "Internal server error occurred", + "error_code": "INTERNAL_ERROR", + }, + } + }, + }, + } + + def configure_route_security( + self, path: str, method: str, schema: Dict[str, Any] + ) -> None: + """ + Configure security requirements for a specific route. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + """ + if path not in Config.INSECURE_PATHS: + schema["paths"][path][method]["security"] = [ + {"Bearer Auth": []}, + {"API Key": []}, + ] + schema["paths"][path][method]["responses"].update( + self._create_common_responses() + ) + + def create_schema(self) -> Dict[str, Any]: + """ + Create the complete OpenAPI schema. + + Returns: + Dict[str, Any]: Complete OpenAPI schema + """ + openapi_schema = get_openapi( + title=Config.TITLE, + description=Config.DESCRIPTION, + version="1.0.0", + routes=self.app.routes, + tags=self.tags_metadata, + ) + + # Add security schemes + if "components" not in openapi_schema: + openapi_schema["components"] = {} + + openapi_schema["components"]["securitySchemes"] = self._create_security_schemes() + + # Configure route security and responses + for route in self.app.routes: + if isinstance(route, APIRoute) and route.include_in_schema: + path = str(route.path) + methods = [method.lower() for method in route.methods] + + for method in methods: + self.configure_route_security(path, method, openapi_schema) + + # Add custom documentation extensions + openapi_schema["x-documentation"] = { + "postman_collection": "/docs/postman", + "swagger_ui": "/docs", + "redoc": "/redoc", + } + + return openapi_schema + + +def create_openapi_schema(app: FastAPI) -> Dict[str, Any]: + """ + Create OpenAPI schema for a FastAPI application. + + Args: + app: FastAPI application instance + + Returns: + Dict[str, Any]: Complete OpenAPI schema + """ + creator = OpenAPISchemaCreator(app) + return creator.create_schema() \ No newline at end of file diff --git a/DockerApiServices/AllApiNeeds/routers/__init__.py b/DockerApiServices/AllApiNeeds/routers/__init__.py new file mode 100644 index 0000000..e958ef0 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/routers/__init__.py @@ -0,0 +1,5 @@ +from .base_router import test_route + +__all__ = [ + "test_route" +] diff --git a/DockerApiServices/AllApiNeeds/routers/base_router.py b/DockerApiServices/AllApiNeeds/routers/base_router.py new file mode 100644 index 0000000..3502d48 --- /dev/null +++ b/DockerApiServices/AllApiNeeds/routers/base_router.py @@ -0,0 +1,22 @@ +""" +Base router configuration and setup. +""" + +from fastapi import APIRouter, Request +from middleware.auth_middleware import MiddlewareModule + +# Create test router +test_route = APIRouter(prefix="/test", tags=["Test"]) + +@test_route.get("/health") +@MiddlewareModule.auth_required +async def health_check(request: Request): + return {"status": "healthy", "message": "Service is running"} + +@test_route.get("/ping") +async def ping_test(): + return {"ping": "pong", "service": "base-router"} + +# Initialize and include test routes +def init_test_routes(): + return test_route diff --git a/DockerApiServices/AuthServiceApi/Dockerfile b/DockerApiServices/AuthServiceApi/Dockerfile new file mode 100644 index 0000000..5da8984 --- /dev/null +++ b/DockerApiServices/AuthServiceApi/Dockerfile @@ -0,0 +1,30 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy all required directories +COPY DockerApiServices/AllApiNeeds /app/ +COPY ApiLibrary /app/ApiLibrary +COPY ApiValidations /app/ApiValidations +COPY AllConfigs /app/AllConfigs +COPY ErrorHandlers /app/ErrorHandlers +COPY Schemas /app/Schemas +COPY Services /app/Services + +# Install Python dependencies +COPY DockerApiServices/requirements.txt /app/ +RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt + +# Copy application code +COPY . . + +# Set Python path to include app directory +ENV PYTHONPATH=/app + +# Run the application using the configured uvicorn server +CMD ["python", "app.py"] diff --git a/DockerApiServices/AuthServiceApi/requirements.txt b/DockerApiServices/AuthServiceApi/requirements.txt new file mode 100644 index 0000000..562468b --- /dev/null +++ b/DockerApiServices/AuthServiceApi/requirements.txt @@ -0,0 +1,15 @@ +fastapi==0.104.1 +uvicorn==0.24.0.post1 +pydantic==2.10.5 +sqlalchemy==2.0.37 +psycopg2-binary==2.9.10 +python-dateutil==2.9.0.post0 +motor==3.3.2 +redis==5.2.1 +pytest==7.4.4 +pytest-asyncio==0.21.2 +pytest-cov==4.1.0 +coverage==7.6.10 +arrow==1.3.0 +redmail==0.6.0 +sqlalchemy-mixins==2.0.5 diff --git a/DockerApiServices/EventServiceApi/Dockerfile b/DockerApiServices/EventServiceApi/Dockerfile new file mode 100644 index 0000000..147a559 --- /dev/null +++ b/DockerApiServices/EventServiceApi/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY pyproject.toml . +RUN pip install poetry && \ + poetry config virtualenvs.create false && \ + poetry install --no-dev + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "EventServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/DockerApiServices/README.md b/DockerApiServices/README.md new file mode 100644 index 0000000..a6b91b3 --- /dev/null +++ b/DockerApiServices/README.md @@ -0,0 +1,59 @@ +# Docker Services Guide + +This repository contains multiple microservices that can be run using Docker Compose. + +## Quick Start (With Cache) +For regular development when dependencies haven't changed: +```bash +# Build and run Auth Service +docker compose -f ../docker-compose-services.yml up auth-service + +# Build and run Event Service +docker compose -f ../docker-compose-services.yml up event-service + +# Build and run Validation Service +docker compose -f ../docker-compose-services.yml up validation-service + +# Build and run all services +docker compose -f ../docker-compose-services.yml up +``` + +## Clean Build (No Cache) +Use these commands when changing Dockerfile or dependencies: +```bash +# Auth Service +docker compose -f ../docker-compose-services.yml build --no-cache auth-service && docker compose -f ../docker-compose-services.yml up auth-service + +# Event Service +docker compose -f ../docker-compose-services.yml build --no-cache event-service && docker compose -f ../docker-compose-services.yml up event-service + +# Validation Service +docker compose -f ../docker-compose-services.yml build --no-cache validation-service && docker compose -f ../docker-compose-services.yml up validation-service + +# All Services +docker compose -f ../docker-compose-services.yml build --no-cache && docker compose -f ../docker-compose-services.yml up +``` + +## Service Ports +- Auth Service: `http://localhost:8000` + - `/test/health` - Protected health check endpoint (requires authentication) + - `/test/ping` - Public ping endpoint +- Event Service: `http://localhost:8001` +- Validation Service: `http://localhost:8002` + +## Development Notes +- Use clean build (--no-cache) when: + - Changing Dockerfile + - Updating dependencies + - Experiencing caching issues +- Use regular build (with cache) when: + - Only changing application code + - For faster development iterations +- Run in detached mode: + ```bash + docker compose -f ../docker-compose-services.yml up -d auth-service + ``` +- Stop services: + ```bash + docker compose -f ../docker-compose-services.yml down + ``` diff --git a/DockerApiServices/ValidationServiceApi/Dockerfile b/DockerApiServices/ValidationServiceApi/Dockerfile new file mode 100644 index 0000000..e99b8ae --- /dev/null +++ b/DockerApiServices/ValidationServiceApi/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY pyproject.toml . +RUN pip install poetry && \ + poetry config virtualenvs.create false && \ + poetry install --no-dev + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "ValidationServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/DockerApiServices/pyproject.toml b/DockerApiServices/pyproject.toml new file mode 100644 index 0000000..228fa42 --- /dev/null +++ b/DockerApiServices/pyproject.toml @@ -0,0 +1,76 @@ +[tool.poetry] +name = "wag-management-api-services" +version = "0.1.1" +description = "WAG Management API Service" +authors = ["Karatay Berkay "] + +[tool.poetry.dependencies] +python = "^3.9" +# FastAPI and Web +fastapi = "^0.104.1" +uvicorn = "^0.24.0" +pydantic = "^2.5.2" + +# MongoDB +motor = "3.3.2" # Pinned version +pymongo = "4.5.0" # Pinned version to match motor + +# PostgreSQL +sqlalchemy = "^2.0.23" +sqlalchemy-mixins = "^2.0.5" +psycopg2-binary = "^2.9.9" + +# Redis +redis = "^5.0.1" +arrow = "^1.3.0" + +# Email +redmail = "^0.6.0" + +# Testing +pytest = "^7.4.3" +pytest-asyncio = "^0.21.1" +pytest-cov = "^4.1.0" + +# Utilities +python-dateutil = "^2.8.2" +typing-extensions = "^4.8.0" + +[tool.poetry.group.dev.dependencies] +black = "^23.11.0" +isort = "^5.12.0" +mypy = "^1.7.1" +flake8 = "^6.1.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 88 +target-version = ['py39'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +check_untyped_defs = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --cov=Services" +testpaths = [ + "Ztest", +] +python_files = ["test_*.py"] +asyncio_mode = "auto" diff --git a/DockerApiServices/requirements.txt b/DockerApiServices/requirements.txt new file mode 100644 index 0000000..3516646 --- /dev/null +++ b/DockerApiServices/requirements.txt @@ -0,0 +1,17 @@ +fastapi==0.104.1 +uvicorn==0.24.0.post1 +pydantic==2.10.5 +sqlalchemy==2.0.37 +psycopg2-binary==2.9.10 +python-dateutil==2.9.0.post0 +motor==3.3.2 +redis==5.2.1 +pytest==7.4.4 +pytest-asyncio==0.21.2 +pytest-cov==4.1.0 +coverage==7.6.10 +arrow==1.3.0 +redmail==0.6.0 +sqlalchemy-mixins==2.0.5 +prometheus-client==0.19.0 +prometheus-fastapi-instrumentator==6.1.0 diff --git a/DockerApiServices/steps.txt b/DockerApiServices/steps.txt index 5f88adf..024c74d 100644 --- a/DockerApiServices/steps.txt +++ b/DockerApiServices/steps.txt @@ -1,12 +1,29 @@ -What to do with services? +WAG Management API Microservices Setup -* +1. Authentication Service (Port 8000) + - User authentication and authorization + - JWT token management + - Role-based access control + - Uses PostgreSQL for user data -* +2. Event Service (Port 8001) + - Event processing and handling + - Message queue integration + - Real-time notifications + - Uses MongoDB for event storage -* +3. Validation Service (Port 8002) + - Request validation + - Data sanitization + - Schema validation + - Uses Redis for caching -* - -* +To run the services: +```bash +docker compose up --build +``` +Access services at: +- Auth Service: http://localhost:8000 +- Event Service: http://localhost:8001 +- Validation Service: http://localhost:8002 diff --git a/README.md b/README.md index f768e76..91643b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,84 @@ -# wag-managment-api-service-version-4 +# WAG Management API Service v4 -wag managment api service version 4 \ No newline at end of file +This service provides a comprehensive API for managing WAG (Wide Area Gateway) systems. It handles configuration, monitoring, and control operations for WAG devices in the network infrastructure. + +## Quick Start + +To run the tests using Docker Compose: + +```bash +docker compose -f docker-compose.test.yml up --build +``` + +## Project Structure + +### Core Services and Components + +- `Services/` - Core service implementations + - `PostgresDb/` - PostgreSQL database operations and models + - `MongoDb/` - MongoDB operations and document models + - `Redis/` - Redis caching and session management + - `Email/` - Email notification service + +- `ApiValidations/` - Request validation and data sanitization + - Input validation rules + - Data sanitization filters + - Schema validation middleware + +- `ApiLibrary/` - Common utilities and helper functions + - Shared functions and utilities + - Common constants and configurations + - Helper classes and decorators + +### Configuration and Settings + +- `AllConfigs/` - Configuration management + - Database configurations + - Service settings + - Environment-specific configs + +- `Schemas/` - Data models and schema definitions + - Request/Response models + - Database schemas + - API contract definitions + +### Docker and Deployment + +- `DockerApiServices/` - API service Docker configurations + - API service Dockerfile + - Service dependencies + +- `DockerStoreServices/` - Storage service Docker configurations + - Database service Dockerfiles + - Storage service dependencies + +### Error Handling and Events + +- `ErrorHandlers/` - Error handling and exception management + - Custom exceptions + - Error handlers + - Exception middleware + +- `ApiEvents/` - Event handling and processing + - Event listeners + - Event dispatchers + - Message queue handlers + +### Language and Testing + +- `LanguageModels/` - Localization and language support + - Language files + - Translation models + - i18n configurations + +- `Ztest/` - Test suite + - Unit tests + - Integration tests + - Test fixtures and utilities + +### Additional Components + +- `scripts/` - Utility scripts and tools + - Deployment scripts + - Database migrations + - Maintenance utilities diff --git a/Services/MongoDb/Models/action_models/base.py b/Services/MongoDb/Models/action_models/base.py index 566afde..d5b6df5 100644 --- a/Services/MongoDb/Models/action_models/base.py +++ b/Services/MongoDb/Models/action_models/base.py @@ -11,7 +11,7 @@ from ApiLibrary import system_arrow class PyObjectId(ObjectId): """Custom type for handling MongoDB ObjectId in Pydantic models.""" - + @classmethod def __get_pydantic_core_schema__( cls, @@ -21,17 +21,21 @@ class PyObjectId(ObjectId): """Define the core schema for PyObjectId.""" return core_schema.json_or_python_schema( json_schema=core_schema.str_schema(), - python_schema=core_schema.union_schema([ - core_schema.is_instance_schema(ObjectId), - core_schema.chain_schema([ - core_schema.str_schema(), - core_schema.no_info_plain_validator_function(cls.validate), - ]), - ]), + python_schema=core_schema.union_schema( + [ + core_schema.is_instance_schema(ObjectId), + core_schema.chain_schema( + [ + core_schema.str_schema(), + core_schema.no_info_plain_validator_function(cls.validate), + ] + ), + ] + ), serialization=core_schema.plain_serializer_function_ser_schema( lambda x: str(x), return_schema=core_schema.str_schema(), - when_used='json', + when_used="json", ), ) @@ -54,14 +58,14 @@ class PyObjectId(ObjectId): class MongoBaseModel(BaseModel): """Base model for all MongoDB documents.""" - + model_config = ConfigDict( arbitrary_types_allowed=True, json_encoders={ObjectId: str}, populate_by_name=True, from_attributes=True, validate_assignment=True, - extra='allow' + extra="allow", ) # Optional _id field that will be ignored in create operations @@ -69,11 +73,11 @@ class MongoBaseModel(BaseModel): def get_extra(self, field_name: str, default: Any = None) -> Any: """Safely get extra field value. - + Args: field_name: Name of the extra field to retrieve default: Default value to return if field doesn't exist - + Returns: Value of the extra field if it exists, otherwise the default value """ @@ -81,7 +85,7 @@ class MongoBaseModel(BaseModel): def as_dict(self) -> Dict[str, Any]: """Convert model to dictionary including all fields and extra fields. - + Returns: Dict containing all model fields and extra fields with proper type conversion """ @@ -94,18 +98,18 @@ class MongoDocument(MongoBaseModel): created_at: float = Field(default_factory=lambda: system_arrow.now().timestamp()) updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp()) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def prevent_protected_fields(cls, data: Any) -> Any: """Prevent user from setting protected fields like _id and timestamps.""" if isinstance(data, dict): # Remove protected fields from input - data.pop('_id', None) - data.pop('created_at', None) - data.pop('updated_at', None) - + data.pop("_id", None) + data.pop("created_at", None) + data.pop("updated_at", None) + # Set timestamps - data['created_at'] = system_arrow.now().timestamp() - data['updated_at'] = system_arrow.now().timestamp() - + data["created_at"] = system_arrow.now().timestamp() + data["updated_at"] = system_arrow.now().timestamp() + return data diff --git a/Services/MongoDb/Models/action_models/domain.py b/Services/MongoDb/Models/action_models/domain.py index 2f5e58c..cac9162 100644 --- a/Services/MongoDb/Models/action_models/domain.py +++ b/Services/MongoDb/Models/action_models/domain.py @@ -7,7 +7,7 @@ including domain history and access details. from datetime import datetime from typing import Any, Dict, List, Optional -from pydantic import Field, model_validator +from pydantic import BaseModel, Field, ConfigDict, model_validator from ApiLibrary import system_arrow from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument @@ -15,40 +15,38 @@ from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocu class DomainData(MongoBaseModel): """Model for domain data. - + Attributes: user_uu_id: Unique identifier of the user main_domain: Primary domain other_domains_list: List of additional domains extra_data: Additional domain-related data """ - + user_uu_id: str = Field(..., description="User's unique identifier") main_domain: str = Field(..., description="Primary domain") other_domains_list: List[str] = Field( - default_factory=list, - description="List of additional domains" + default_factory=list, description="List of additional domains" ) extra_data: Optional[Dict[str, Any]] = Field( default_factory=dict, alias="extraData", - description="Additional domain-related data" + description="Additional domain-related data", ) - class Config: - from_attributes = True - populate_by_name = True - validate_assignment = True + model_config = ConfigDict( + from_attributes=True, populate_by_name=True, validate_assignment=True + ) class DomainDocument(MongoDocument): """Model for domain-related documents.""" - + data: DomainData = Field(..., description="Domain data") - + def update_main_domain(self, new_domain: str) -> None: """Update the main domain and move current to history. - + Args: new_domain: New main domain to set """ @@ -60,19 +58,19 @@ class DomainDocument(MongoDocument): class DomainDocumentCreate(MongoDocument): """Model for creating new domain documents.""" + data: DomainData = Field(..., description="Initial domain data") - class Config: - from_attributes = True - populate_by_name = True - validate_assignment = True + model_config = ConfigDict( + from_attributes=True, populate_by_name=True, validate_assignment=True + ) class DomainDocumentUpdate(MongoDocument): """Model for updating existing domain documents.""" + data: DomainData = Field(..., description="Updated domain data") - class Config: - from_attributes = True - populate_by_name = True - validate_assignment = True + model_config = ConfigDict( + from_attributes=True, populate_by_name=True, validate_assignment=True + ) diff --git a/Services/MongoDb/Models/action_models/password.py b/Services/MongoDb/Models/action_models/password.py index 5f7609c..e08707a 100644 --- a/Services/MongoDb/Models/action_models/password.py +++ b/Services/MongoDb/Models/action_models/password.py @@ -15,7 +15,7 @@ from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocu class PasswordHistoryDetail(MongoBaseModel): """Model for password history details.""" - + timestamp: datetime ip_address: Optional[str] = Field(None, alias="ipAddress") user_agent: Optional[str] = Field(None, alias="userAgent") @@ -24,27 +24,26 @@ class PasswordHistoryDetail(MongoBaseModel): class PasswordHistoryData(MongoBaseModel): """Model for password history data.""" - + password_history: List[str] = Field([], alias="passwordHistory") access_history_detail: Dict[str, PasswordHistoryDetail] = Field( - default_factory=dict, - alias="accessHistoryDetail" + default_factory=dict, alias="accessHistoryDetail" ) class PasswordDocument(MongoDocument): """Model for password-related documents.""" - + data: PasswordHistoryData class PasswordDocumentCreate(MongoBaseModel): """Model for creating new password documents.""" - + data: PasswordHistoryData = Field(..., description="Initial password data") class PasswordDocumentUpdate(MongoBaseModel): """Model for updating existing password documents.""" - + data: PasswordHistoryData diff --git a/Services/MongoDb/Models/actions.py b/Services/MongoDb/Models/actions.py index b7be0c1..51ea587 100644 --- a/Services/MongoDb/Models/actions.py +++ b/Services/MongoDb/Models/actions.py @@ -30,23 +30,19 @@ class MongoActions( MongoInsertMixin, MongoFindMixin, MongoDeleteMixin, - MongoAggregateMixin + MongoAggregateMixin, ): """Main MongoDB actions class that inherits all CRUD operation mixins. - + This class provides a unified interface for all MongoDB operations while managing collections based on company UUID and storage reason. """ def __init__( - self, - client: MongoClient, - database: str, - company_uuid: str, - storage_reason: str + self, client: MongoClient, database: str, company_uuid: str, storage_reason: str ): """Initialize MongoDB actions with client and collection info. - + Args: client: MongoDB client database: Database name to use @@ -62,7 +58,7 @@ class MongoActions( def use_collection(self, storage_reason: str) -> None: """Switch to a different collection. - + Args: storage_reason: New storage reason for collection naming """ @@ -82,7 +78,9 @@ class MongoActions( """Insert multiple documents.""" return super().insert_many(self.collection, documents) - def find_one(self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None): + def find_one( + self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None + ): """Find a single document.""" return super().find_one(self.collection, filter_query, projection) @@ -96,12 +94,7 @@ class MongoActions( ): """Find multiple documents.""" return super().find_many( - self.collection, - filter_query, - projection, - sort, - limit, - skip + self.collection, filter_query, projection, sort, limit, skip ) def update_one( @@ -111,12 +104,7 @@ class MongoActions( upsert: bool = False, ): """Update a single document.""" - return super().update_one( - self.collection, - filter_query, - update_data, - upsert - ) + return super().update_one(self.collection, filter_query, update_data, upsert) def update_many( self, @@ -125,12 +113,7 @@ class MongoActions( upsert: bool = False, ): """Update multiple documents.""" - return super().update_many( - self.collection, - filter_query, - update_data, - upsert - ) + return super().update_many(self.collection, filter_query, update_data, upsert) def delete_one(self, filter_query: Dict[str, Any]): """Delete a single document.""" diff --git a/Services/MongoDb/Models/exception_handlers.py b/Services/MongoDb/Models/exception_handlers.py index a0dd469..031a60c 100644 --- a/Services/MongoDb/Models/exception_handlers.py +++ b/Services/MongoDb/Models/exception_handlers.py @@ -19,27 +19,27 @@ from Services.MongoDb.Models.exceptions import ( PasswordHistoryError, PasswordReuseError, PasswordHistoryLimitError, - InvalidPasswordDetailError + InvalidPasswordDetailError, ) from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi def handle_mongo_errors(func: Callable) -> Callable: """Decorator to handle MongoDB operation errors. - + Args: func: Function to wrap with error handling - + Returns: Wrapped function with error handling """ + async def wrapper(*args, **kwargs) -> Any: try: return await func(*args, **kwargs) except ConnectionFailure as e: raise MongoConnectionError( - message=str(e), - details={"error_type": "connection_failure"} + message=str(e), details={"error_type": "connection_failure"} ).to_http_exception() except DuplicateKeyError as e: raise MongoDuplicateKeyError( @@ -48,143 +48,138 @@ def handle_mongo_errors(func: Callable) -> Callable: ).to_http_exception() except PyMongoError as e: raise MongoBaseException( - message=str(e), - details={"error_type": "pymongo_error"} + message=str(e), details={"error_type": "pymongo_error"} ).to_http_exception() except Exception as e: raise HTTPExceptionApi( lang="en", error_code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + return wrapper async def mongo_base_exception_handler( - request: Request, - exc: MongoBaseException + request: Request, exc: MongoBaseException ) -> JSONResponse: """Handle base MongoDB exceptions. - + Args: request: FastAPI request exc: MongoDB base exception - + Returns: JSON response with error details """ return JSONResponse( - status_code=exc.status_code, - content={"error": exc.to_http_exception()} + status_code=exc.status_code, content={"error": exc.to_http_exception()} ) async def mongo_connection_error_handler( - request: Request, - exc: MongoConnectionError + request: Request, exc: MongoConnectionError ) -> JSONResponse: """Handle MongoDB connection errors. - + Args: request: FastAPI request exc: MongoDB connection error - + Returns: JSON response with connection error details """ return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - content={"error": exc.to_http_exception()} + content={"error": exc.to_http_exception()}, ) async def mongo_document_not_found_handler( - request: Request, - exc: MongoDocumentNotFoundError + request: Request, exc: MongoDocumentNotFoundError ) -> JSONResponse: """Handle document not found errors. - + Args: request: FastAPI request exc: Document not found error - + Returns: JSON response with not found error details """ return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, - content={"error": exc.to_http_exception()} + content={"error": exc.to_http_exception()}, ) async def mongo_validation_error_handler( - request: Request, - exc: MongoValidationError + request: Request, exc: MongoValidationError ) -> JSONResponse: """Handle validation errors. - + Args: request: FastAPI request exc: Validation error - + Returns: JSON response with validation error details """ return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - content={"error": exc.to_http_exception()} + content={"error": exc.to_http_exception()}, ) async def mongo_duplicate_key_error_handler( - request: Request, - exc: MongoDuplicateKeyError + request: Request, exc: MongoDuplicateKeyError ) -> JSONResponse: """Handle duplicate key errors. - + Args: request: FastAPI request exc: Duplicate key error - + Returns: JSON response with duplicate key error details """ return JSONResponse( - status_code=status.HTTP_409_CONFLICT, - content={"error": exc.to_http_exception()} + status_code=status.HTTP_409_CONFLICT, content={"error": exc.to_http_exception()} ) async def password_history_error_handler( - request: Request, - exc: PasswordHistoryError + request: Request, exc: PasswordHistoryError ) -> JSONResponse: """Handle password history errors. - + Args: request: FastAPI request exc: Password history error - + Returns: JSON response with password history error details """ return JSONResponse( - status_code=exc.status_code, - content={"error": exc.to_http_exception()} + status_code=exc.status_code, content={"error": exc.to_http_exception()} ) def register_exception_handlers(app: Any) -> None: """Register all MongoDB exception handlers with FastAPI app. - + Args: app: FastAPI application instance """ app.add_exception_handler(MongoBaseException, mongo_base_exception_handler) app.add_exception_handler(MongoConnectionError, mongo_connection_error_handler) - app.add_exception_handler(MongoDocumentNotFoundError, mongo_document_not_found_handler) + app.add_exception_handler( + MongoDocumentNotFoundError, mongo_document_not_found_handler + ) app.add_exception_handler(MongoValidationError, mongo_validation_error_handler) app.add_exception_handler(MongoDuplicateKeyError, mongo_duplicate_key_error_handler) app.add_exception_handler(PasswordHistoryError, password_history_error_handler) app.add_exception_handler(PasswordReuseError, password_history_error_handler) app.add_exception_handler(PasswordHistoryLimitError, password_history_error_handler) - app.add_exception_handler(InvalidPasswordDetailError, password_history_error_handler) \ No newline at end of file + app.add_exception_handler( + InvalidPasswordDetailError, password_history_error_handler + ) diff --git a/Services/MongoDb/Models/exceptions.py b/Services/MongoDb/Models/exceptions.py index 773d868..94e3720 100644 --- a/Services/MongoDb/Models/exceptions.py +++ b/Services/MongoDb/Models/exceptions.py @@ -12,12 +12,12 @@ from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi class MongoBaseException(Exception): """Base exception for MongoDB-related errors.""" - + def __init__( self, message: str, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, - details: Optional[Dict[str, Any]] = None + details: Optional[Dict[str, Any]] = None, ): self.message = message self.status_code = status_code @@ -34,128 +34,110 @@ class MongoBaseException(Exception): class MongoConnectionError(MongoBaseException): """Raised when there's an error connecting to MongoDB.""" - + def __init__( self, message: str = "Failed to connect to MongoDB", - details: Optional[Dict[str, Any]] = None + details: Optional[Dict[str, Any]] = None, ): super().__init__( message=message, status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - details=details + details=details, ) class MongoDocumentNotFoundError(MongoBaseException): """Raised when a document is not found in MongoDB.""" - + def __init__( self, collection: str, filter_query: Dict[str, Any], - message: Optional[str] = None + message: Optional[str] = None, ): message = message or f"Document not found in collection '{collection}'" super().__init__( message=message, status_code=status.HTTP_404_NOT_FOUND, - details={ - "collection": collection, - "filter": filter_query - } + details={"collection": collection, "filter": filter_query}, ) class MongoValidationError(MongoBaseException): """Raised when document validation fails.""" - - def __init__( - self, - message: str, - field_errors: Optional[Dict[str, str]] = None - ): + + def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None): super().__init__( message=message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - details={"field_errors": field_errors or {}} + details={"field_errors": field_errors or {}}, ) class MongoDuplicateKeyError(MongoBaseException): """Raised when trying to insert a document with a duplicate key.""" - + def __init__( self, collection: str, key_pattern: Dict[str, Any], - message: Optional[str] = None + message: Optional[str] = None, ): message = message or f"Duplicate key error in collection '{collection}'" super().__init__( message=message, status_code=status.HTTP_409_CONFLICT, - details={ - "collection": collection, - "key_pattern": key_pattern - } + details={"collection": collection, "key_pattern": key_pattern}, ) class PasswordHistoryError(MongoBaseException): """Base exception for password history-related errors.""" - + def __init__( self, message: str, status_code: int = status.HTTP_400_BAD_REQUEST, - details: Optional[Dict[str, Any]] = None + details: Optional[Dict[str, Any]] = None, ): super().__init__(message, status_code, details) class PasswordReuseError(PasswordHistoryError): """Raised when attempting to reuse a recent password.""" - + def __init__( self, message: str = "Password was used recently", - history_limit: Optional[int] = None + history_limit: Optional[int] = None, ): details = {"history_limit": history_limit} if history_limit else None super().__init__( message=message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - details=details + details=details, ) class PasswordHistoryLimitError(PasswordHistoryError): """Raised when password history limit is reached.""" - - def __init__( - self, - limit: int, - message: Optional[str] = None - ): + + def __init__(self, limit: int, message: Optional[str] = None): message = message or f"Password history limit of {limit} reached" super().__init__( message=message, status_code=status.HTTP_409_CONFLICT, - details={"limit": limit} + details={"limit": limit}, ) class InvalidPasswordDetailError(PasswordHistoryError): """Raised when password history detail is invalid.""" - - def __init__( - self, - message: str, - field_errors: Optional[Dict[str, str]] = None - ): + + def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None): super().__init__( message=message, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - details={"field_errors": field_errors or {}} + details={"field_errors": field_errors or {}}, ) diff --git a/Services/MongoDb/Models/mixins.py b/Services/MongoDb/Models/mixins.py index 7a67d9a..c2d2177 100644 --- a/Services/MongoDb/Models/mixins.py +++ b/Services/MongoDb/Models/mixins.py @@ -34,25 +34,16 @@ def handle_mongo_errors(func): try: return func(*args, **kwargs) except ConnectionFailure: - raise HTTPExceptionApi( - error_code="HTTP_503_SERVICE_UNAVAILABLE", - lang="en" - ) + raise HTTPExceptionApi(error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en") except ServerSelectionTimeoutError: - raise HTTPExceptionApi( - error_code="HTTP_504_GATEWAY_TIMEOUT", - lang="en" - ) + raise HTTPExceptionApi(error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en") except OperationFailure as e: - raise HTTPExceptionApi( - error_code="HTTP_400_BAD_REQUEST", - lang="en" - ) + raise HTTPExceptionApi(error_code="HTTP_400_BAD_REQUEST", lang="en") except PyMongoError as e: raise HTTPExceptionApi( - error_code="HTTP_500_INTERNAL_SERVER_ERROR", - lang="en" + error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en" ) + return wrapper diff --git a/Services/MongoDb/database.py b/Services/MongoDb/database.py index b0f5089..3d5f88d 100644 --- a/Services/MongoDb/database.py +++ b/Services/MongoDb/database.py @@ -42,11 +42,15 @@ class MongoFindMixin: class MongoUpdateMixin: """Mixin for MongoDB update operations.""" - def update_one(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult: + def update_one( + self, filter_query: Dict[str, Any], update: Dict[str, Any] + ) -> UpdateResult: """Update a single document.""" return self.collection.update_one(filter_query, update) - def update_many(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult: + def update_many( + self, filter_query: Dict[str, Any], update: Dict[str, Any] + ) -> UpdateResult: """Update multiple documents.""" return self.collection.update_many(filter_query, update) @@ -100,7 +104,7 @@ class MongoDBHandler( ) else: connection_url = f"mongodb://{MongoConfig.HOST}:{MongoConfig.PORT}" - + # Build connection options connection_kwargs = { "host": connection_url, @@ -110,11 +114,11 @@ class MongoDBHandler( "waitQueueTimeoutMS": 2000, # How long a thread will wait for a connection "serverSelectionTimeoutMS": 5000, # How long to wait for server selection } - + self._client = MongoClient(**connection_kwargs) - + # Test connection - self._client.admin.command('ping') + self._client.admin.command("ping") def close(self): """Close MongoDB connection.""" diff --git a/Services/PostgresDb/database.py b/Services/PostgresDb/database.py index 6f76499..0fb604d 100644 --- a/Services/PostgresDb/database.py +++ b/Services/PostgresDb/database.py @@ -3,8 +3,7 @@ from functools import lru_cache from typing import Generator from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, scoped_session, Session +from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session from AllConfigs.SqlDatabase.configs import WagDatabase diff --git a/Ztest/fixtures.py b/Ztest/fixtures.py new file mode 100644 index 0000000..4968857 --- /dev/null +++ b/Ztest/fixtures.py @@ -0,0 +1,13 @@ +"""Test fixtures and models.""" + +from sqlalchemy import Column, String + +from Services.PostgresDb.Models.mixins import CrudCollection + + +class TestUser(CrudCollection): + """Test user model for PostgreSQL tests.""" + + __tablename__ = "test_users" + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) diff --git a/Ztest/models.py b/Ztest/models.py new file mode 100644 index 0000000..6cbe94a --- /dev/null +++ b/Ztest/models.py @@ -0,0 +1,13 @@ +"""Test models.""" + +from sqlalchemy import Column, String + +from Services.PostgresDb.Models.mixins import CrudCollection + + +class UserModel(CrudCollection): + """User model for PostgreSQL tests.""" + + __tablename__ = "test_users" + username = Column(String, unique=True, index=True) + email = Column(String, unique=True, index=True) diff --git a/Ztest/test_mongo.py b/Ztest/test_mongo.py index 71c733c..57ab629 100644 --- a/Ztest/test_mongo.py +++ b/Ztest/test_mongo.py @@ -7,7 +7,7 @@ from Services.MongoDb.Models.actions import MongoActions from Services.MongoDb.Models.action_models.domain import ( DomainData, DomainDocumentCreate, - DomainDocumentUpdate + DomainDocumentUpdate, ) from AllConfigs.NoSqlDatabase.configs import MongoConfig @@ -17,7 +17,7 @@ def mongo_client(): """Create MongoDB test client.""" # Connect using configured credentials client = MongoClient(MongoConfig.URL) - client.admin.command('ping') # Test connection + client.admin.command("ping") # Test connection yield client client.close() @@ -27,12 +27,12 @@ def mongo_actions(mongo_client): """Create MongoActions instance for testing.""" if not mongo_client: pytest.skip("MongoDB connection not available") - + actions = MongoActions( client=mongo_client, database=MongoConfig.DATABASE_NAME, company_uuid="test_company", - storage_reason="domains" + storage_reason="domains", ) yield actions try: @@ -45,37 +45,36 @@ def mongo_actions(mongo_client): def test_mongo_crud_operations(mongo_actions: MongoActions): """Test CRUD operations with MongoActions.""" - + # Create test data domain_data = DomainData( user_uu_id="test_user", main_domain="example.com", - other_domains_list=["old.com"] + other_domains_list=["old.com"], ) create_doc = DomainDocumentCreate(data=domain_data) - + # Test create result = mongo_actions.insert_one(create_doc.model_dump()) assert result.inserted_id is not None - + # Test read doc = mongo_actions.find_one({"data.main_domain": "example.com"}) assert doc is not None assert doc["data"]["main_domain"] == "example.com" - + # Test update update_data = DomainData( user_uu_id="test_user", main_domain="new.com", - other_domains_list=["example.com", "old.com"] + other_domains_list=["example.com", "old.com"], ) update_doc = DomainDocumentUpdate(data=update_data) result = mongo_actions.update_one( - {"_id": doc["_id"]}, - {"$set": update_doc.model_dump()} + {"_id": doc["_id"]}, {"$set": update_doc.model_dump()} ) assert result.modified_count == 1 - + # Test delete result = mongo_actions.delete_one({"_id": doc["_id"]}) assert result.deleted_count == 1 @@ -83,23 +82,18 @@ def test_mongo_crud_operations(mongo_actions: MongoActions): def test_mongo_aggregate(mongo_actions: MongoActions): """Test aggregate operations with MongoActions.""" - + # Insert test documents docs = [ DomainDocumentCreate( - data=DomainData( - user_uu_id="user1", - main_domain=f"domain{i}.com" - ) + data=DomainData(user_uu_id="user1", main_domain=f"domain{i}.com") ).model_dump() for i in range(3) ] mongo_actions.insert_many(docs) - + # Test aggregation - pipeline = [ - {"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}} - ] + pipeline = [{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}] result = mongo_actions.aggregate(pipeline) result_list = list(result) assert len(result_list) == 1 @@ -107,4 +101,4 @@ def test_mongo_aggregate(mongo_actions: MongoActions): if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file + pytest.main([__file__, "-v"]) diff --git a/Ztest/test_postgres.py b/Ztest/test_postgres.py index b6fa7c0..8be5491 100644 --- a/Ztest/test_postgres.py +++ b/Ztest/test_postgres.py @@ -1,21 +1,12 @@ """Test PostgreSQL database operations.""" import pytest -from sqlalchemy import Column, String, create_engine, text +from sqlalchemy import create_engine, text from sqlalchemy.orm import Session from Services.PostgresDb.database import Base, get_db -from Services.PostgresDb.Models.mixins import CrudCollection from AllConfigs.SqlDatabase.configs import WagDatabase - - -class TestUser(CrudCollection): - """Test user model for PostgreSQL tests.""" - - __tablename__ = "test_users" - - username = Column(String, unique=True, index=True) - email = Column(String, unique=True, index=True) +from Ztest.models import UserModel @pytest.fixture(scope="session") @@ -24,12 +15,12 @@ def db_engine(): # Use the same database URL but with test database test_db_url = WagDatabase.DATABASE_URL engine = create_engine(test_db_url, echo=True) - + # Create all tables Base.metadata.create_all(bind=engine) - + yield engine - + # Drop all tables after tests Base.metadata.drop_all(bind=engine) @@ -40,9 +31,9 @@ def db_session(db_engine): connection = db_engine.connect() transaction = connection.begin() session = Session(bind=connection) - + yield session - + # Rollback the transaction after each test transaction.rollback() connection.close() @@ -52,12 +43,12 @@ def db_session(db_engine): def test_create_user(db_session): """Test creating a user in the database.""" # Create user using CrudMixin methods - user = TestUser(username="testuser", email="test@example.com") + user = UserModel(username="testuser", email="test@example.com") db_session.add(user) db_session.commit() - + # Verify user was created - db_user = db_session.query(TestUser).filter_by(username="testuser").first() + db_user = db_session.query(UserModel).filter_by(username="testuser").first() assert db_user is not None assert db_user.email == "test@example.com" assert db_user.created_at is not None @@ -68,16 +59,16 @@ def test_create_user(db_session): def test_update_user(db_session): """Test updating a user in the database.""" # Create user - user = TestUser(username="updateuser", email="update@example.com") + user = UserModel(username="updateuser", email="update@example.com") db_session.add(user) db_session.commit() - + # Update user using CrudMixin methods - user.update(session=db_session, email="newemail@example.com") + user.update(db=db_session, email="newemail@example.com") db_session.commit() - + # Verify update - updated_user = db_session.query(TestUser).filter_by(username="updateuser").first() + updated_user = db_session.query(UserModel).filter_by(username="updateuser").first() assert updated_user.email == "newemail@example.com" assert updated_user.updated_at is not None @@ -85,16 +76,16 @@ def test_update_user(db_session): def test_soft_delete_user(db_session): """Test soft deleting a user from the database.""" # Create user - user = TestUser(username="deleteuser", email="delete@example.com") + user = UserModel(username="deleteuser", email="delete@example.com") db_session.add(user) db_session.commit() - + # Soft delete by updating deleted and active flags - user.update(session=db_session, deleted=True, active=False) + user.update(db=db_session, deleted=True, active=False) db_session.commit() - + # Verify soft deletion - deleted_user = db_session.query(TestUser).filter_by(username="deleteuser").first() + deleted_user = db_session.query(UserModel).filter_by(username="deleteuser").first() assert deleted_user is not None assert deleted_user.deleted assert not deleted_user.active @@ -105,4 +96,4 @@ def test_get_db_context_manager(): with get_db() as session: # Verify we can execute a simple query result = session.execute(text("SELECT 1")) - assert result.scalar() == 1 \ No newline at end of file + assert result.scalar() == 1 diff --git a/docker-compose-services.yml b/docker-compose-services.yml new file mode 100644 index 0000000..52f9354 --- /dev/null +++ b/docker-compose-services.yml @@ -0,0 +1,22 @@ +services: + auth-service: + build: + context: . + dockerfile: DockerApiServices/AuthServiceApi/Dockerfile + ports: + - "41575:41575" + + event-service: + build: + context: . + dockerfile: DockerApiServices/EventServiceApi/Dockerfile + ports: + - "8001:8000" + + validation-service: + build: + context: . + dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile + ports: + - "8002:8000" +# and lets try to implement potry again in the dockerfile now we now that it is about copy of files