From 3dba4c1ddef6ed680c67d43a8e8a580b2124c4cf Mon Sep 17 00:00:00 2001 From: Marciel Torres Date: Sun, 28 Jan 2024 17:39:29 -0300 Subject: [PATCH 1/2] Adding FastAPI --- Dockerfile | 3 +++ Makefile | 4 +-- README.md | 19 +++++++++++++- docker-compose.yml | 4 ++- pyproject.toml | 6 ++++- run.py | 25 +++++++++++++++++++ settings.conf | 1 + src/api.py | 10 ++++++++ src/main.py | 16 ------------ tests/it/__init__.py | 0 tests/it/test_api.py | 15 +++++++++++ tests/test_main.py | 8 ------ tests/unit/config/__init__.py | 0 tests/{ => unit}/config/settings_to_test.conf | 0 tests/{ => unit}/config/test_settings.py | 10 ++++---- 15 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 run.py create mode 100644 src/api.py delete mode 100755 src/main.py create mode 100644 tests/it/__init__.py create mode 100644 tests/it/test_api.py delete mode 100644 tests/test_main.py create mode 100644 tests/unit/config/__init__.py rename tests/{ => unit}/config/settings_to_test.conf (100%) rename tests/{ => unit}/config/test_settings.py (78%) diff --git a/Dockerfile b/Dockerfile index f724787..caef0e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.11.0-slim-buster AS base +EXPOSE 8000 + WORKDIR /src COPY pyproject.toml . @@ -16,3 +18,4 @@ FROM dependencies AS production COPY src src COPY settings.conf src COPY logging.conf src +COPY run.py src diff --git a/Makefile b/Makefile index b13e60c..dc306f6 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ local/lint/fix: poetry run ruff . --fix --exit-non-zero-on-fix local/run: - poetry run python src/main.py + poetry run python run.py ############################################ # COMMANDS TO RUN USING DOCKER (RECOMMENDED) @@ -44,7 +44,7 @@ docker/lint/fix: docker-compose run ${APP_NAME} poetry run ruff . --fix --exit-non-zero-on-fix docker/run: - docker-compose run ${APP_NAME} poetry run python src/main.py + docker-compose run --service-ports ${APP_NAME} poetry run python run.py #################################### # DOCKER IMAGE COMMANDS diff --git a/README.md b/README.md index d201705..89aed83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # fast-api-boilerplate-project -it's a simple and useful boilerplate for python projects using fast api framework +it's a simple and useful boilerplate for python projects using FastAPI framework ## Technology and Resources @@ -9,6 +9,8 @@ it's a simple and useful boilerplate for python projects using fast api framewor - [Docker Compose](https://docs.docker.com/compose/) - **pre-requisite** - [Poetry](https://python-poetry.org/) - **pre-requisite** - [Ruff](https://github.com/astral-sh/ruff) +- [FastAPI](https://fastapi.tiangolo.com/) +- [Uvicorn](https://www.uvicorn.org/) *Please pay attention on **pre-requisites** resources that you must install/configure.* @@ -38,6 +40,21 @@ push image | `make docker/image/push` | - | to push the docker image *Please, check all available commands in the [Makefile](Makefile) for more information*. +### Uvicorn settings + +Uvicorn is an ASGI web server implementation for Python and you can [configure](https://www.uvicorn.org/settings/) it overriding these values on the [settings.conf](settings.conf) file. + +Variable | Description | Available Values | Default Value | Required +--- | --- | --- | --- | --- +UVICORN_HOST | The host of the application | `a valid host address` | `0.0.0.0` | Yes +UVICORN_PORT | The application port | `a valid port number` | `8000` | Yes +UVICORN_WORKERS | The number of uvicorn workers | `a valid number` | `dev` | No +UVICORN_ACCESS_LOG | Enable or disable the access log | `True / False` | `True` | No +UVICORN_LOG_LEVEL | Set the log level | `critical / error / warning / info / debug / trace'` | `info` | No + +*Note: The default value of these configs are available on [run.py](run.py).* + + ## Logging This project uses a simple way to configure the log with [logging.conf](logging.conf) to show the logs on the container output console. diff --git a/docker-compose.yml b/docker-compose.yml index 846c6e7..d061182 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,4 +9,6 @@ services: target: "development" volumes: - ".:/src" - env_file: .env \ No newline at end of file + env_file: .env + ports: + - "8000:8000" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index be21cc9..e9eb4b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,14 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.11" +fastapi = "0.109.0" +uvicorn = "0.27.0" [tool.poetry.dev-dependencies] pytest = "7.4.4" pytest-cov = "^4.1.0" ruff = "^0.1.6" +httpx = "0.26.0" [tool.pytest.ini_options] testpaths = ["tests",] @@ -19,7 +22,7 @@ pythonpath = ["src",] [tool.coverage.run] branch = true -omit = ["*/tests/*"] +omit = ["*/tests/*", "run.py"] [tool.coverage.report] show_missing = true @@ -32,6 +35,7 @@ directory = "htmlcov" line-length = 120 select = ["E", "F", "W", "I", "N", "S"] target-version = "py311" +exclude = ["run.py"] [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/run.py b/run.py new file mode 100644 index 0000000..7499e56 --- /dev/null +++ b/run.py @@ -0,0 +1,25 @@ +from uvicorn import run + +from src.config.settings import settings +from src.api import app as application + + +def main() -> None: + app_host = settings.get('UVICORN_HOST', '0.0.0.0') + app_port = settings.get('UVICORN_PORT', 8000) + app_num_of_workers = settings.get('UVICORN_WORKERS', 4) + uvicorn_access_log = True if settings.get("UVICORN_ACCESS_LOG", "False") == "True" else False + uvicorn_log_level = settings.get("UVICORN_LOG_LEVEL", "info").lower() + + run( + "run:application", + host=app_host, + port=app_port, + workers=app_num_of_workers, + log_level=uvicorn_log_level, + access_log=uvicorn_access_log, + ) + + +if __name__ == "__main__": + main() diff --git a/settings.conf b/settings.conf index 3c5347f..53cdac4 100644 --- a/settings.conf +++ b/settings.conf @@ -1,5 +1,6 @@ [default] app_name=fast-api-boilerplate-project +app_description=fast-api-boilerplate-project [dev] diff --git a/src/api.py b/src/api.py new file mode 100644 index 0000000..5375394 --- /dev/null +++ b/src/api.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI + +from src.config.settings import settings + +app = FastAPI(title=settings.get('app_name'), description=settings.get('app_description')) + + +@app.get("/health-check") +async def health_check(): + return {"message": "OK"} diff --git a/src/main.py b/src/main.py deleted file mode 100755 index 73d958d..0000000 --- a/src/main.py +++ /dev/null @@ -1,16 +0,0 @@ -from logging import getLogger -from logging.config import fileConfig as logConfig - -from config.settings import settings - -logConfig("./logging.conf", disable_existing_loggers=False) -logger = getLogger(__name__) - - -def hello() -> str: - logger.info(f"Hello from {settings.get('app_name')}") - return "Hello" - - -if __name__ == "__main__": # pragma: no cover - print(hello()) diff --git a/tests/it/__init__.py b/tests/it/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/it/test_api.py b/tests/it/test_api.py new file mode 100644 index 0000000..f28d18a --- /dev/null +++ b/tests/it/test_api.py @@ -0,0 +1,15 @@ +from unittest import TestCase + +from fastapi.testclient import TestClient + +from src.api import app + + +class ApiITTest(TestCase): + def setUp(self): + self.client_api = TestClient(app) + + def test_when_i_call_health_check_should_be_success(self): + response = self.client_api.get("/health-check") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"message": "OK"}) diff --git a/tests/test_main.py b/tests/test_main.py deleted file mode 100644 index 09d5fd4..0000000 --- a/tests/test_main.py +++ /dev/null @@ -1,8 +0,0 @@ -from unittest import TestCase - -from src.main import hello - - -class MainTest(TestCase): - def test_main_hello(self): - self.assertEqual(hello(), "Hello") diff --git a/tests/unit/config/__init__.py b/tests/unit/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/config/settings_to_test.conf b/tests/unit/config/settings_to_test.conf similarity index 100% rename from tests/config/settings_to_test.conf rename to tests/unit/config/settings_to_test.conf diff --git a/tests/config/test_settings.py b/tests/unit/config/test_settings.py similarity index 78% rename from tests/config/test_settings.py rename to tests/unit/config/test_settings.py index dd66714..668186a 100644 --- a/tests/config/test_settings.py +++ b/tests/unit/config/test_settings.py @@ -6,7 +6,7 @@ class SettingsTest(TestCase): def setUp(self) -> None: - self.settings = Settings(file='./tests/config/settings_to_test.conf') + self.settings = Settings(file='./tests/unit/config/settings_to_test.conf') def test_get_setting_value_with_success(self): self.assertEqual(self.settings.get('app_name'), 'app-name') @@ -22,20 +22,20 @@ def test_get_setting_float_value_with_success(self): @mock.patch.dict(os.environ, {'ENV': 'prod'}, clear=True) def test_get_setting_value_from_production_env_with_success(self): - prod_settings = Settings(file='./tests/config/settings_to_test.conf') + prod_settings = Settings(file='./tests/unit/config/settings_to_test.conf') self.assertEqual(prod_settings.get('app_var'), 'prod-app-var') @mock.patch.dict(os.environ, {'ENV': 'qa'}, clear=True) def test_get_setting_value_from_qa_env_with_success(self): - qa_settings = Settings(file='./tests/config/settings_to_test.conf') + qa_settings = Settings(file='./tests/unit/config/settings_to_test.conf') self.assertEqual(qa_settings.get('app_var'), 'qa-app-var') @mock.patch.dict(os.environ, {'ENV': 'test'}, clear=True) def test_get_setting_value_from_test_env_with_success(self): - test_settings = Settings(file='./tests/config/settings_to_test.conf') + test_settings = Settings(file='./tests/unit/config/settings_to_test.conf') self.assertEqual(test_settings.get('app_var'), 'test-app-var') @mock.patch.dict(os.environ, {'ENV': 'dev'}, clear=True) def test_get_setting_value_from_dev_env_with_success(self): - dev_settings = Settings(file='./tests/config/settings_to_test.conf') + dev_settings = Settings(file='./tests/unit/config/settings_to_test.conf') self.assertEqual(dev_settings.get('app_var'), 'dev-app-var') From fb811d080e8f923103febb82b16306b4082e24fb Mon Sep 17 00:00:00 2001 From: Marciel Torres Date: Sun, 28 Jan 2024 17:45:22 -0300 Subject: [PATCH 2/2] Update settings.conf --- settings.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.conf b/settings.conf index 53cdac4..f26eca6 100644 --- a/settings.conf +++ b/settings.conf @@ -1,6 +1,6 @@ [default] app_name=fast-api-boilerplate-project -app_description=fast-api-boilerplate-project +app_description=fast-api-boilerplate-project-description [dev]