diff --git a/Makefile b/Makefile index 0308d77..ebf7f12 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,19 @@ -black: - black ./lib +format: flake8 pylint ruff black -lint: flake8 pylint +black: + black ./lib && black ./tests flake8: - flake8 --ignore E501,E402,F401,W503 ./lib + flake8 --ignore E501,E402,F401,W503,C0414 ./lib && flake8 --ignore E501,E402,F401,W503,C0414 ./tests pylint: - pylint --extension-pkg-whitelist='pydantic' ./lib/* + pylint --extension-pkg-whitelist='pydantic' ./lib && pylint --extension-pkg-whitelist='pydantic' ./tests + +ruff: + ruff check --fix + +test: + python3 -m pytest . dev: python3 -m uvicorn lib:app --reload --port 3000 @@ -19,3 +25,5 @@ clean: build: docker build -t infinity-api . --no-cache + +@PHONY: black lint flake8 pylint test dev clean build ruff diff --git a/README.md b/README.md index afd731b..bab77cc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,10 @@ - Install dependencies `python3 -m pip install -r requirements.txt` ## Development -- black ./lib -- pylint --extension-pkg-whitelist='pydantic' ./lib/* -- flake8 --ignore E501,E402,F401,W503 ./lib +- make format +- make test +- make clean +- make build ## Starting the server - Setup MONGODB_CONNECTION_STRING: diff --git a/lib/__init__.py b/lib/__init__.py index 171b18e..c9b8f3b 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -25,4 +25,4 @@ def parse_error(error): return f"{exc_type}: {exc_obj}" -from lib.api import app # pylint: disable=wrong-import-position,cyclic-import +from lib.api import app as app # pylint: disable=wrong-import-position,cyclic-import diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py index fb1c91e..20f4874 100644 --- a/lib/controllers/environment.py +++ b/lib/controllers/environment.py @@ -27,18 +27,8 @@ class EnvController: - CRUD operations over models.Env on the database """ - def __init__(self, env: Env): - self._env = env - - @property - def env(self) -> Env: - return self._env - - @env.setter - def env(self, env: Env): - self._env = env - - async def create_env(self) -> Union[EnvCreated, HTTPException]: + @staticmethod + async def create_env(env: Env) -> Union[EnvCreated, HTTPException]: """ Create a env in the database. @@ -46,7 +36,7 @@ async def create_env(self) -> Union[EnvCreated, HTTPException]: views.EnvCreated """ try: - async with EnvRepository(self.env) as env_repo: + async with EnvRepository(env) as env_repo: await env_repo.create_env() except PyMongoError as e: logger.error( @@ -157,8 +147,9 @@ async def get_rocketpy_env_binary( f"Call to controllers.environment.get_rocketpy_env_binary completed for Env {env_id}" ) + @staticmethod async def update_env_by_id( - self, env_id: str + env_id: str, env: Env ) -> Union[EnvUpdated, HTTPException]: """ Update a models.Env in the database. @@ -173,7 +164,7 @@ async def update_env_by_id( HTTP 404 Not Found: If the env is not found in the database. """ try: - async with EnvRepository(self.env) as env_repo: + async with EnvRepository(env) as env_repo: await env_repo.update_env_by_id(env_id) except PyMongoError as e: logger.error( diff --git a/lib/routes/environment.py b/lib/routes/environment.py index 1eb84a6..6834ed3 100644 --- a/lib/routes/environment.py +++ b/lib/routes/environment.py @@ -36,7 +36,7 @@ async def create_env(env: Env) -> EnvCreated: ``` models.Env JSON ``` """ with tracer.start_as_current_span("create_env"): - return await EnvController(env).create_env() + return await EnvController.create_env(env) @router.get("/{env_id}") @@ -63,11 +63,11 @@ async def update_env(env_id: str, env: Env) -> EnvUpdated: ``` """ with tracer.start_as_current_span("update_env"): - return await EnvController(env).update_env_by_id(env_id) + return await EnvController.update_env_by_id(env_id, env) @router.get( - "/rocketpy/{env_id}", + "/{env_id}/rocketpy", responses={ 203: { "description": "Binary file download", @@ -118,4 +118,4 @@ async def delete_env(env_id: str) -> EnvDeleted: ``` env_id: str ``` """ with tracer.start_as_current_span("delete_env"): - return await EnvController(env_id).delete_env_by_id(env_id) + return await EnvController.delete_env_by_id(env_id) diff --git a/pyproject.toml b/pyproject.toml index c3dbcae..a667ee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,7 @@ -[build-system] -requires = ["setuptools", "setuptools_scm"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} - [project] name = "Infinity-API" -version = "2.2.0" +version = "2.3.0" description = "RESTFULL open API for rocketpy" -dynamic = ["dependencies"] requires-python = ">=3.12" authors = [ {name = "Gabriel Barberini", email = "gabrielbarberinirc@gmail.com"} @@ -21,7 +13,7 @@ maintainers = [ readme = "README.md" keywords = ["rocketpy", "API", "simulation", "rocket", "flight"] classifiers = [ - "Development Status :: Alpha", + "Development Status :: Production", "Programming Language :: Python" ] @@ -52,3 +44,14 @@ disable = """ raise-missing-from, too-many-instance-attributes, """ + +[tool.ruff] +line-length = 79 +target-version = "py313" + +[tool.ruff.lint] +select = ["E", "F", "N", "Q"] +ignore = ["N815", "E501", "Q000", "E402"] +fixable = [ + "F401", +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..00564b5 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,3 @@ +flake8 +pylint +ruff diff --git a/tests/test_routes/test_environment_route.py b/tests/test_routes/test_environment_route.py new file mode 100644 index 0000000..ca7ee51 --- /dev/null +++ b/tests/test_routes/test_environment_route.py @@ -0,0 +1,114 @@ +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch +from lib.models.environment import Env +from lib.controllers.environment import EnvController +from lib.views.environment import ( + EnvCreated, + EnvUpdated, + EnvDeleted, + EnvSummary, +) +from lib import app + +client = TestClient(app) + + +@pytest.fixture +def mock_env(): + return Env(latitude=0, longitude=0) + + +@pytest.fixture +def mock_env_summary(): + return EnvSummary() + + +def test_create_env(mock_env): + with patch.object( + EnvController, "create_env", return_value=EnvCreated(env_id="123") + ) as mock_create_env: + response = client.post( + "/environments/", json={"latitude": 0, "longitude": 0} + ) + assert response.status_code == 200 + assert response.json() == { + "env_id": "123", + "message": "Environment successfully created", + } + mock_create_env.assert_called_once_with(Env(latitude=0, longitude=0)) + + +def test_read_env(mock_env): + with patch.object( + EnvController, "get_env_by_id", return_value=mock_env + ) as mock_read_env: + response = client.get("/environments/123") + assert response.status_code == 200 + expected_content = mock_env.model_dump() + expected_content["date"] = expected_content["date"].isoformat() + assert response.json() == expected_content + mock_read_env.assert_called_once_with("123") + + +def test_update_env(): + with patch.object( + EnvController, + "update_env_by_id", + return_value=EnvUpdated(env_id="123"), + ) as mock_update_env: + response = client.put( + "/environments/123", json={"longitude": 1, "latitude": 1} + ) + assert response.status_code == 200 + assert response.json() == { + "env_id": "123", + "message": "Environment successfully updated", + } + mock_update_env.assert_called_once_with( + "123", Env(latitude=1, longitude=1) + ) + + +def test_delete_env(): + with patch.object( + EnvController, + "delete_env_by_id", + return_value=EnvDeleted(env_id="123"), + ) as mock_delete_env: + response = client.delete("/environments/123") + assert response.status_code == 200 + assert response.json() == { + "env_id": "123", + "message": "Environment successfully deleted", + } + mock_delete_env.assert_called_once_with("123") + + +def test_simulate_env(mock_env_summary): + with patch.object( + EnvController, "simulate_env", return_value=mock_env_summary + ) as mock_simulate_env: + response = client.get("/environments/123/summary") + assert response.status_code == 200 + expected_content = mock_env_summary.model_dump() + expected_content["date"] = expected_content["date"].isoformat() + expected_content["local_date"] = expected_content[ + "local_date" + ].isoformat() + expected_content["datetime_date"] = expected_content[ + "datetime_date" + ].isoformat() + assert response.json() == expected_content + mock_simulate_env.assert_called_once_with("123") + + +def test_read_rocketpy_env(mock_env): + with patch.object( + EnvController, "get_rocketpy_env_binary", return_value=b'rocketpy' + ) as mock_read_rocketpy_env: + response = client.get("/environments/123/rocketpy") + assert response.status_code == 203 + assert response.content == b'rocketpy' + assert response.headers["content-type"] == "application/octet-stream" + mock_read_rocketpy_env.assert_called_once_with("123")