commit aad8c7f4151bcf6c048cfa21dfc209bbc6b6f03f Author: Andros Fenollosa Date: Fri Jan 24 09:39:17 2025 +0100 First commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..99ade3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.12-slim AS base-core-app + +# Prevents Python from writing pyc files to disc (equivalent to python -B option) +ENV PYTHONDONTWRITEBYTECODE=1 +# Prevents Python from buffering stdout and stderr (equivalent to python -u option) +ENV PYTHONUNBUFFERED=1 +# Sets python path to be able to import modules +ENV PYTHONPATH=":/usr/src/app" +# Sets the default python breakpoint to use pudb +ENV PYTHONBREAKPOINT="pudb.set_trace" + +# set work directory +WORKDIR /usr/src/app + +# set time +RUN ln -fs /usr/share/zoneinfo/Europe/Madrid /etc/localtime +RUN dpkg-reconfigure -f noninteractive tzdata + +# install software +RUN apt update && apt upgrade -y +RUN apt install -y build-essential python3-dev libpq-dev python3-pip sqlite3 + +# install dependencies +RUN pip3 install --upgrade pip + +COPY ./requirements.txt . +RUN pip3 install -r requirements.txt + +# api: fastapi +COPY ./src/infra/api/fastapi/requirements.txt requirements_fastapi.txt +RUN pip3 install -r requirements_fastapi.txt + +# api: flask +COPY ./src/infra/api/flask/requirements.txt requirements_flask.txt +RUN pip3 install -r requirements_flask.txt diff --git a/Dockerfile-test b/Dockerfile-test new file mode 100644 index 0000000..0ffaf86 --- /dev/null +++ b/Dockerfile-test @@ -0,0 +1,10 @@ +FROM base-core-app + +# Install requirements +COPY test/requirements.txt . +RUN pip3 install -r requirements.txt + +# Entrypoint +#ENTRYPOINT ["pytest", "test/infra/api/fastapi/test_client_fastapi.py", "-k", "_"] +#ENTRYPOINT ["pytest", "test/infra/authentication/keycloak/test_users_keycloak.py", "-k", "_"] +ENTRYPOINT ["pytest", "test/infra/api/fastapi/test_client_fastapi.py", "test/infra/api/fastapi/test_roles_fastapi.py", "test/infra/api/fastapi/test_users_fastapi.py", "test/infra/authentication/keycloak/test_users_keycloak.py", "test/infra/authentication/keycloak/test_app_keycloak.py", "test/infra/authentication/keycloak/test_connection_keycloak.py", "test/infra/authentication/keycloak/test_client_keycloak.py"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73a98f8 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help +help: + @perl -nle'print $& if m{^[a-zA-Z_.-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' + +build: ## Build base image + docker compose pull + docker build -t base-core-app . + +network: ## Create network + docker network create -d bridge core_app + +tests: ## Run tests + docker compose build test; \ + docker compose up test + +proxy: ## Run Proxy + docker compose stop nginx; \ + docker compose up nginx --build -d --remove-orphans + +proxy.logs: ## Show Proxy logs + docker compose logs -f nginx-proxy + +api.fastapi.run: ## Run API + docker compose stop api-fastapi; \ + docker compose up api-fastapi --build -d + +api.fastapi.logs: ## Show API logs + docker compose logs -f api-fastapi + +api.flask.run: ## Run API + docker compose stop api-flask; \ + docker compose up api-flask --build -d + +api.flask.logs: ## Show API logs + docker compose logs -f api-flask + +mail: ## Run Mail server (MailHog) + docker compose stop mailhog; \ + docker compose up mailhog --build -d diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad3e5c8 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# API template with Clean Architecture + +This is a template for building APIs with Clean Architecture. It contains two examples of APIs built with FastAPI and Flask. + +- `src` contains the source code. + + +## Prepare + +```bash +make build network +``` + +## Run FastAPI + +```bash +make api.fastapi.run +``` + +Now, you can test the API with the following command: + +```bash +curl -X 'GET' 'http://localhost:8000/api/v1/documents/?appName=app_test&clientId=client_test' -H 'accept: application/json' | jq +``` + +## Run Flask + +```bash +make api.flask.run +``` + +Now, you can test the API with the following command: + +```bash +curl -X 'GET' 'http://localhost:5000/api/v1/documents/?appName=app_test&clientId=client_test' -H 'accept: application/json' | jq +``` diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..9312c3d --- /dev/null +++ b/compose.yaml @@ -0,0 +1,54 @@ +services: + + test: + build: + dockerfile: ./Dockerfile-test + env_file: .env + volumes: + - .:/usr/src/app + networks: + - core_app + + + api-fastapi: + build: src/infra/api/fastapi/ + env_file: .env + ports: + - "8000:8000" + volumes: + - ./src:/usr/src/app/src + - ./src/infra/api/fastapi/:/usr/src/app + networks: + - core_app + + api-flask: + build: src/infra/api/flask/ + env_file: .env + ports: + - "5000:5000" + volumes: + - ./src:/usr/src/app/src + - ./src/infra/api/flask/:/usr/src/app + networks: + - core_app + + nginx: + image: nginx:latest + container_name: keycloak.internal + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + networks: + - core_app + + mailhog: + image: mailhog/mailhog + ports: + - "8025:8025" + - "1025:1025" + + +networks: + core_app: + external: true diff --git a/pre-commit-config.yaml b/pre-commit-config.yaml new file mode 100644 index 0000000..aa324a3 --- /dev/null +++ b/pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.4 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a07e975 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Faker diff --git a/src/core/decorators.py b/src/core/decorators.py new file mode 100644 index 0000000..abf3548 --- /dev/null +++ b/src/core/decorators.py @@ -0,0 +1,33 @@ +from functools import wraps + +from pydantic import ValidationError + +from src.core.entity.ResponseTypes import ResponseTypes + + +def check_params(Model): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + params = kwargs.get('params', None) + if params is not None: + try: + Model.model_validate(params, strict=True) + except ValidationError as e: + errors = [] + for error in e.errors(): + errors.append( + { + 'field': error['loc'][0], + 'message': error['msg'], + } + ) + return { + 'type': ResponseTypes.PARAMETERS_ERROR, + 'errors': errors, + } + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/src/core/entity/Documents.py b/src/core/entity/Documents.py new file mode 100644 index 0000000..876c487 --- /dev/null +++ b/src/core/entity/Documents.py @@ -0,0 +1,5 @@ +from enum import Enum + +class DocumentsType(Enum): + DIRECTORY = 'directory' + FILE = 'file' diff --git a/src/core/entity/ResponseTypes.py b/src/core/entity/ResponseTypes.py new file mode 100644 index 0000000..04f5b5c --- /dev/null +++ b/src/core/entity/ResponseTypes.py @@ -0,0 +1,8 @@ +class ResponseTypes: + SUCCESS = 'Success' # The process ended correctly + PARAMETERS_ERROR = 'ParametersError' # Missing or invalid parameters + ACCESS_DENIED = 'AccessDenied' # The user does not have permission to access the resource + RESOURCE_ERROR = 'ResourceError' # The process ended correctly but the resource is not available (DB, file, etc) + SYSTEM_ERROR = ( + 'SystemError' # The process ended with an error. Python error + ) diff --git a/src/core/use_case/__init__.py b/src/core/use_case/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/use_case/documents_use_case.py b/src/core/use_case/documents_use_case.py new file mode 100644 index 0000000..11df88e --- /dev/null +++ b/src/core/use_case/documents_use_case.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel, StrictStr + +from src.core.decorators import check_params + + +class DocumentListModel(BaseModel): + app_name: StrictStr + client_id: StrictStr + + +@check_params(DocumentListModel) +def documents_list(storage, params) -> dict: + """ + List documents + + :param storage: Storage + :return: dict + """ + return storage.list_files(params) diff --git a/src/infra/api/fastapi/Dockerfile b/src/infra/api/fastapi/Dockerfile new file mode 100644 index 0000000..507f761 --- /dev/null +++ b/src/infra/api/fastapi/Dockerfile @@ -0,0 +1,4 @@ +FROM base-core-app + +# launcher +ENTRYPOINT fastapi dev main.py --host 0.0.0.0 diff --git a/src/infra/api/fastapi/auth.py b/src/infra/api/fastapi/auth.py new file mode 100644 index 0000000..f2c15f6 --- /dev/null +++ b/src/infra/api/fastapi/auth.py @@ -0,0 +1,135 @@ +import os +from enum import Enum +from functools import wraps + +from core_auth.core.entity.ResponseTypes import ResponseTypes +from src.core.use_case import ( + clients_use_case, + users_use_case, +) +from src.infra.api.fastapi.tools import ( + get_auth, +) + + +class Permission(Enum): + CAN_READ_USERS = 'can_read_users' + CAN_EDIT_USER_DATA = 'can_edit_user_data' + CAN_EDIT_USER_PASSWORD = 'can_edit_user_password' + CAN_DELETE_USERS = 'can_delete_users' + + +def get_access_token(access_token: str) -> dict: + auth = get_auth() + try: + token_introspect = auth.token_introspect( + params={ + 'access_token': access_token, + 'app_name': os.environ.get('APP_NAME'), + } + ) + return { + 'type': ResponseTypes.SUCCESS, + 'data': token_introspect['data'], + } + except Exception as e: + return { + 'type': ResponseTypes.ACCESS_DENIED, + 'errors': [{'field': 'access_token', 'message': str(e)}], + 'data': None, + } + + +def auth_required(func): + @wraps(func) + async def wrapper(*args, **kwargs): + if os.environ.get('API_DISABLE_AUTH', 'False').lower() != 'true': + access_token = kwargs['request'].headers.get('Authorization', None) + if not access_token: + return { + 'type': ResponseTypes.ACCESS_DENIED, + 'errors': [ + { + 'field': 'access_token', + 'message': 'Missing access token', + } + ], + 'data': None, + } + access_token = access_token.split(' ')[1] + access_token_data = get_access_token(access_token) + if access_token_data.get('data', {}).get('active', False) is False: + return { + 'type': ResponseTypes.ACCESS_DENIED, + 'errors': [ + { + 'field': 'access_token', + 'message': 'Access token is not active. Check that the domain is correct and the token is not expired.', + } + ], + 'data': None, + } + if access_token_data['type'] == ResponseTypes.SUCCESS: + kwargs['access_token_data'] = access_token_data['data'] + else: + kwargs['access_token_data'] = None + return await func(*args, **kwargs) + + return wrapper + + +def check_permissions(permissions: list[str]): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + if os.environ.get('API_DISABLE_AUTH', 'False').lower() != 'true': + access_token_data = kwargs['access_token_data'] + auth = get_auth() + + group_name = access_token_data.get('group', [None])[0] + user_data = users_use_case.get( + auth=auth, + params={ + 'app_name': os.environ.get('APP_NAME'), + 'client_id': clients_use_case.get_client_by_name( + auth=auth, + params={ + 'app_name': os.environ.get('APP_NAME'), + 'client_name': group_name, + }, + )['data']['id'], + 'user_id': access_token_data['sub'], + }, + ) + if user_data['type'] != ResponseTypes.SUCCESS: + return { + 'type': ResponseTypes.ACCESS_DENIED, + 'errors': [ + { + 'field': 'check permissions', + 'message': 'We could not find the user associated with the access token when checking permissions', + } + ], + 'data': None, + } + user_permissions = [ + permission['name'] + for permission in user_data['data']['role']['permissions'] + ] + for permission in permissions: + if permission not in user_permissions: + return { + 'type': ResponseTypes.ACCESS_DENIED, + 'errors': [ + { + 'field': 'permissions', + 'message': 'You do not have permission to access this resource', + } + ], + 'data': None, + } + return await func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/src/infra/api/fastapi/main.py b/src/infra/api/fastapi/main.py new file mode 100644 index 0000000..d83520f --- /dev/null +++ b/src/infra/api/fastapi/main.py @@ -0,0 +1,45 @@ +import os + +from src.core.entity.ResponseTypes import ResponseTypes +from src.infra.api.fastapi.middleware import ( + CorrectHTTPCodeMiddleware, + ErrorsToCamelCase, + ResponseToCamelCase, +) +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.infra.api.fastapi.routers import documents + +app = FastAPI() + +origins = list( + filter( + lambda url: f"{url.strip()}", + os.environ.get("API_CORS_ORIGINS", "*").split(","), + ) +) + +# Middlewares +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(CorrectHTTPCodeMiddleware) +app.add_middleware(ResponseToCamelCase) +app.add_middleware(ErrorsToCamelCase) + +# Routers +app.include_router(documents.router) + + +@app.get("/") +async def index(): + return { + "type": ResponseTypes.SUCCESS, + "error": [], + "data": "Welcome to the Core Auth API. Check the documentation at /docs.", + } diff --git a/src/infra/api/fastapi/middleware.py b/src/infra/api/fastapi/middleware.py new file mode 100644 index 0000000..c2932ae --- /dev/null +++ b/src/infra/api/fastapi/middleware.py @@ -0,0 +1,69 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + +from src.core.entity.ResponseTypes import ResponseTypes +from src.infra.api.fastapi.tools import ( + convert_all_to_camel_case, + convert_keys_to_camel_case, + get_body, + set_body, +) +from fastapi import status + + +class CorrectHTTPCodeMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + """ + Adjust the HTTP code based on the response type + + There will be delay and large use of RAM due to the size of the files that are returned. + Source: https://stackoverflow.com/questions/69670125/how-to-log-raw-http-request-response-in-python-fastapi/73464007#73464007 + If the files are too large, consider using a different approach. For example, saving the files in a temporary folder and returning the path to the client or a decorator. + """ + response = await call_next(request) + response_json = await get_body(response) + if response_json: + match response_json.get("type", None): + case ResponseTypes.PARAMETERS_ERROR: + response.status_code = status.HTTP_400_BAD_REQUEST + case ResponseTypes.ACCESS_DENIED: + response.status_code = status.HTTP_401_UNAUTHORIZED + case ResponseTypes.RESOURCE_ERROR: + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + case ResponseTypes.SYSTEM_ERROR: + response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + case _: + response.status_code = status.HTTP_200_OK + else: + response.status_code = status.HTTP_200_OK + return response + + +class ResponseToCamelCase(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + """ + Convert snake_case to camelCase + """ + response = await call_next(request) + response_json = await get_body(response) + if response_json: + response_json = convert_keys_to_camel_case(response_json) + return set_body(response, response_json) + return response + + +class ErrorsToCamelCase(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + """ + Convert error from snake_case to camelCase + """ + response = await call_next(request) + response_json = await get_body(response) + if ( + response_json + and "type" in response_json + and response_json["type"] != ResponseTypes.SUCCESS + ): + response_json["errors"] = convert_all_to_camel_case(response_json["errors"]) + return set_body(response, response_json) + return response diff --git a/src/infra/api/fastapi/requirements.txt b/src/infra/api/fastapi/requirements.txt new file mode 100644 index 0000000..2c4acd4 --- /dev/null +++ b/src/infra/api/fastapi/requirements.txt @@ -0,0 +1,3 @@ +fastapi[standard] +pyhumps +pyjwt diff --git a/src/infra/api/fastapi/routers/__init__.py b/src/infra/api/fastapi/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/infra/api/fastapi/routers/documents.py b/src/infra/api/fastapi/routers/documents.py new file mode 100644 index 0000000..401836c --- /dev/null +++ b/src/infra/api/fastapi/routers/documents.py @@ -0,0 +1,37 @@ +import os +from typing import Any +from pydantic import BaseModel +from src.infra.api.fastapi.tools import ( + convert_keys_to_snake_case, +) +from fastapi import APIRouter, Depends, Request, Response +from src.core.use_case import documents_use_case +from src.infra.storage.AzureStorage import AzureStorage + +router = APIRouter( + prefix="/api/v1/documents", + tags=["documents"], +) + + +class DocumentListModel(BaseModel): + appName: Any | None = None + clientId: Any | None = None + + +@router.get( + "/", + summary="Get list documents", + description="Get list documents", +) +async def list_documents( + request: Request, + response: Response, + query: DocumentListModel = Depends(), +) -> dict: + storage = AzureStorage( + account_name=os.getenv("AZURE_STORAGE_ACCOUNT_NAME", ""), + account_key=os.getenv("AZURE_STORAGE_ACCOUNT_KEY", ""), + ) + params = convert_keys_to_snake_case(query.dict(exclude_none=True)) + return documents_use_case.documents_list(storage=storage, params=params) diff --git a/src/infra/api/fastapi/tools.py b/src/infra/api/fastapi/tools.py new file mode 100644 index 0000000..d24cfd7 --- /dev/null +++ b/src/infra/api/fastapi/tools.py @@ -0,0 +1,105 @@ +import json +from typing import Any + +from starlette.concurrency import iterate_in_threadpool + +from fastapi import Response + + +def is_json(myjson): + try: + json.loads(myjson) + except ValueError: + return False + return True + + +async def get_body(response): + response_body = [chunk async for chunk in response.body_iterator] + response.body_iterator = iterate_in_threadpool(iter(response_body)) + if not response_body: + return None + decode_str = response_body[0].decode() + if is_json(decode_str): + return json.loads(decode_str) + return None + + +def set_body(response: Response, body: dict[str, Any]) -> Response: + """ + Set the body of the response + """ + response.body = json.dumps(body).encode("utf-8") + response.headers["Content-Length"] = str(len(response.body)) + return Response( + content=json.dumps(body).encode("utf-8"), + status_code=response.status_code, + headers=dict(response.headers), + media_type=response.media_type, + ) + + +def to_camel_case(input_str) -> str: + """ + Convert snake_case to camelCase + """ + components = input_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) + + +def convert_keys_to_camel_case(item: dict[str, Any]) -> dict[str, Any]: + """ + Convert the keys of the dictionary to camel case (snake_case to camelCase) in all levels + """ + if isinstance(item, dict): + new_dict = {} + for k, v in item.items(): + new_key = to_camel_case(k) + new_dict[new_key] = convert_keys_to_camel_case(v) + return new_dict + elif isinstance(item, list): + return [convert_keys_to_camel_case(item) for item in item] + else: + return item + + +def convert_all_to_camel_case(item: dict[str, Any]) -> dict[str, Any]: + """ + Convert all keys to camel case (snake_case to camelCase) in all levels + """ + if isinstance(item, dict): + new_dict = {} + for k, v in item.items(): + new_key = to_camel_case(k) + new_value = to_camel_case(v) if isinstance(v, str) else v + new_dict[new_key] = convert_all_to_camel_case(new_value) + return new_dict + elif isinstance(item, list): + return [convert_all_to_camel_case(item) for item in item] + else: + return item + + +def to_snake_case(input_str) -> str: + """ + Convert camelCase to snake_case + """ + return "".join(["_" + i.lower() if i.isupper() else i for i in input_str]).lstrip( + "_" + ) + + +def convert_keys_to_snake_case(item: dict[str, Any]) -> dict[str, Any]: + """ + Convert the keys of the dictionary to snake case (camelCase to snake_case) in all levels + """ + if isinstance(item, dict): + new_dict = {} + for k, v in item.items(): + new_key = to_snake_case(k) + new_dict[new_key] = v + return new_dict + elif isinstance(item, list): + return [convert_keys_to_snake_case(item) for item in item] + else: + return item diff --git a/src/infra/api/flask/Dockerfile b/src/infra/api/flask/Dockerfile new file mode 100644 index 0000000..ace4485 --- /dev/null +++ b/src/infra/api/flask/Dockerfile @@ -0,0 +1,4 @@ +FROM base-core-app + +# launcher +ENTRYPOINT flask --app main run --host=0.0.0.0 --debug diff --git a/src/infra/api/flask/main.py b/src/infra/api/flask/main.py new file mode 100644 index 0000000..9eceeab --- /dev/null +++ b/src/infra/api/flask/main.py @@ -0,0 +1,21 @@ +import os + +from flask import Flask, request + +from src.core.use_case import documents_use_case +from src.infra.storage.AzureStorage import AzureStorage +from src.infra.api.flask.middlewares import register_middlewares + +app = Flask(__name__) + +register_middlewares(app) + + +@app.route("/api/v1/documents/") +def documents_list(): + storage = AzureStorage( + account_name=os.getenv("AZURE_STORAGE_ACCOUNT_NAME", ""), + account_key=os.getenv("AZURE_STORAGE_ACCOUNT_KEY", ""), + ) + params = request.args + return documents_use_case.documents_list(storage=storage, params=params) diff --git a/src/infra/api/flask/middlewares.py b/src/infra/api/flask/middlewares.py new file mode 100644 index 0000000..c081120 --- /dev/null +++ b/src/infra/api/flask/middlewares.py @@ -0,0 +1,92 @@ +import json +from flask import request +from typing import Any + + +def to_snake_case(input_str) -> str: + """ + Convert camelCase to snake_case + """ + return "".join(["_" + i.lower() if i.isupper() else i for i in input_str]).lstrip( + "_" + ) + + +def to_camel_case(input_str) -> str: + """ + Convert snake_case to camelCase + """ + components = input_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) + + +def convert_keys_to_snake_case(item: dict[str, Any]) -> dict[str, Any]: + """ + Convert the keys of the dictionary to snake case (camelCase to snake_case) in all levels + """ + if isinstance(item, dict): + new_dict = {} + for k, v in item.items(): + new_key = to_snake_case(k) + new_dict[new_key] = v + return new_dict + elif isinstance(item, list): + return [convert_keys_to_snake_case(item) for item in item] + else: + return item + + +def convert_keys_to_camel_case(item: dict[str, Any]) -> dict[str, Any]: + """ + Convert the keys of the dictionary to camel case (snake_case to camelCase) in all levels + """ + if isinstance(item, dict): + new_dict = {} + for k, v in item.items(): + new_key = to_camel_case(k) + new_dict[new_key] = convert_keys_to_camel_case(v) + return new_dict + elif isinstance(item, list): + return [convert_keys_to_camel_case(item) for item in item] + else: + return item + + +def register_middlewares(app): + """ + Registra los middlewares a la instancia de Flask + """ + + @app.before_request + def convert_request_keys_to_snake_case_middleware(): + """ + Convert the keys of the request to snake case (camelCase to snake_case) in all levels + """ + request.args = convert_keys_to_snake_case(request.args) + + @app.after_request + def convert_response_to_camel_case_middleware(response): + """ + Convert the keys of the response to camel case (snake_case to camelCase) in all levels + """ + response.data = json.dumps( + convert_keys_to_camel_case(json.loads(response.data)) + ) + return response + + @app.after_request + def convert_response_to_json_middleware(response): + """ + Convert the response to JSON format + """ + response.headers["Content-Type"] = "application/json" + return response + + @app.after_request + def add_cors_headers_middleware(response): + """ + Add CORS headers to the response + """ + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE" + return response diff --git a/src/infra/api/flask/requirements.txt b/src/infra/api/flask/requirements.txt new file mode 100644 index 0000000..7e10602 --- /dev/null +++ b/src/infra/api/flask/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/src/infra/storage/AzureStorage.py b/src/infra/storage/AzureStorage.py new file mode 100644 index 0000000..bcbd4a4 --- /dev/null +++ b/src/infra/storage/AzureStorage.py @@ -0,0 +1,35 @@ +from src.core.entity.ResponseTypes import ResponseTypes +from src.core.decorators import check_params +from src.core.use_case.documents_use_case import DocumentListModel +from src.core.entity.Documents import DocumentsType +from random import randint +from faker import Faker +fake = Faker() + +class AzureStorage: + def __init__(self, account_name, account_key): + self.account_name = account_name + self.account_key = account_key + + def put_file(self, container_name, file_path): + pass + + def download_file(self, container_name, file_path): + pass + + @check_params(DocumentListModel) + def list_files(self, container_name): + return { + 'type': ResponseTypes.SUCCESS, + 'errors': [], + 'data': [ + { + 'id': id, + 'name': fake.file_name(), + 'type': DocumentsType.FILE.value if randint(0, 1) else DocumentsType.DIRECTORY.value, + 'public_url': fake.url() + fake.file_name(), + 'created_at': fake.date_time_this_year(), + 'parent_id': randint(1, 10) if randint(0, 1) else None, + } + for id in range(1, 10)], + }