diff --git a/.env-devel b/.env-devel index d5df4deab06..3169f8fa259 100644 --- a/.env-devel +++ b/.env-devel @@ -132,7 +132,7 @@ DYNAMIC_SIDECAR_API_SAVE_RESTORE_STATE_TIMEOUT=01:00:00 DIRECTOR_V2_TRACING=null # DYNAMIC_SCHEDULER ---- -DYNAMIC_SCHEDULER_LOGLEVEL=DEBUG +DYNAMIC_SCHEDULER_LOGLEVEL=INFO DYNAMIC_SCHEDULER_PROFILING=1 DYNAMIC_SCHEDULER_USE_INTERNAL_SCHEDULER=0 DYNAMIC_SCHEDULER_STOP_SERVICE_TIMEOUT=01:00:00 @@ -164,6 +164,9 @@ INVITATIONS_TRACING=null LOG_FORMAT_LOCAL_DEV_ENABLED=1 LOG_FILTER_MAPPING='{}' +NOTIFICATIONS_LOGLEVEL=INFO +NOTIFICATIONS_TRACING=null + PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES=30 PAYMENTS_ACCESS_TOKEN_SECRET_KEY=2c0411810565e063309be1457009fb39ce023946f6a354e6935107b57676 PAYMENTS_AUTORECHARGE_DEFAULT_MONTHLY_LIMIT=10000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8705bf17e2e..7a263044f3e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -35,6 +35,7 @@ Makefile @pcrespov @sanderegg /services/efs-guardian/ @matusdrobuliak66 /services/invitations/ @pcrespov /services/migration/ @pcrespov +/services/notifications/ @GitHK /services/payments/ @pcrespov @matusdrobuliak66 /services/resource-usage-tracker/ @matusdrobuliak66 /services/static-webserver/ @GitHK diff --git a/.github/workflows/ci-testing-deploy.yml b/.github/workflows/ci-testing-deploy.yml index 521567e6072..4646a2c73c6 100644 --- a/.github/workflows/ci-testing-deploy.yml +++ b/.github/workflows/ci-testing-deploy.yml @@ -62,6 +62,7 @@ jobs: settings-library: ${{ steps.filter.outputs.settings-library }} simcore-sdk: ${{ steps.filter.outputs.simcore-sdk }} agent: ${{ steps.filter.outputs.agent }} + notifications: ${{ steps.filter.outputs.notifications }} api: ${{ steps.filter.outputs.api }} api-server: ${{ steps.filter.outputs.api-server }} autoscaling: ${{ steps.filter.outputs.autoscaling }} @@ -150,6 +151,12 @@ jobs: - 'services/docker-compose*' - 'scripts/mypy/*' - 'mypy.ini' + notifications: + - 'packages/**' + - 'services/notifications/**' + - 'services/docker-compose*' + - 'scripts/mypy/*' + - 'mypy.ini' api: - 'api/**' api-server: @@ -602,6 +609,49 @@ jobs: with: flags: unittests #optional + unit-test-notifications: + needs: changes + if: ${{ needs.changes.outputs.notifications == 'true' || github.event_name == 'push' }} + timeout-minutes: 18 # if this timeout gets too small, then split the tests + name: "[unit] notifications" + runs-on: ${{ matrix.os }} + strategy: + matrix: + python: ["3.11"] + os: [ubuntu-24.04] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: setup docker buildx + id: buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + - name: setup python environment + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.5.x" + enable-cache: false + cache-dependency-glob: "**/notifications/requirements/ci.txt" + - name: show system version + run: ./ci/helpers/show_system_versions.bash + - name: install + run: ./ci/github/unit-testing/notifications.bash install + - name: typecheck + run: ./ci/github/unit-testing/notifications.bash typecheck + - name: test + if: ${{ !cancelled() }} + run: ./ci/github/unit-testing/notifications.bash test + - uses: codecov/codecov-action@v5 + if: ${{ !cancelled() }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: unittests #optional unit-test-api: needs: changes @@ -1834,6 +1884,7 @@ jobs: unit-test-common-library, unit-test-notifications-library, unit-test-payments, + unit-test-notifications, unit-test-dynamic-scheduler, unit-test-postgres-database, unit-test-python-linting, diff --git a/Makefile b/Makefile index e1e51612e5d..4b28fb44e74 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ SERVICES_NAMES_TO_BUILD := \ efs-guardian \ invitations \ migration \ + notifications \ payments \ resource-usage-tracker \ dynamic-scheduler \ diff --git a/ci/github/unit-testing/notifications.bash b/ci/github/unit-testing/notifications.bash new file mode 100755 index 00000000000..4f78013c19e --- /dev/null +++ b/ci/github/unit-testing/notifications.bash @@ -0,0 +1,43 @@ +#!/bin/bash +# http://redsymbol.net/articles/unofficial-bash-strict-mode/ +set -o errexit # abort on nonzero exitstatus +set -o nounset # abort on unbound variable +set -o pipefail # don't hide errors within pipes +IFS=$'\n\t' + +install() { + make devenv + # shellcheck source=/dev/null + source .venv/bin/activate + pushd services/notifications + make install-ci + popd + uv pip list +} + +test() { + # shellcheck source=/dev/null + source .venv/bin/activate + pushd services/notifications + make test-ci-unit + popd +} + +typecheck() { + # shellcheck source=/dev/null + source .venv/bin/activate + uv pip install mypy + pushd services/notifications + make mypy + popd +} + +# Check if the function exists (bash specific) +if declare -f "$1" >/dev/null; then + # call arguments verbatim + "$@" +else + # Show a helpful error + echo "'$1' is not a known function name" >&2 + exit 1 +fi diff --git a/packages/models-library/src/models_library/api_schemas_notifications/__init__.py b/packages/models-library/src/models_library/api_schemas_notifications/__init__.py new file mode 100644 index 00000000000..dfa868dc522 --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_notifications/__init__.py @@ -0,0 +1,9 @@ +from typing import Final + +from pydantic import TypeAdapter + +from ..rabbitmq_basic_types import RPCNamespace + +NOTIFICATIONS_RPC_NAMESPACE: Final[RPCNamespace] = TypeAdapter( + RPCNamespace +).validate_python("notifications") diff --git a/packages/models-library/src/models_library/errors.py b/packages/models-library/src/models_library/errors.py index cec882e12b7..d1498f63474 100644 --- a/packages/models-library/src/models_library/errors.py +++ b/packages/models-library/src/models_library/errors.py @@ -34,9 +34,10 @@ class ErrorDict(_ErrorDictRequired, total=False): ctx: dict[str, Any] -RABBITMQ_CLIENT_UNHEALTHY_MSG = "RabbitMQ client is in a bad state!" +RABBITMQ_CLIENT_UNHEALTHY_MSG = "RabbitMQ cannot be reached!" +POSRGRES_DATABASE_UNHEALTHY_MSG = "Postgres cannot be reached!" REDIS_CLIENT_UNHEALTHY_MSG = "Redis cannot be reached!" -DOCKER_API_PROXY_UNHEALTHY_MSG = "docker-api-proxy service is not reachable!" +DOCKER_API_PROXY_UNHEALTHY_MSG = "docker-api-proxy cannot be reached!" # NOTE: Here we do not just import as 'from pydantic.error_wrappers import ErrorDict' diff --git a/packages/pytest-simcore/src/pytest_simcore/simcore_services.py b/packages/pytest-simcore/src/pytest_simcore/simcore_services.py index d231fa25307..54a0b839395 100644 --- a/packages/pytest-simcore/src/pytest_simcore/simcore_services.py +++ b/packages/pytest-simcore/src/pytest_simcore/simcore_services.py @@ -47,6 +47,7 @@ "autoscaling": "/", "clusters-keeper": "/", "dask-scheduler": "/health", + "notifications": "/", "datcore-adapter": "/v0/live", "director-v2": "/", "dynamic-schdlr": "/", diff --git a/services/docker-compose-build.yml b/services/docker-compose-build.yml index 7fe0baf7024..34fc98dc264 100644 --- a/services/docker-compose-build.yml +++ b/services/docker-compose-build.yml @@ -169,6 +169,23 @@ services: org.opencontainers.image.source: "${VCS_URL}" org.opencontainers.image.revision: "${VCS_REF}" + notifications: + image: local/notifications:${BUILD_TARGET:?build_target_required} + build: + context: ../ + dockerfile: services/notifications/Dockerfile + cache_from: + - local/notifications:${BUILD_TARGET:?build_target_required} + - ${DOCKER_REGISTRY:-itisfoundation}/notifications:master-github-latest + - ${DOCKER_REGISTRY:-itisfoundation}/notifications:staging-github-latest + - ${DOCKER_REGISTRY:-itisfoundation}/notifications:release-github-latest + target: production + labels: + org.label-schema.schema-version: "1.0" + org.opencontainers.image.created: "${BUILD_DATE}" + org.opencontainers.image.source: "${VCS_URL}" + org.opencontainers.image.revision: "${VCS_REF}" + resource-usage-tracker: image: local/resource-usage-tracker:${BUILD_TARGET:?build_target_required} build: diff --git a/services/docker-compose-deploy.yml b/services/docker-compose-deploy.yml index f3997a0b11d..e6c21da36db 100644 --- a/services/docker-compose-deploy.yml +++ b/services/docker-compose-deploy.yml @@ -25,6 +25,8 @@ services: image: ${DOCKER_REGISTRY:-itisfoundation}/invitations:${DOCKER_IMAGE_TAG:-latest} migration: image: ${DOCKER_REGISTRY:-itisfoundation}/migration:${DOCKER_IMAGE_TAG:-latest} + notifications: + image: ${DOCKER_REGISTRY:-itisfoundation}/notifications:${DOCKER_IMAGE_TAG:-latest} payments: image: ${DOCKER_REGISTRY:-itisfoundation}/payments:${DOCKER_IMAGE_TAG:-latest} dynamic-scheduler: diff --git a/services/docker-compose.devel.yml b/services/docker-compose.devel.yml index 9cef4939712..2b994b99ffd 100644 --- a/services/docker-compose.devel.yml +++ b/services/docker-compose.devel.yml @@ -72,6 +72,16 @@ services: - ../packages:/devel/packages - ${HOST_UV_CACHE_DIR}:/home/scu/.cache/uv + notifications: + environment: + <<: *common-environment + NOTIFICATIONS_PROFILING : ${NOTIFICATIONS_PROFILING} + NOTIFICATIONS_LOGLEVEL: DEBUG + volumes: + - ./notifications:/devel/services/notifications + - ../packages:/devel/packages + - ${HOST_UV_CACHE_DIR}:/home/scu/.cache/uv + clusters-keeper: environment: <<: *common-environment diff --git a/services/docker-compose.local.yml b/services/docker-compose.local.yml index 54523179785..f07fa42453a 100644 --- a/services/docker-compose.local.yml +++ b/services/docker-compose.local.yml @@ -88,6 +88,14 @@ services: - "8008:8000" - "3017:3000" + notifications: + environment: + <<: *common_environment + INVITATIONS_REMOTE_DEBUGGING_PORT : 3000 + ports: + - "8015:8000" + - "3023:3000" + payments: environment: <<: *common_environment diff --git a/services/docker-compose.yml b/services/docker-compose.yml index 45eca7fd7ff..f60ae312164 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -1096,6 +1096,35 @@ services: TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT: ${TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT} TRACING_OPENTELEMETRY_COLLECTOR_PORT: ${TRACING_OPENTELEMETRY_COLLECTOR_PORT} + notifications: + image: ${DOCKER_REGISTRY:-itisfoundation}/notifications:${DOCKER_IMAGE_TAG:-latest} + init: true + hostname: "{{.Node.Hostname}}-{{.Task.Slot}}" + + environment: + LOG_FILTER_MAPPING : ${LOG_FILTER_MAPPING} + LOG_FORMAT_LOCAL_DEV_ENABLED: ${LOG_FORMAT_LOCAL_DEV_ENABLED} + + NOTIFICATIONS_LOGLEVEL: ${NOTIFICATIONS_LOGLEVEL} + NOTIFICATIONS_TRACING: ${NOTIFICATIONS_TRACING} + + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_ENDPOINT: ${POSTGRES_ENDPOINT} + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_USER: ${POSTGRES_USER} + + RABBIT_HOST: ${RABBIT_HOST} + RABBIT_PASSWORD: ${RABBIT_PASSWORD} + RABBIT_PORT: ${RABBIT_PORT} + RABBIT_SECURE: ${RABBIT_SECURE} + RABBIT_USER: ${RABBIT_USER} + + TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT: ${TRACING_OPENTELEMETRY_COLLECTOR_ENDPOINT} + TRACING_OPENTELEMETRY_COLLECTOR_PORT: ${TRACING_OPENTELEMETRY_COLLECTOR_PORT} + + dask-sidecar: image: ${DOCKER_REGISTRY:-itisfoundation}/dask-sidecar:${DOCKER_IMAGE_TAG:-latest} init: true diff --git a/services/dynamic-scheduler/tests/unit/test_cli.py b/services/dynamic-scheduler/tests/unit/test_cli.py index 6bdaa62d1e6..6c89f5f71dc 100644 --- a/services/dynamic-scheduler/tests/unit/test_cli.py +++ b/services/dynamic-scheduler/tests/unit/test_cli.py @@ -38,7 +38,7 @@ def test_echo_dotenv(cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch): with monkeypatch.context() as patch: setenvs_from_dict(patch, environs) - ApplicationSettings.create_from_envs() + assert ApplicationSettings.create_from_envs() def test_list_settings( diff --git a/services/dynamic-sidecar/tests/unit/test_api_rest_health.py b/services/dynamic-sidecar/tests/unit/test_api_rest_health.py index c94d81b8db7..987ddbf1e63 100644 --- a/services/dynamic-sidecar/tests/unit/test_api_rest_health.py +++ b/services/dynamic-sidecar/tests/unit/test_api_rest_health.py @@ -29,4 +29,4 @@ async def test_is_unhealthy_via_rabbitmq(test_client: TestClient) -> None: test_client.application.state.rabbitmq_client._healthy_state = False # noqa: SLF001 response = await test_client.get("/health") assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE, response - assert response.json() == {"detail": "RabbitMQ client is in a bad state!"} + assert response.json() == {"detail": "RabbitMQ cannot be reached!"} diff --git a/services/notifications/Dockerfile b/services/notifications/Dockerfile new file mode 100644 index 00000000000..0718d6f7aa8 --- /dev/null +++ b/services/notifications/Dockerfile @@ -0,0 +1,176 @@ +# syntax=docker/dockerfile:1 + +# Define arguments in the global scope +ARG PYTHON_VERSION="3.11.9" +ARG UV_VERSION="0.6" + +FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv_build +# we docker image is built based on debian +FROM python:${PYTHON_VERSION}-slim-bookworm AS base + + +# +# USAGE: +# cd services/notifications +# docker build -f Dockerfile -t notifications:prod --target production ../../ +# docker run notifications:prod +# +# REQUIRED: context expected at ``osparc-simcore/`` folder because we need access to osparc-simcore/packages + +LABEL maintainer=GitHK + +# for docker apt caching to work this needs to be added: [https://vsupalov.com/buildkit-cache-mount-dockerfile/] +RUN rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=private \ + set -eux && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + gosu \ + && apt-get clean -y \ + # verify that the binary works + && gosu nobody true + +# simcore-user uid=8004(scu) gid=8004(scu) groups=8004(scu) +ENV SC_USER_ID=8004 \ + SC_USER_NAME=scu \ + SC_BUILD_TARGET=base \ + SC_BOOT_MODE=default + +RUN adduser \ + --uid ${SC_USER_ID} \ + --disabled-password \ + --gecos "" \ + --shell /bin/sh \ + --home /home/${SC_USER_NAME} \ + ${SC_USER_NAME} + + +# Sets utf-8 encoding for Python et al +ENV LANG=C.UTF-8 + +# Turns off writing .pyc files; superfluous on an ephemeral container. +ENV PYTHONDONTWRITEBYTECODE=1 \ + VIRTUAL_ENV=/home/scu/.venv + +# Ensures that the python and pip executables used in the image will be +# those from our virtualenv. +ENV PATH="${VIRTUAL_ENV}/bin:$PATH" + +# -------------------------- Build stage ------------------- +# Installs build/package management tools and third party dependencies +# +# + /build WORKDIR +# +FROM base AS build + +ENV SC_BUILD_TARGET=build + +RUN --mount=type=cache,target=/var/cache/apt,sharing=private \ + set -eux \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential + +# install UV https://docs.astral.sh/uv/guides/integration/docker/#installing-uv +COPY --from=uv_build /uv /uvx /bin/ + +# NOTE: python virtualenv is used here such that installed +# packages may be moved to production image easily by copying the venv +RUN uv venv "${VIRTUAL_ENV}" + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --upgrade \ + wheel \ + setuptools + +WORKDIR /build + +# install base 3rd party dependencies + + + +# --------------------------Prod-depends-only stage ------------------- +# This stage is for production only dependencies that get partially wiped out afterwards (final docker image concerns) +# +# + /build +# + services/notifications [scu:scu] WORKDIR +# +FROM build AS prod-only-deps + +ENV SC_BUILD_TARGET=prod-only-deps + +WORKDIR /build/services/notifications + +RUN \ + --mount=type=bind,source=packages,target=/build/packages,rw \ + --mount=type=bind,source=services/notifications,target=/build/services/notifications,rw \ + --mount=type=cache,target=/root/.cache/uv \ + uv pip sync \ + requirements/prod.txt \ + && uv pip list + + +# --------------------------Production stage ------------------- +# Final cleanup up to reduce image size and startup setup +# Runs as scu (non-root user) +# +# + /home/scu $HOME = WORKDIR +# + services/notifications [scu:scu] +# +FROM base AS production + +ENV SC_BUILD_TARGET=production \ + SC_BOOT_MODE=production + +ENV PYTHONOPTIMIZE=TRUE +# https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode +ENV UV_COMPILE_BYTECODE=1 + +WORKDIR /home/scu + +# ensure home folder is read/writable for user scu +RUN chown -R scu /home/scu + +# Starting from clean base image, copies pre-installed virtualenv from prod-only-deps +COPY --chown=scu:scu --from=prod-only-deps ${VIRTUAL_ENV} ${VIRTUAL_ENV} + +# Copies booting scripts +COPY --chown=scu:scu services/notifications/docker services/notifications/docker +RUN chmod +x services/notifications/docker/*.sh + + +HEALTHCHECK --interval=30s \ + --timeout=20s \ + --start-period=30s \ + --retries=3 \ + CMD ["python3", "services/notifications/docker/healthcheck.py", "http://localhost:8000/"] + +EXPOSE 8000 + +ENTRYPOINT [ "/bin/sh", "services/notifications/docker/entrypoint.sh" ] +CMD ["/bin/sh", "services/notifications/docker/boot.sh"] + + +# --------------------------Development stage ------------------- +# Source code accessible in host but runs in container +# Runs as myu with same gid/uid as host +# Placed at the end to speed-up the build if images targeting production +# +# + /devel WORKDIR +# + services (mounted volume) +# +FROM build AS development + +ENV SC_BUILD_TARGET=development \ + SC_DEVEL_MOUNT=/devel/services/notifications + +WORKDIR /devel + +RUN chown -R scu:scu "${VIRTUAL_ENV}" + +EXPOSE 8000 +EXPOSE 3000 + +ENTRYPOINT ["/bin/sh", "services/notifications/docker/entrypoint.sh"] +CMD ["/bin/sh", "services/notifications/docker/boot.sh"] diff --git a/services/notifications/Makefile b/services/notifications/Makefile new file mode 100644 index 00000000000..bc14e6354c1 --- /dev/null +++ b/services/notifications/Makefile @@ -0,0 +1,18 @@ +# +# DEVELOPMENT recipes for notifications +# +include ../../scripts/common.Makefile +include ../../scripts/common-service.Makefile + + +.env-ignore: + $(APP_CLI_NAME) echo-dotenv > $@ + +.PHONY: openapi.json +openapi-specs: openapi.json +openapi.json: .env-ignore ## produces openapi.json + # generating openapi specs file (need to have the environment set for this) + @set -o allexport; \ + source $<; \ + set +o allexport; \ + python3 -c "import json; from $(APP_PACKAGE_NAME).main import *; print( json.dumps(the_app.openapi(), indent=2) )" > $@ diff --git a/services/notifications/VERSION b/services/notifications/VERSION new file mode 100644 index 00000000000..8acdd82b765 --- /dev/null +++ b/services/notifications/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/services/notifications/docker/boot.sh b/services/notifications/docker/boot.sh new file mode 100755 index 00000000000..bd14ca19221 --- /dev/null +++ b/services/notifications/docker/boot.sh @@ -0,0 +1,65 @@ +#!/bin/sh +set -o errexit +set -o nounset + +IFS=$(printf '\n\t') + +INFO="INFO: [$(basename "$0")] " + +echo "$INFO" "Booting in ${SC_BOOT_MODE} mode ..." +echo "$INFO" "User :$(id "$(whoami)")" +echo "$INFO" "Workdir : $(pwd)" + +# +# DEVELOPMENT MODE +# +# - prints environ info +# - installs requirements in mounted volume +# +if [ "${SC_BUILD_TARGET}" = "development" ]; then + echo "$INFO" "Environment :" + printenv | sed 's/=/: /' | sed 's/^/ /' | sort + echo "$INFO" "Python :" + python --version | sed 's/^/ /' + command -v python | sed 's/^/ /' + + cd services/notifications + uv pip --quiet sync requirements/dev.txt + cd - + echo "$INFO" "PIP :" + uv pip list +fi + +if [ "${SC_BOOT_MODE}" = "debug" ]; then + # NOTE: production does NOT pre-installs debugpy + uv pip install debugpy +fi + +# +# RUNNING application +# +APP_LOG_LEVEL=${LOGLEVEL:-${LOG_LEVEL:-${LOGLEVEL:-INFO}}} +NOTIFICATIONS_SERVER_REMOTE_DEBUG_PORT=3000 +SERVER_LOG_LEVEL=$(echo "${APP_LOG_LEVEL}" | tr '[:upper:]' '[:lower:]') +echo "$INFO" "Log-level app/server: $APP_LOG_LEVEL/$SERVER_LOG_LEVEL" + +if [ "${SC_BOOT_MODE}" = "debug" ]; then + reload_dir_packages=$(find /devel/packages -maxdepth 3 -type d -path "*/src/*" ! -path "*.*" -exec echo '--reload-dir {} \' \;) + + exec sh -c " + cd services/notifications/src/simcore_service_notifications && \ + python -m debugpy --listen 0.0.0.0:${NOTIFICATIONS_SERVER_REMOTE_DEBUG_PORT} -m uvicorn main:the_app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + $reload_dir_packages + --reload-dir . \ + --log-level \"${SERVER_LOG_LEVEL}\" + " +else + exec uvicorn simcore_service_notifications.main:the_app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level "${SERVER_LOG_LEVEL}" \ + --no-access-log +fi diff --git a/services/notifications/docker/entrypoint.sh b/services/notifications/docker/entrypoint.sh new file mode 100755 index 00000000000..1568d6affdc --- /dev/null +++ b/services/notifications/docker/entrypoint.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# +# - Executes *inside* of the container upon start as --user [default root] +# - Notice that the container *starts* as --user [default root] but +# *runs* as non-root user [scu] +# +set -o errexit +set -o nounset + +IFS=$(printf '\n\t') + +INFO="INFO: [$(basename "$0")] " +WARNING="WARNING: [$(basename "$0")] " +ERROR="ERROR: [$(basename "$0")] " + +echo "$INFO" "Entrypoint for stage ${SC_BUILD_TARGET} ..." +echo "$INFO" "User :$(id "$(whoami)")" +echo "$INFO" "Workdir : $(pwd)" +echo "$INFO" "User : $(id scu)" +echo "$INFO" "python : $(command -v python)" +echo "$INFO" "pip : $(command -v pip)" + +# +# DEVELOPMENT MODE +# - expects docker run ... -v $(pwd):$SC_DEVEL_MOUNT +# - mounts source folders +# - deduces host's uid/gip and assigns to user within docker +# +if [ "${SC_BUILD_TARGET}" = "development" ]; then + echo "$INFO" "development mode detected..." + stat "${SC_DEVEL_MOUNT}" >/dev/null 2>&1 || + (echo "$ERROR" "You must mount '$SC_DEVEL_MOUNT' to deduce user and group ids" && exit 1) + + echo "$INFO" "setting correct user id/group id..." + HOST_USERID=$(stat --format=%u "${SC_DEVEL_MOUNT}") + HOST_GROUPID=$(stat --format=%g "${SC_DEVEL_MOUNT}") + CONT_GROUPNAME=$(getent group "${HOST_GROUPID}" | cut --delimiter=: --fields=1) + if [ "$HOST_USERID" -eq 0 ]; then + echo "$WARNING" "Folder mounted owned by root user... adding $SC_USER_NAME to root..." + adduser "$SC_USER_NAME" root + else + echo "$INFO" "Folder mounted owned by user $HOST_USERID:$HOST_GROUPID-'$CONT_GROUPNAME'..." + # take host's credentials in $SC_USER_NAME + if [ -z "$CONT_GROUPNAME" ]; then + echo "$WARNING" "Creating new group grp$SC_USER_NAME" + CONT_GROUPNAME=grp$SC_USER_NAME + addgroup --gid "$HOST_GROUPID" "$CONT_GROUPNAME" + else + echo "$INFO" "group already exists" + fi + echo "$INFO" "Adding $SC_USER_NAME to group $CONT_GROUPNAME..." + adduser "$SC_USER_NAME" "$CONT_GROUPNAME" + + echo "$WARNING" "Changing ownership [this could take some time]" + echo "$INFO" "Changing $SC_USER_NAME:$SC_USER_NAME ($SC_USER_ID:$SC_USER_ID) to $SC_USER_NAME:$CONT_GROUPNAME ($HOST_USERID:$HOST_GROUPID)" + usermod --uid "$HOST_USERID" --gid "$HOST_GROUPID" "$SC_USER_NAME" + + echo "$INFO" "Changing group properties of files around from $SC_USER_ID to group $CONT_GROUPNAME" + find / -path /proc -prune -o -group "$SC_USER_ID" -exec chgrp --no-dereference "$CONT_GROUPNAME" {} \; + # change user property of files already around + echo "$INFO" "Changing ownership properties of files around from $SC_USER_ID to group $CONT_GROUPNAME" + find / -path /proc -prune -o -user "$SC_USER_ID" -exec chown --no-dereference "$SC_USER_NAME" {} \; + fi +fi + + +echo "$INFO Starting $* ..." +echo " $SC_USER_NAME rights : $(id "$SC_USER_NAME")" +echo " local dir : $(ls -al)" + +exec gosu "$SC_USER_NAME" "$@" diff --git a/services/notifications/docker/healthcheck.py b/services/notifications/docker/healthcheck.py new file mode 100755 index 00000000000..9e3f3274a29 --- /dev/null +++ b/services/notifications/docker/healthcheck.py @@ -0,0 +1,40 @@ +#!/bin/python +""" Healthcheck script to run inside docker + +Example of usage in a Dockerfile +``` + COPY --chown=scu:scu docker/healthcheck.py docker/healthcheck.py + HEALTHCHECK --interval=30s \ + --timeout=30s \ + --start-period=1s \ + --retries=3 \ + CMD python3 docker/healthcheck.py http://localhost:8000/ +``` + +Q&A: + 1. why not to use curl instead of a python script? + - SEE https://blog.sixeyed.com/docker-healthchecks-why-not-to-use-curl-or-iwr/ +""" +import os +import sys +from urllib.request import urlopen + +SUCCESS, UNHEALTHY = 0, 1 + +# Disabled if boots with debugger (e.g. debug, pdb-debug, debug-ptvsd, debugpy, etc) +ok = "debug" in os.environ.get("SC_BOOT_MODE", "").lower() + +# Queries host +# pylint: disable=consider-using-with +ok = ( + ok + or urlopen( + "{host}{baseurl}".format( + host=sys.argv[1], baseurl=os.environ.get("SIMCORE_NODE_BASEPATH", "") + ) # adds a base-path if defined in environ + ).getcode() + == 200 +) + + +sys.exit(SUCCESS if ok else UNHEALTHY) diff --git a/services/notifications/openapi.json b/services/notifications/openapi.json new file mode 100644 index 00000000000..501d6014b46 --- /dev/null +++ b/services/notifications/openapi.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "simcore-service-notifications", + "description": "Service used for sending notifications to users via different channels", + "version": "0.0.1" + }, + "servers": [ + { + "url": "/", + "description": "Default server: requests directed to serving url" + } + ], + "paths": { + "/": { + "get": { + "summary": "Check Service Health", + "operationId": "check_service_health__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheckGet" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "HealthCheckGet": { + "properties": { + "timestamp": { + "type": "string", + "title": "Timestamp" + } + }, + "type": "object", + "required": [ + "timestamp" + ], + "title": "HealthCheckGet", + "example": { + "timestamp": "simcore_service_directorv2.api.routes.health@2023-07-03T12:59:12.024551+00:00" + } + } + } + } +} diff --git a/services/notifications/requirements/Makefile b/services/notifications/requirements/Makefile new file mode 100644 index 00000000000..e1319af9d7f --- /dev/null +++ b/services/notifications/requirements/Makefile @@ -0,0 +1,10 @@ +# +# Targets to pip-compile requirements +# +include ../../../requirements/base.Makefile + +# Add here any extra explicit dependency: e.g. _migration.txt: _base.txt + +_base.in: constraints.txt +_test.in: constraints.txt +_tools.in: constraints.txt diff --git a/services/notifications/requirements/_base.in b/services/notifications/requirements/_base.in new file mode 100644 index 00000000000..d820f83a153 --- /dev/null +++ b/services/notifications/requirements/_base.in @@ -0,0 +1,21 @@ +# +# Specifies third-party dependencies for 'services/notifications/src' +# +# NOTE: ALL version constraints MUST be commented +--constraint ../../../requirements/constraints.txt +--constraint ./constraints.txt + +# intra-repo required dependencies +--requirement ../../../packages/common-library/requirements/_base.in +--requirement ../../../packages/models-library/requirements/_base.in +--requirement ../../../packages/settings-library/requirements/_base.in +--requirement ../../../packages/postgres-database/requirements/_base.in +# service-library[fastapi] +--requirement ../../../packages/service-library/requirements/_base.in +--requirement ../../../packages/service-library/requirements/_fastapi.in + + +fastapi +packaging +pydantic +uvicorn diff --git a/services/notifications/requirements/_base.txt b/services/notifications/requirements/_base.txt new file mode 100644 index 00000000000..3e5d8ce47dd --- /dev/null +++ b/services/notifications/requirements/_base.txt @@ -0,0 +1,552 @@ +aio-pika==9.5.5 + # via -r requirements/../../../packages/service-library/requirements/_base.in +aiocache==0.12.3 + # via -r requirements/../../../packages/service-library/requirements/_base.in +aiodebug==2.3.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +aiodocker==0.24.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +aiofiles==24.1.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.11.14 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # aiodocker +aiormq==6.8.1 + # via aio-pika +aiosignal==1.3.2 + # via aiohttp +alembic==1.15.1 + # via -r requirements/../../../packages/postgres-database/requirements/_base.in +annotated-types==0.7.0 + # via pydantic +anyio==4.9.0 + # via + # fast-depends + # faststream + # httpx + # starlette +arrow==1.3.0 + # via + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/_base.in +asgiref==3.8.1 + # via opentelemetry-instrumentation-asgi +asyncpg==0.30.0 + # via sqlalchemy +attrs==25.3.0 + # via + # aiohttp + # jsonschema + # referencing +certifi==2025.1.31 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # httpcore + # httpx + # requests +charset-normalizer==3.4.1 + # via requests +click==8.1.8 + # via + # typer + # uvicorn +deprecated==1.2.18 + # via + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-semantic-conventions +dnspython==2.7.0 + # via email-validator +email-validator==2.2.0 + # via pydantic +exceptiongroup==1.2.2 + # via aio-pika +fast-depends==2.4.12 + # via faststream +fastapi==0.115.12 + # via + # -r requirements/../../../packages/service-library/requirements/_fastapi.in + # -r requirements/_base.in + # fastapi-lifespan-manager +fastapi-lifespan-manager==0.1.4 + # via -r requirements/../../../packages/service-library/requirements/_fastapi.in +faststream==0.5.37 + # via -r requirements/../../../packages/service-library/requirements/_base.in +frozenlist==1.5.0 + # via + # aiohttp + # aiosignal +googleapis-common-protos==1.69.2 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +greenlet==3.1.1 + # via sqlalchemy +grpcio==1.71.0 + # via opentelemetry-exporter-otlp-proto-grpc +h11==0.14.0 + # via + # httpcore + # uvicorn +httpcore==1.0.7 + # via httpx +httpx==0.28.1 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/service-library/requirements/_fastapi.in +idna==3.10 + # via + # anyio + # email-validator + # httpx + # requests + # yarl +importlib-metadata==8.6.1 + # via opentelemetry-api +jsonschema==4.23.0 + # via + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in +jsonschema-specifications==2024.10.1 + # via jsonschema +mako==1.3.9 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # alembic +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 + # via mako +mdurl==0.1.2 + # via markdown-it-py +multidict==6.2.0 + # via + # aiohttp + # yarl +opentelemetry-api==1.31.1 + # via + # -r requirements/../../../packages/service-library/requirements/_base.in + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-asyncpg + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-logging + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-exporter-otlp==1.31.1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +opentelemetry-exporter-otlp-proto-common==1.31.1 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.31.1 + # via opentelemetry-exporter-otlp +opentelemetry-exporter-otlp-proto-http==1.31.1 + # via opentelemetry-exporter-otlp +opentelemetry-instrumentation==0.52b1 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-asyncpg + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-logging + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests +opentelemetry-instrumentation-asgi==0.52b1 + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-asyncpg==0.52b1 + # via -r requirements/../../../packages/postgres-database/requirements/_base.in +opentelemetry-instrumentation-fastapi==0.52b1 + # via -r requirements/../../../packages/service-library/requirements/_fastapi.in +opentelemetry-instrumentation-httpx==0.52b1 + # via -r requirements/../../../packages/service-library/requirements/_fastapi.in +opentelemetry-instrumentation-logging==0.52b1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +opentelemetry-instrumentation-redis==0.52b1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +opentelemetry-instrumentation-requests==0.52b1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +opentelemetry-proto==1.31.1 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.31.1 + # via + # -r requirements/../../../packages/service-library/requirements/_base.in + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-semantic-conventions==0.52b1 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-asyncpg + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-sdk +opentelemetry-util-http==0.52b1 + # via + # opentelemetry-instrumentation-asgi + # opentelemetry-instrumentation-fastapi + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-requests +orjson==3.10.16 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in +packaging==24.2 + # via + # -r requirements/_base.in + # opentelemetry-instrumentation +pamqp==3.3.0 + # via aiormq +prometheus-client==0.21.1 + # via + # -r requirements/../../../packages/service-library/requirements/_fastapi.in + # prometheus-fastapi-instrumentator +prometheus-fastapi-instrumentator==7.1.0 + # via -r requirements/../../../packages/service-library/requirements/_fastapi.in +propcache==0.3.1 + # via + # aiohttp + # yarl +protobuf==5.29.4 + # via + # googleapis-common-protos + # opentelemetry-proto +psutil==7.0.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +psycopg2-binary==2.9.10 + # via sqlalchemy +pycryptodome==3.22.0 + # via stream-zip +pydantic==2.11.0 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/postgres-database/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/_base.in + # fast-depends + # fastapi + # pydantic-extra-types + # pydantic-settings +pydantic-core==2.33.0 + # via pydantic +pydantic-extra-types==2.10.3 + # via + # -r requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/_base.in +pydantic-settings==2.7.0 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/_base.in +pygments==2.19.1 + # via rich +pyinstrument==5.0.1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +python-dateutil==2.9.0.post0 + # via arrow +python-dotenv==1.1.0 + # via pydantic-settings +pyyaml==6.0.2 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/service-library/requirements/_base.in +redis==5.2.1 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/service-library/requirements/_base.in +referencing==0.35.1 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # jsonschema + # jsonschema-specifications +requests==2.32.3 + # via opentelemetry-exporter-otlp-proto-http +rich==13.9.4 + # via + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/_base.in + # typer +rpds-py==0.24.0 + # via + # jsonschema + # referencing +shellingham==1.5.4 + # via typer +six==1.17.0 + # via python-dateutil +sniffio==1.3.1 + # via anyio +sqlalchemy==1.4.54 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # -r requirements/../../../packages/postgres-database/requirements/_base.in + # alembic +starlette==0.46.1 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # fastapi + # prometheus-fastapi-instrumentator +stream-zip==0.0.83 + # via -r requirements/../../../packages/service-library/requirements/_base.in +tenacity==9.0.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +toolz==1.0.0 + # via -r requirements/../../../packages/service-library/requirements/_base.in +tqdm==4.67.1 + # via -r requirements/../../../packages/service-library/requirements/_base.in +typer==0.15.2 + # via + # -r requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/_base.in + # -r requirements/../../../packages/settings-library/requirements/_base.in +types-python-dateutil==2.9.0.20241206 + # via arrow +typing-extensions==4.13.0 + # via + # aiodebug + # alembic + # anyio + # fastapi + # faststream + # opentelemetry-sdk + # pydantic + # pydantic-core + # pydantic-extra-types + # typer + # typing-inspection +typing-inspection==0.4.0 + # via pydantic +urllib3==2.3.0 + # via + # -c requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/postgres-database/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/models-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/service-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../packages/common-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../packages/settings-library/requirements/../../../requirements/constraints.txt + # -c requirements/../../../requirements/constraints.txt + # requests +uvicorn==0.34.0 + # via + # -r requirements/../../../packages/service-library/requirements/_fastapi.in + # -r requirements/_base.in +wrapt==1.17.2 + # via + # deprecated + # opentelemetry-instrumentation + # opentelemetry-instrumentation-httpx + # opentelemetry-instrumentation-redis +yarl==1.18.3 + # via + # -r requirements/../../../packages/postgres-database/requirements/_base.in + # -r requirements/../../../packages/service-library/requirements/_base.in + # aio-pika + # aiohttp + # aiormq +zipp==3.21.0 + # via importlib-metadata diff --git a/services/notifications/requirements/_test.in b/services/notifications/requirements/_test.in new file mode 100644 index 00000000000..0269b0db420 --- /dev/null +++ b/services/notifications/requirements/_test.in @@ -0,0 +1,24 @@ +# +# Specifies dependencies required to run 'services/notifications/test' +# both for unit and integration tests!! +# +--constraint ../../../requirements/constraints.txt +--constraint ./constraints.txt + +# Adds base AS CONSTRAINT specs, not requirement. +# - Resulting _text.txt is a frozen list of EXTRA packages for testing, besides _base.txt +# +--constraint _base.txt + + +asgi-lifespan +coverage +docker +faker +httpx +pytest +pytest-asyncio +pytest-cov +pytest-mock +pytest-runner +python-dotenv diff --git a/services/notifications/requirements/_test.txt b/services/notifications/requirements/_test.txt new file mode 100644 index 00000000000..39c817aa927 --- /dev/null +++ b/services/notifications/requirements/_test.txt @@ -0,0 +1,93 @@ +anyio==4.9.0 + # via + # -c requirements/_base.txt + # httpx +asgi-lifespan==2.1.0 + # via -r requirements/_test.in +certifi==2025.1.31 + # via + # -c requirements/../../../requirements/constraints.txt + # -c requirements/_base.txt + # httpcore + # httpx + # requests +charset-normalizer==3.4.1 + # via + # -c requirements/_base.txt + # requests +coverage==7.7.1 + # via + # -r requirements/_test.in + # pytest-cov +docker==7.1.0 + # via -r requirements/_test.in +faker==37.1.0 + # via -r requirements/_test.in +h11==0.14.0 + # via + # -c requirements/_base.txt + # httpcore +httpcore==1.0.7 + # via + # -c requirements/_base.txt + # httpx +httpx==0.28.1 + # via + # -c requirements/../../../requirements/constraints.txt + # -c requirements/_base.txt + # -r requirements/_test.in +idna==3.10 + # via + # -c requirements/_base.txt + # anyio + # httpx + # requests +iniconfig==2.1.0 + # via pytest +packaging==24.2 + # via + # -c requirements/_base.txt + # pytest +pluggy==1.5.0 + # via pytest +pytest==8.3.5 + # via + # -r requirements/_test.in + # pytest-asyncio + # pytest-cov + # pytest-mock +pytest-asyncio==0.23.8 + # via + # -c requirements/../../../requirements/constraints.txt + # -r requirements/_test.in +pytest-cov==6.0.0 + # via -r requirements/_test.in +pytest-mock==3.14.0 + # via -r requirements/_test.in +pytest-runner==6.0.1 + # via -r requirements/_test.in +python-dotenv==1.1.0 + # via + # -c requirements/_base.txt + # -r requirements/_test.in +requests==2.32.3 + # via + # -c requirements/_base.txt + # docker +sniffio==1.3.1 + # via + # -c requirements/_base.txt + # anyio + # asgi-lifespan +typing-extensions==4.13.0 + # via + # -c requirements/_base.txt + # anyio +tzdata==2025.2 + # via faker +urllib3==2.3.0 + # via + # -c requirements/../../../requirements/constraints.txt + # -c requirements/_base.txt + # docker + # requests diff --git a/services/notifications/requirements/_tools.in b/services/notifications/requirements/_tools.in new file mode 100644 index 00000000000..1def82c12a3 --- /dev/null +++ b/services/notifications/requirements/_tools.in @@ -0,0 +1,5 @@ +--constraint ../../../requirements/constraints.txt +--constraint _base.txt +--constraint _test.txt + +--requirement ../../../requirements/devenv.txt diff --git a/services/notifications/requirements/_tools.txt b/services/notifications/requirements/_tools.txt new file mode 100644 index 00000000000..4deff3bbf27 --- /dev/null +++ b/services/notifications/requirements/_tools.txt @@ -0,0 +1,82 @@ +astroid==3.3.9 + # via pylint +black==25.1.0 + # via -r requirements/../../../requirements/devenv.txt +build==1.2.2.post1 + # via pip-tools +bump2version==1.0.1 + # via -r requirements/../../../requirements/devenv.txt +cfgv==3.4.0 + # via pre-commit +click==8.1.8 + # via + # -c requirements/_base.txt + # black + # pip-tools +dill==0.3.9 + # via pylint +distlib==0.3.9 + # via virtualenv +filelock==3.18.0 + # via virtualenv +identify==2.6.9 + # via pre-commit +isort==6.0.1 + # via + # -r requirements/../../../requirements/devenv.txt + # pylint +mccabe==0.7.0 + # via pylint +mypy==1.15.0 + # via -r requirements/../../../requirements/devenv.txt +mypy-extensions==1.0.0 + # via + # black + # mypy +nodeenv==1.9.1 + # via pre-commit +packaging==24.2 + # via + # -c requirements/_base.txt + # -c requirements/_test.txt + # black + # build +pathspec==0.12.1 + # via black +pip==25.0.1 + # via pip-tools +pip-tools==7.4.1 + # via -r requirements/../../../requirements/devenv.txt +platformdirs==4.3.7 + # via + # black + # pylint + # virtualenv +pre-commit==4.2.0 + # via -r requirements/../../../requirements/devenv.txt +pylint==3.3.6 + # via -r requirements/../../../requirements/devenv.txt +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pyyaml==6.0.2 + # via + # -c requirements/../../../requirements/constraints.txt + # -c requirements/_base.txt + # pre-commit +ruff==0.11.2 + # via -r requirements/../../../requirements/devenv.txt +setuptools==78.1.0 + # via pip-tools +tomlkit==0.13.2 + # via pylint +typing-extensions==4.13.0 + # via + # -c requirements/_base.txt + # -c requirements/_test.txt + # mypy +virtualenv==20.29.3 + # via pre-commit +wheel==0.45.1 + # via pip-tools diff --git a/services/notifications/requirements/ci.txt b/services/notifications/requirements/ci.txt new file mode 100644 index 00000000000..21975753559 --- /dev/null +++ b/services/notifications/requirements/ci.txt @@ -0,0 +1,23 @@ +# Shortcut to install all packages for the contigous integration (CI) of 'services/notifications' +# +# - As ci.txt but w/ tests +# +# Usage: +# pip install -r requirements/ci.txt +# + +# installs base + tests requirements +--requirement _base.txt +--requirement _test.txt +--requirement _tools.txt + +# installs this repo's packages +simcore-common-library @ ../../packages/common-library/ +simcore-models-library @ ../../packages/models-library/ +simcore-postgres-database @ ../../packages/postgres-database/ +pytest-simcore @ ../../packages/pytest-simcore/ +simcore-service-library[fastapi] @ ../../packages/service-library/ +simcore-settings-library @ ../../packages/settings-library/ + +# installs current package +simcore-service-notifications @ . diff --git a/services/notifications/requirements/constraints.txt b/services/notifications/requirements/constraints.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/requirements/dev.txt b/services/notifications/requirements/dev.txt new file mode 100644 index 00000000000..4e73fc7a83a --- /dev/null +++ b/services/notifications/requirements/dev.txt @@ -0,0 +1,23 @@ +# Shortcut to install all packages needed to develop 'services/notifications' +# +# - As ci.txt but with current and repo packages in develop (edit) mode +# +# Usage: +# pip install -r requirements/dev.txt +# + +# installs base + tests + tools requirements +--requirement _base.txt +--requirement _test.txt +--requirement _tools.txt + +# installs this repo's packages +--editable ../../packages/common-library +--editable ../../packages/models-library +--editable ../../packages/postgres-database +--editable ../../packages/pytest-simcore +--editable ../../packages/service-library[fastapi] +--editable ../../packages/settings-library + +# installs current package +--editable . diff --git a/services/notifications/requirements/prod.txt b/services/notifications/requirements/prod.txt new file mode 100644 index 00000000000..f203156b59c --- /dev/null +++ b/services/notifications/requirements/prod.txt @@ -0,0 +1,20 @@ +# Shortcut to install 'services/notifications' for production +# +# - As ci.txt but w/o tests +# +# Usage: +# pip install -r requirements/prod.txt +# + +# installs base requirements +--requirement _base.txt + +# installs this repo's packages +simcore-common-library @ ../../packages/common-library/ +simcore-models-library @ ../../packages/models-library/ +simcore-postgres-database @ ../../packages/postgres-database/ +simcore-service-library[fastapi] @ ../../packages/service-library/ +simcore-settings-library @ ../../packages/settings-library/ + +# installs current package +simcore-service-notifications @ . diff --git a/services/notifications/setup.cfg b/services/notifications/setup.cfg new file mode 100644 index 00000000000..c3123ece6cf --- /dev/null +++ b/services/notifications/setup.cfg @@ -0,0 +1,15 @@ +[bumpversion] +current_version = 1 +commit = True +message = services/notifications version: {current_version} → {new_version} +tag = False +commit_args = --no-verify + +[bumpversion:file:VERSION] + +[tool:pytest] +asyncio_mode = auto + +[mypy] +plugins = + pydantic.mypy diff --git a/services/notifications/setup.py b/services/notifications/setup.py new file mode 100755 index 00000000000..c50365f3e25 --- /dev/null +++ b/services/notifications/setup.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +import re +import sys +from pathlib import Path + +from setuptools import find_packages, setup + + +def read_reqs(reqs_path: Path) -> set[str]: + return { + r + for r in re.findall( + r"(^[^#\n-][\w\[,\]]+[-~>=<.\w]*)", + reqs_path.read_text(), + re.MULTILINE, + ) + if isinstance(r, str) + } + + +CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent + +NAME = "simcore-service-notifications" +VERSION = (CURRENT_DIR / "VERSION").read_text().strip() +AUTHORS = ("Andrei Neagu (GitHK)",) +DESCRIPTION = "Service used for sending notifications to users via different channels" + +PROD_REQUIREMENTS = tuple( + read_reqs(CURRENT_DIR / "requirements" / "_base.txt") + | { + "simcore-models-library", + "simcore-service-library", + "simcore-settings-library", + } +) + +TEST_REQUIREMENTS = tuple(read_reqs(CURRENT_DIR / "requirements" / "_test.txt")) + +SETUP = { + "name": NAME, + "version": VERSION, + "author": AUTHORS, + "description": DESCRIPTION, + "license": "MIT license", + "python_requires": "~=3.11", + "packages": find_packages(where="src"), + "package_dir": { + "": "src", + }, + "include_package_data": True, + "install_requires": PROD_REQUIREMENTS, + "test_suite": "tests", + "tests_require": TEST_REQUIREMENTS, + "extras_require": {"test": TEST_REQUIREMENTS}, + "entry_points": { + "console_scripts": [ + "simcore-service-notifications = simcore_service_notifications.cli:main", + "simcore-service = simcore_service_notifications.cli:main", + ], + }, +} + +if __name__ == "__main__": + setup(**SETUP) diff --git a/services/notifications/src/simcore_service_notifications/__init__.py b/services/notifications/src/simcore_service_notifications/__init__.py new file mode 100644 index 00000000000..94fc632e7af --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/__init__.py @@ -0,0 +1 @@ +from ._meta import __version__ diff --git a/services/notifications/src/simcore_service_notifications/_meta.py b/services/notifications/src/simcore_service_notifications/_meta.py new file mode 100644 index 00000000000..1f054b45d73 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/_meta.py @@ -0,0 +1,36 @@ +"""Application's metadata""" + +from importlib.metadata import distribution, version +from typing import Final + +from packaging.version import Version + +_current_distribution = distribution("simcore-service-notifications") +__version__: str = version("simcore-service-notifications") + + +APP_NAME: Final[str] = _current_distribution.metadata["Name"] +VERSION: Final[Version] = Version(__version__) +API_VTAG: str = f"v{VERSION.major}" + + +def get_summary() -> str: + return _current_distribution.metadata.get_all("Summary", [""])[-1] + + +SUMMARY: Final[str] = get_summary() + + +APP_STARTED_BANNER_MSG = rf""" + _______ _ ___ _ _ +(_______) _ (_) / __(_) _ (_) + _ _ ___ _| |_ _ _| |__ _ ____ _____ _| |_ _ ___ ____ ___ +| | | |/ _ (_ _| (_ __| |/ ___(____ (_ _| |/ _ \| _ \ /___) +| | | | |_| || |_| | | | | ( (___/ ___ | | |_| | |_| | | | |___ | +|_| |_|\___/ \__|_| |_| |_|\____\_____| \__|_|\___/|_| |_(___/ + {API_VTAG}""" + + +APP_FINISHED_BANNER_MSG = "{:=^100}".format( + f"🎉 App {APP_NAME}=={VERSION} shutdown completed 🎉" +) diff --git a/services/notifications/src/simcore_service_notifications/api/__init__.py b/services/notifications/src/simcore_service_notifications/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/src/simcore_service_notifications/api/rest/__init__.py b/services/notifications/src/simcore_service_notifications/api/rest/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/src/simcore_service_notifications/api/rest/_health.py b/services/notifications/src/simcore_service_notifications/api/rest/_health.py new file mode 100644 index 00000000000..5f38f21d5e0 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/api/rest/_health.py @@ -0,0 +1,33 @@ +from typing import Annotated + +import arrow +from fastapi import APIRouter, Depends +from models_library.api_schemas__common.health import HealthCheckGet +from models_library.errors import ( + POSRGRES_DATABASE_UNHEALTHY_MSG, + RABBITMQ_CLIENT_UNHEALTHY_MSG, +) +from servicelib.rabbitmq import RabbitMQClient + +from ...clients.postgres import PostgresLiveness +from .dependencies import get_postgres_liveness, get_rabbitmq_client + +router = APIRouter() + + +class HealthCheckError(RuntimeError): + """Failed a health check""" + + +@router.get("/", response_model=HealthCheckGet) +async def check_service_health( + rabbitmq_client: Annotated[RabbitMQClient, Depends(get_rabbitmq_client)], + postgres_liveness: Annotated[PostgresLiveness, Depends(get_postgres_liveness)], +): + if not rabbitmq_client.healthy: + raise HealthCheckError(RABBITMQ_CLIENT_UNHEALTHY_MSG) + + if not postgres_liveness.is_responsive: + raise HealthCheckError(POSRGRES_DATABASE_UNHEALTHY_MSG) + + return HealthCheckGet(timestamp=f"{__name__}@{arrow.utcnow().datetime.isoformat()}") diff --git a/services/notifications/src/simcore_service_notifications/api/rest/dependencies.py b/services/notifications/src/simcore_service_notifications/api/rest/dependencies.py new file mode 100644 index 00000000000..962154ea9f7 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/api/rest/dependencies.py @@ -0,0 +1,26 @@ +"""Free functions to inject dependencies in routes handlers""" + +from typing import Annotated, cast + +from fastapi import Depends, FastAPI, Request +from servicelib.rabbitmq._client_rpc import RabbitMQRPCClient + +from ...clients.postgres import PostgresLiveness +from ...clients.postgres import get_postgres_liveness as get_postgress_db_liveness + + +def get_application(request: Request) -> FastAPI: + return cast(FastAPI, request.app) + + +def get_rabbitmq_client( + app: Annotated[FastAPI, Depends(get_application)], +) -> RabbitMQRPCClient: + assert isinstance(app.state.rabbitmq_rpc_server, RabbitMQRPCClient) # nosec + return app.state.rabbitmq_rpc_server + + +def get_postgres_liveness( + app: Annotated[FastAPI, Depends(get_application)], +) -> PostgresLiveness: + return get_postgress_db_liveness(app) diff --git a/services/notifications/src/simcore_service_notifications/api/rest/routing.py b/services/notifications/src/simcore_service_notifications/api/rest/routing.py new file mode 100644 index 00000000000..5fae78c42ce --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/api/rest/routing.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, HTTPException +from servicelib.fastapi.exceptions_utils import ( + handle_errors_as_500, + http_exception_as_json_response, +) + +from ._health import router as health_router + + +def initialize_rest_api(app: FastAPI) -> None: + app.include_router(health_router) + + app.add_exception_handler(Exception, handle_errors_as_500) + app.add_exception_handler(HTTPException, http_exception_as_json_response) diff --git a/services/notifications/src/simcore_service_notifications/api/rpc/__init__.py b/services/notifications/src/simcore_service_notifications/api/rpc/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/src/simcore_service_notifications/api/rpc/routing.py b/services/notifications/src/simcore_service_notifications/api/rpc/routing.py new file mode 100644 index 00000000000..c43bcdb7c05 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/api/rpc/routing.py @@ -0,0 +1,23 @@ +from collections.abc import AsyncIterator + +from fastapi import FastAPI +from fastapi_lifespan_manager import State +from models_library.api_schemas_notifications import NOTIFICATIONS_RPC_NAMESPACE +from servicelib.rabbitmq import RPCRouter + +from ...clients.rabbitmq import get_rabbitmq_rpc_server + +ROUTERS: list[RPCRouter] = [ + # import and use all routers here +] + + +async def rpc_api_routes_lifespan(app: FastAPI) -> AsyncIterator[State]: + rpc_server = get_rabbitmq_rpc_server(app) + + for router in ROUTERS: + await rpc_server.register_router( + router, NOTIFICATIONS_RPC_NAMESPACE, app + ) # pragma: no cover + + yield {} diff --git a/services/notifications/src/simcore_service_notifications/cli.py b/services/notifications/src/simcore_service_notifications/cli.py new file mode 100644 index 00000000000..13bee086290 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/cli.py @@ -0,0 +1,82 @@ +import logging +import os + +import typer +from settings_library.postgres import PostgresSettings +from settings_library.rabbit import RabbitSettings +from settings_library.utils_cli import ( + create_settings_command, + create_version_callback, + print_as_envfile, +) + +from ._meta import APP_NAME, __version__ +from .core.settings import ApplicationSettings + +log = logging.getLogger(__name__) + +main = typer.Typer( + name=APP_NAME, + pretty_exceptions_enable=False, + pretty_exceptions_show_locals=False, +) + +main.command()(create_settings_command(settings_cls=ApplicationSettings, logger=log)) +main.callback()(create_version_callback(__version__)) + + +@main.command() +def echo_dotenv(ctx: typer.Context, *, minimal: bool = True) -> None: + """Generates and displays a valid environment variables file (also known as dot-envfile) + + Usage: + $ simcore-service echo-dotenv > .env + $ cat .env + $ set -o allexport; source .env; set +o allexport + """ + assert ctx # nosec + + # NOTE: we normally DO NOT USE `os.environ` to capture env vars but this is a special case + # The idea here is to have a command that can generate a **valid** `.env` file that can be used + # to initialized the app. For that reason we fill required fields of the `ApplicationSettings` with + # "fake" but valid values (e.g. generating a password or adding tags as `replace-with-api-key). + # Nonetheless, if the caller of this CLI has already some **valid** env vars in the environment we want to use them ... + # and that is why we use `os.environ`. + + settings = ApplicationSettings.create_from_envs( + SC_BOOT_MODE="default", + NOTIFICATIONS_POSTGRES=os.environ.get( + "NOTIFICATIONS_POSTGRES", + PostgresSettings.create_from_envs( + POSTGRES_HOST=os.environ.get( + "POSTGRES_HOST", "replace-with-postgres-host" + ), + POSTGRES_USER=os.environ.get( + "POSTGRES_USER", "replace-with-postgres-user" + ), + POSTGRES_DB=os.environ.get("POSTGRES_DB", "replace-with-postgres-db"), + POSTGRES_PASSWORD=os.environ.get( + "POSTGRES_PASSWORD", "replace-with-postgres-password" + ), + ), + ), + NOTIFICATIONS_RABBITMQ=os.environ.get( + "NOTIFICATIONS_RABBITMQ", + RabbitSettings.create_from_envs( + RABBIT_HOST=os.environ.get("RABBIT_HOST", "replace-with-rabbit-host"), + RABBIT_SECURE=os.environ.get("RABBIT_SECURE", "True"), + RABBIT_USER=os.environ.get("RABBIT_USER", "replace-with-rabbit-user"), + RABBIT_PASSWORD=os.environ.get( + "RABBIT_PASSWORD", "replace-with-rabbit-password" + ), + ), + ), + ) + + print_as_envfile( + settings, + compact=False, + verbose=True, + show_secrets=True, + exclude_unset=minimal, + ) diff --git a/services/notifications/src/simcore_service_notifications/clients/__init__.py b/services/notifications/src/simcore_service_notifications/clients/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/src/simcore_service_notifications/clients/postgres/__init__.py b/services/notifications/src/simcore_service_notifications/clients/postgres/__init__.py new file mode 100644 index 00000000000..e0883fba1ce --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/clients/postgres/__init__.py @@ -0,0 +1,33 @@ +import logging +from collections.abc import AsyncIterator + +from fastapi import FastAPI +from fastapi_lifespan_manager import State +from servicelib.fastapi.postgres_lifespan import PostgresLifespanState +from servicelib.logging_utils import log_context + +from ._liveness import PostgresLiveness + +_logger = logging.getLogger(__name__) + + +async def postgres_lifespan(app: FastAPI, state: State) -> AsyncIterator[State]: + app.state.engine = state[PostgresLifespanState.POSTGRES_ASYNC_ENGINE] + + app.state.postgres_liveness = PostgresLiveness(app) + + with log_context(_logger, logging.INFO, msg="setup postgres health"): + await app.state.postgres_liveness.setup() + + yield {} + + with log_context(_logger, logging.INFO, msg="teardown postgres health"): + await app.state.postgres_liveness.teardown() + + +def get_postgres_liveness(app: FastAPI) -> PostgresLiveness: + assert isinstance(app.state.postgres_liveness, PostgresLiveness) # nosec + return app.state.postgres_liveness + + +__all__: tuple[str, ...] = ("PostgresLiveness",) diff --git a/services/notifications/src/simcore_service_notifications/clients/postgres/_liveness.py b/services/notifications/src/simcore_service_notifications/clients/postgres/_liveness.py new file mode 100644 index 00000000000..57bc7a40076 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/clients/postgres/_liveness.py @@ -0,0 +1,43 @@ +import logging +from asyncio import Task +from datetime import timedelta +from typing import Final + +from fastapi import FastAPI +from models_library.healthchecks import IsResponsive, LivenessResult +from servicelib.async_utils import cancel_wait_task +from servicelib.background_task import create_periodic_task +from servicelib.db_asyncpg_utils import check_postgres_liveness +from servicelib.fastapi.db_asyncpg_engine import get_engine +from servicelib.logging_utils import log_catch + +_logger = logging.getLogger(__name__) + +_LVENESS_CHECK_INTERVAL: Final[timedelta] = timedelta(seconds=10) + + +class PostgresLiveness: + def __init__(self, app: FastAPI) -> None: + self.app = app + + self._liveness_result: LivenessResult = IsResponsive(elapsed=timedelta(0)) + self._task: Task | None = None + + async def _check_task(self) -> None: + self._liveness_result = await check_postgres_liveness(get_engine(self.app)) + + @property + def is_responsive(self) -> bool: + return isinstance(self._liveness_result, IsResponsive) + + async def setup(self) -> None: + self._task = create_periodic_task( + self._check_task, + interval=_LVENESS_CHECK_INTERVAL, + task_name="posgress_liveness_check", + ) + + async def teardown(self) -> None: + if self._task is not None: + with log_catch(_logger, reraise=False): + await cancel_wait_task(self._task, max_delay=5) diff --git a/services/notifications/src/simcore_service_notifications/clients/rabbitmq.py b/services/notifications/src/simcore_service_notifications/clients/rabbitmq.py new file mode 100644 index 00000000000..3c205c40162 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/clients/rabbitmq.py @@ -0,0 +1,30 @@ +from collections.abc import AsyncIterator +from typing import cast + +from fastapi import FastAPI +from fastapi_lifespan_manager import State +from servicelib.rabbitmq import RabbitMQRPCClient, wait_till_rabbitmq_responsive +from settings_library.rabbit import RabbitSettings + +from ..core.settings import ApplicationSettings + + +async def rabbitmq_lifespan(app: FastAPI) -> AsyncIterator[State]: + settings: ApplicationSettings = app.state.settings + rabbit_settings: RabbitSettings = settings.NOTIFICATIONS_RABBITMQ + app.state.rabbitmq_rpc_server = None + + await wait_till_rabbitmq_responsive(rabbit_settings.dsn) + + app.state.rabbitmq_rpc_server = await RabbitMQRPCClient.create( + client_name="dynamic_scheduler_rpc_server", settings=rabbit_settings + ) + + yield {} + + await app.state.rabbitmq_rpc_server.close() + + +def get_rabbitmq_rpc_server(app: FastAPI) -> RabbitMQRPCClient: + assert app.state.rabbitmq_rpc_server # nosec + return cast(RabbitMQRPCClient, app.state.rabbitmq_rpc_server) diff --git a/services/notifications/src/simcore_service_notifications/core/__init__.py b/services/notifications/src/simcore_service_notifications/core/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/src/simcore_service_notifications/core/application.py b/services/notifications/src/simcore_service_notifications/core/application.py new file mode 100644 index 00000000000..3b7cfe92c26 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/core/application.py @@ -0,0 +1,62 @@ +import logging + +from fastapi import FastAPI +from servicelib.fastapi.openapi import ( + get_common_oas_options, + override_fastapi_openapi_method, +) +from servicelib.fastapi.prometheus_instrumentation import ( + initialize_prometheus_instrumentation, +) +from servicelib.fastapi.tracing import initialize_tracing +from servicelib.logging_utils import config_all_loggers + +from .._meta import API_VTAG, APP_NAME, SUMMARY, VERSION +from ..api.rest.routing import initialize_rest_api +from . import events +from .settings import ApplicationSettings + +_logger = logging.getLogger(__name__) + + +def _initialise_logger(settings: ApplicationSettings): + # SEE https://github.com/ITISFoundation/osparc-simcore/issues/3148 + logging.basicConfig(level=settings.LOG_LEVEL.value) # NOSONAR + logging.root.setLevel(settings.LOG_LEVEL.value) + config_all_loggers( + log_format_local_dev_enabled=settings.NOTIFICATIONS_VOLUMES_LOG_FORMAT_LOCAL_DEV_ENABLED, + logger_filter_mapping=settings.NOTIFICATIONS_VOLUMES_LOG_FILTER_MAPPING, + tracing_settings=settings.NOTIFICATIONS_TRACING, + ) + + +def create_app() -> FastAPI: + settings = ApplicationSettings.create_from_envs() + _logger.debug(settings.model_dump_json(indent=2)) + + _initialise_logger(settings) + + assert settings.SC_BOOT_MODE # nosec + app = FastAPI( + debug=settings.SC_BOOT_MODE.is_devel_mode(), + title=APP_NAME, + description=SUMMARY, + version=f"{VERSION}", + openapi_url=f"/api/{API_VTAG}/openapi.json", + lifespan=events.create_app_lifespan(), + **get_common_oas_options(is_devel_mode=settings.SC_BOOT_MODE.is_devel_mode()), + ) + override_fastapi_openapi_method(app) + app.state.settings = settings + + initialize_rest_api(app) + + if settings.NOTIFICATIONS_PROMETHEUS_INSTRUMENTATION_ENABLED: + initialize_prometheus_instrumentation(app) + + if settings.NOTIFICATIONS_TRACING: + initialize_tracing( + app, settings.NOTIFICATIONS_TRACING, APP_NAME + ) # pragma: no cover + + return app diff --git a/services/notifications/src/simcore_service_notifications/core/events.py b/services/notifications/src/simcore_service_notifications/core/events.py new file mode 100644 index 00000000000..65a828744e5 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/core/events.py @@ -0,0 +1,58 @@ +from collections.abc import AsyncIterator + +from fastapi import FastAPI +from fastapi_lifespan_manager import LifespanManager, State +from servicelib.fastapi.postgres_lifespan import ( + create_postgres_database_input_state, + postgres_database_lifespan, +) +from servicelib.fastapi.prometheus_instrumentation import ( + create_prometheus_instrumentationmain_input_state, + prometheus_instrumentation_lifespan, +) + +from .._meta import APP_FINISHED_BANNER_MSG, APP_STARTED_BANNER_MSG +from ..api.rpc.routing import rpc_api_routes_lifespan +from ..clients.postgres import postgres_lifespan +from ..clients.rabbitmq import rabbitmq_lifespan +from .settings import ApplicationSettings + + +async def _banner_lifespan(app: FastAPI) -> AsyncIterator[State]: + _ = app + print(APP_STARTED_BANNER_MSG, flush=True) # noqa: T201 + yield {} + print(APP_FINISHED_BANNER_MSG, flush=True) # noqa: T201 + + +async def _settings_lifespan(app: FastAPI) -> AsyncIterator[State]: + settings: ApplicationSettings = app.state.settings + yield { + **create_postgres_database_input_state(settings.NOTIFICATIONS_POSTGRES), + **create_prometheus_instrumentationmain_input_state( + enabled=settings.NOTIFICATIONS_PROMETHEUS_INSTRUMENTATION_ENABLED + ), + } + + +def create_app_lifespan(): + # WARNING: order matters + app_lifespan = LifespanManager() + app_lifespan.add(_settings_lifespan) + + # - postgres + app_lifespan.add(postgres_database_lifespan) + app_lifespan.add(postgres_lifespan) + + # - rabbitmq + app_lifespan.add(rabbitmq_lifespan) + + # - rpc api routes + app_lifespan.add(rpc_api_routes_lifespan) + + # - prometheus instrumentation + app_lifespan.add(prometheus_instrumentation_lifespan) + + app_lifespan.add(_banner_lifespan) + + return app_lifespan diff --git a/services/notifications/src/simcore_service_notifications/core/settings.py b/services/notifications/src/simcore_service_notifications/core/settings.py new file mode 100644 index 00000000000..6f7e13a546e --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/core/settings.py @@ -0,0 +1,81 @@ +from typing import Annotated + +from common_library.basic_types import DEFAULT_FACTORY +from models_library.basic_types import BootModeEnum, LogLevel +from pydantic import AliasChoices, Field, field_validator +from servicelib.logging_utils_filtering import LoggerName, MessageSubstring +from settings_library.base import BaseCustomSettings +from settings_library.postgres import PostgresSettings +from settings_library.rabbit import RabbitSettings +from settings_library.tracing import TracingSettings +from settings_library.utils_logging import MixinLoggingSettings + + +class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings): + LOG_LEVEL: Annotated[ + LogLevel, + Field( + validation_alias=AliasChoices( + "NOTIFICATIONS_LOGLEVEL", + "LOG_LEVEL", + "LOGLEVEL", + ), + ), + ] = LogLevel.WARNING + + SC_BOOT_MODE: BootModeEnum | None + + NOTIFICATIONS_VOLUMES_LOG_FORMAT_LOCAL_DEV_ENABLED: Annotated[ + bool, + Field( + validation_alias=AliasChoices( + "NOTIFICATIONS_VOLUMES_LOG_FORMAT_LOCAL_DEV_ENABLED", + "LOG_FORMAT_LOCAL_DEV_ENABLED", + ), + description=( + "Enables local development log format. WARNING: make sure it is " + "disabled if you want to have structured logs!" + ), + ), + ] = False + + NOTIFICATIONS_VOLUMES_LOG_FILTER_MAPPING: Annotated[ + dict[LoggerName, list[MessageSubstring]], + Field( + default_factory=dict, + validation_alias=AliasChoices( + "NOTIFICATIONS_VOLUMES_LOG_FILTER_MAPPING", "LOG_FILTER_MAPPING" + ), + description="is a dictionary that maps specific loggers (such as 'uvicorn.access' or 'gunicorn.access') to a list of log message patterns that should be filtered out.", + ), + ] = DEFAULT_FACTORY + + NOTIFICATIONS_RABBITMQ: Annotated[ + RabbitSettings, + Field( + description="settings for service/rabbitmq", + json_schema_extra={"auto_default_from_env": True}, + ), + ] + + NOTIFICATIONS_POSTGRES: Annotated[ + PostgresSettings, + Field( + json_schema_extra={"auto_default_from_env": True}, + ), + ] + + NOTIFICATIONS_TRACING: Annotated[ + TracingSettings | None, + Field( + description="settings for opentelemetry tracing", + json_schema_extra={"auto_default_from_env": True}, + ), + ] + + NOTIFICATIONS_PROMETHEUS_INSTRUMENTATION_ENABLED: bool = True + + @field_validator("LOG_LEVEL") + @classmethod + def valid_log_level(cls, value) -> LogLevel: + return LogLevel(cls.validate_log_level(value)) diff --git a/services/notifications/src/simcore_service_notifications/main.py b/services/notifications/src/simcore_service_notifications/main.py new file mode 100644 index 00000000000..8b2e0ed3196 --- /dev/null +++ b/services/notifications/src/simcore_service_notifications/main.py @@ -0,0 +1,3 @@ +from simcore_service_notifications.core.application import create_app + +the_app = create_app() diff --git a/services/notifications/src/simcore_service_notifications/services/__init__.py b/services/notifications/src/simcore_service_notifications/services/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/notifications/tests/conftest.py b/services/notifications/tests/conftest.py new file mode 100644 index 00000000000..a310c11b5d5 --- /dev/null +++ b/services/notifications/tests/conftest.py @@ -0,0 +1,26 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + + +import pytest +from models_library.basic_types import BootModeEnum +from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict + +pytest_plugins = [ + "pytest_simcore.docker_compose", + "pytest_simcore.docker_swarm", + "pytest_simcore.postgres_service", + "pytest_simcore.rabbit_service", + "pytest_simcore.repository_paths", +] + + +@pytest.fixture +def mock_environment(monkeypatch: pytest.MonkeyPatch) -> EnvVarsDict: + return setenvs_from_dict( + monkeypatch, + { + "LOGLEVEL": "DEBUG", + "SC_BOOT_MODE": BootModeEnum.DEBUG, + }, + ) diff --git a/services/notifications/tests/unit/conftest.py b/services/notifications/tests/unit/conftest.py new file mode 100644 index 00000000000..e1f57c7c5c7 --- /dev/null +++ b/services/notifications/tests/unit/conftest.py @@ -0,0 +1,48 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +from collections.abc import AsyncIterator + +import pytest +import sqlalchemy as sa +from asgi_lifespan import LifespanManager +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict +from settings_library.rabbit import RabbitSettings +from simcore_service_notifications.core.application import create_app + + +@pytest.fixture +def service_env( + monkeypatch: pytest.MonkeyPatch, + mock_environment: EnvVarsDict, + rabbit_service: RabbitSettings, + postgres_db: sa.engine.Engine, + postgres_env_vars_dict: EnvVarsDict, +) -> EnvVarsDict: + return setenvs_from_dict( + monkeypatch, + { + **mock_environment, + "RABBIT_HOST": rabbit_service.RABBIT_HOST, + "RABBIT_PASSWORD": rabbit_service.RABBIT_PASSWORD.get_secret_value(), + "RABBIT_PORT": f"{rabbit_service.RABBIT_PORT}", + "RABBIT_SECURE": f"{rabbit_service.RABBIT_SECURE}", + "RABBIT_USER": rabbit_service.RABBIT_USER, + **postgres_env_vars_dict, + }, + ) + + +@pytest.fixture +async def initialized_app(service_env: EnvVarsDict) -> AsyncIterator[FastAPI]: + app: FastAPI = create_app() + + async with LifespanManager(app, startup_timeout=30, shutdown_timeout=30): + yield app + + +@pytest.fixture +def test_client(initialized_app: FastAPI) -> TestClient: + return TestClient(initialized_app) diff --git a/services/notifications/tests/unit/test_api_rest__health.py b/services/notifications/tests/unit/test_api_rest__health.py new file mode 100644 index 00000000000..ba418fe7bc3 --- /dev/null +++ b/services/notifications/tests/unit/test_api_rest__health.py @@ -0,0 +1,56 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + + +import pytest +from fastapi import status +from fastapi.testclient import TestClient +from models_library.api_schemas__common.health import HealthCheckGet +from models_library.errors import ( + POSRGRES_DATABASE_UNHEALTHY_MSG, + RABBITMQ_CLIENT_UNHEALTHY_MSG, +) +from models_library.healthchecks import IsNonResponsive +from pytest_mock import MockerFixture +from simcore_service_notifications.api.rest._health import HealthCheckError + +pytest_simcore_core_services_selection = [ + "postgres", + "rabbit", +] + + +def test_health_ok(test_client: TestClient): + response = test_client.get("/") + assert response.status_code == status.HTTP_200_OK + assert HealthCheckGet.model_validate(response.json()) + + +@pytest.fixture +def mock_postgres_liveness(mocker: MockerFixture, test_client: TestClient) -> None: + mocker.patch.object( + test_client.app.state.postgres_liveness, + "_liveness_result", + new=IsNonResponsive(reason="fake"), + ) + + +def test_health_postgres_unhealthy( + mock_postgres_liveness: None, test_client: TestClient +): + with pytest.raises(HealthCheckError) as exc: + test_client.get("/") + assert POSRGRES_DATABASE_UNHEALTHY_MSG in f"{exc.value}" + + +@pytest.fixture +def mock_rabbit_healthy(mocker: MockerFixture, test_client: TestClient) -> None: + mocker.patch.object( + test_client.app.state.rabbitmq_rpc_server, "_healthy_state", new=False + ) + + +def test_health_rabbit_unhealthy(mock_rabbit_healthy: None, test_client: TestClient): + with pytest.raises(HealthCheckError) as exc: + test_client.get("/") + assert RABBITMQ_CLIENT_UNHEALTHY_MSG in f"{exc.value}" diff --git a/services/notifications/tests/unit/test_cli.py b/services/notifications/tests/unit/test_cli.py new file mode 100644 index 00000000000..bcfc7925b61 --- /dev/null +++ b/services/notifications/tests/unit/test_cli.py @@ -0,0 +1,57 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +import traceback + +import pytest +from click.testing import Result +from pytest_simcore.helpers.monkeypatch_envs import ( + EnvVarsDict, + load_dotenv, + setenvs_from_dict, +) +from simcore_service_notifications.cli import main +from simcore_service_notifications.core.settings import ApplicationSettings +from typer.testing import CliRunner + +pytest_simcore_core_services_selection = [ + "postgres", + "rabbit", +] + + +@pytest.fixture +def cli_runner(service_env: EnvVarsDict) -> CliRunner: + return CliRunner() + + +def _format_cli_error(result: Result) -> str: + assert result.exception + tb_message = "\n".join(traceback.format_tb(result.exception.__traceback__)) + return f"Below exception was raised by the cli:\n{tb_message}" + + +async def test_process_cli_options(cli_runner: CliRunner): + result = cli_runner.invoke(main, ["--help"]) + print(result.stdout) + assert result.exit_code == 0, _format_cli_error(result) + + result = cli_runner.invoke(main, ["settings"]) + print(result.stdout) + assert result.exit_code == 0, _format_cli_error(result) + + result = cli_runner.invoke(main, ["--version"]) + print(result.stdout) + assert result.exit_code == 0, _format_cli_error(result) + + +async def test_echo_dotenv(cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch): + result = cli_runner.invoke(main, ["echo-dotenv"]) + print(result.stdout) + assert result.exit_code == 0, _format_cli_error(result) + + environs = load_dotenv(result.stdout) + + with monkeypatch.context() as patch: + setenvs_from_dict(patch, environs) + assert ApplicationSettings.create_from_envs()