diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 150b19c..6a801cf 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,5 +1,5 @@ # Documentation: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsuses -name: github_worflow +name: github_workflow run-name: GitHub Workflow env: @@ -41,11 +41,11 @@ env: # Logging level PROD_LOG_LEVEL: ${{ vars.PROD_LOG_LEVEL }} # Kube configuration - PROD_KUBE_CONFIG: ${{ secrets.DEV_KUBE_CONFIG }} + PROD_KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }} # Allow one concurrent deployment concurrency: - group: github_worflow + group: github_workflow cancel-in-progress: true on: @@ -89,7 +89,7 @@ jobs: release: needs: test runs-on: ubuntu-latest - if: ${{ vars.RUN_CICD == 'true' && success() && (vars.DEPLOY_DEV == 'true' || vars.DEPLOY_PROD == 'true') }} + if: ${{ vars.RUN_CICD == 'true' && success() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/prod') && (vars.DEPLOY_DEV == 'true' || vars.DEPLOY_PROD == 'true') }} steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/requirements-all.txt b/requirements-all.txt index 7188321..63c53a1 100644 --- a/requirements-all.txt +++ b/requirements-all.txt @@ -14,9 +14,9 @@ charset-normalizer==3.1.0 click==8.1.3 coverage==7.2.7 dnspython==2.3.0 -email-validator==1.3.0 +email-validator==2.1.0.post1 exceptiongroup==1.1.1 -fastapi==0.87.0 +fastapi==0.108.0 flake8==5.0.4 frozenlist==1.3.3 h11==0.14.0 @@ -32,7 +32,8 @@ packaging==23.1 Pillow==9.1.0 pluggy==1.2.0 pycodestyle==2.9.1 -pydantic==1.10.9 +pydantic==2.5.3 +pydantic-settings==2.1.0 pyflakes==2.5.0 pytest==7.2.0 pytest-asyncio==0.20.3 @@ -44,9 +45,9 @@ PyYAML==6.0 rfc3986==1.5.0 six==1.16.0 sniffio==1.3.0 -starlette==0.21.0 +starlette==0.29.0 tomli==2.0.1 -typing_extensions==4.6.3 +typing_extensions==4.9.0 urllib3==1.26.16 uvicorn==0.19.0 Werkzeug==2.3.6 diff --git a/requirements.txt b/requirements.txt index 4565a1d..1907b96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,2 @@ -aiofile==3.8.7 common-code[test] @ git+https://github.com/swiss-ai-center/common-code.git@main -flake8==5.0.4 Pillow==9.1.0 -pytest==7.2.0 -pytest-asyncio==0.20.3 -pytest-cov==4.0.0 -pytest-httpserver==1.0.6 diff --git a/src/main.py b/src/main.py index d8d7a60..6b3f196 100644 --- a/src/main.py +++ b/src/main.py @@ -4,9 +4,8 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import RedirectResponse from common_code.config import get_settings -from pydantic import Field from common_code.http_client import HttpClient -from common_code.logger.logger import get_logger +from common_code.logger.logger import get_logger, Logger from common_code.service.controller import router as service_router from common_code.service.service import ServiceService from common_code.storage.service import StorageService @@ -17,6 +16,7 @@ from common_code.service.enums import ServiceStatus from common_code.common.enums import FieldDescriptionType, ExecutionUnitTagName, ExecutionUnitTagAcronym from common_code.common.models import FieldDescription, ExecutionUnitTag +from contextlib import asynccontextmanager # Imports required by the service's model import io @@ -33,8 +33,8 @@ class MyService(Service): """ # Any additional fields must be excluded for Pydantic to work - model: object = Field(exclude=True) - logger: object = Field(exclude=True) + _model: object + _logger: Logger def __init__(self): super().__init__( @@ -58,7 +58,7 @@ def __init__(self): ], has_ai=False ) - self.logger = get_logger(settings) + self._logger = get_logger(settings) def process(self, data): raw = data["image"].data @@ -79,6 +79,54 @@ def process(self, data): } +service_service: ServiceService | None = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Manual instances because startup events doesn't support Dependency Injection + # https://github.com/tiangolo/fastapi/issues/2057 + # https://github.com/tiangolo/fastapi/issues/425 + + # Global variable + global service_service + + # Startup + logger = get_logger(settings) + http_client = HttpClient() + storage_service = StorageService(logger) + my_service = MyService() + tasks_service = TasksService(logger, settings, http_client, storage_service) + service_service = ServiceService(logger, settings, http_client, tasks_service) + + tasks_service.set_service(my_service) + + # Start the tasks service + tasks_service.start() + + async def announce(): + retries = settings.engine_announce_retries + for engine_url in settings.engine_urls: + announced = False + while not announced and retries > 0: + announced = await service_service.announce_service(my_service, engine_url) + retries -= 1 + if not announced: + time.sleep(settings.engine_announce_retry_delay) + if retries == 0: + logger.warning(f"Aborting service announcement after " + f"{settings.engine_announce_retries} retries") + + # Announce the service to its engine + asyncio.ensure_future(announce()) + + yield + + # Shutdown + for engine_url in settings.engine_urls: + await service_service.graceful_shutdown(my_service, engine_url) + + api_description = """ This service analyzes images. It returns the following information: - Format (e.g. image/jpeg) @@ -101,6 +149,7 @@ def process(self, data): # Define the FastAPI application with information app = FastAPI( + lifespan=lifespan, title="Image Analyzer API.", description=api_description, version="1.0.0", @@ -136,53 +185,3 @@ def process(self, data): @app.get("/", include_in_schema=False) async def root(): return RedirectResponse("/docs", status_code=301) - - -service_service: ServiceService | None = None - - -@app.on_event("startup") -async def startup_event(): - # Manual instances because startup events doesn't support Dependency Injection - # https://github.com/tiangolo/fastapi/issues/2057 - # https://github.com/tiangolo/fastapi/issues/425 - - # Global variable - global service_service - - logger = get_logger(settings) - http_client = HttpClient() - storage_service = StorageService(logger) - my_service = MyService() - tasks_service = TasksService(logger, settings, http_client, storage_service) - service_service = ServiceService(logger, settings, http_client, tasks_service) - - tasks_service.set_service(my_service) - - # Start the tasks service - tasks_service.start() - - async def announce(): - retries = settings.engine_announce_retries - for engine_url in settings.engine_urls: - announced = False - while not announced and retries > 0: - announced = await service_service.announce_service(my_service, engine_url) - retries -= 1 - if not announced: - time.sleep(settings.engine_announce_retry_delay) - if retries == 0: - logger.warning(f"Aborting service announcement after " - f"{settings.engine_announce_retries} retries") - - # Announce the service to its engine - asyncio.ensure_future(announce()) - - -@app.on_event("shutdown") -async def shutdown_event(): - # Global variable - global service_service - my_service = MyService() - for engine_url in settings.engine_urls: - await service_service.graceful_shutdown(my_service, engine_url)