Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/instructions/python.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Ensure compatibility with the following library versions:

* Use `f-string` formatting for all string interpolation except for logging message strings.
* Use **relative imports** within the same package/module.
- For imports within the same repository/project, always use relative imports (e.g., `from ..constants import APP_SETTINGS_KEY` instead of `from simcore_service_webserver.constants import APP_SETTINGS_KEY`)
- Use absolute imports only for external libraries and packages
* Place **all imports at the top** of the file.
* Document functions when the code is not self-explanatory or if asked explicitly.

Expand All @@ -44,6 +46,18 @@ Ensure compatibility with the following library versions:
* Prefer `json_dumps` / `json_loads` from `common_library.json_serialization` instead of the built-in `json.dumps` / `json.loads`.
* When using Pydantic models, prefer methods like `model.model_dump_json()` for serialization.

### 7. **Running tests**
### 7. **aiohttp Framework**

* **Application Keys**: Always use `web.AppKey` for type-safe application storage instead of string keys
- Define keys with specific types: `APP_MY_KEY: Final = web.AppKey("APP_MY_KEY", MySpecificType)`
- Use precise types instead of generic `object` when the actual type is known
- Example: `APP_SETTINGS_KEY: Final = web.AppKey("APP_SETTINGS_KEY", ApplicationSettings)`
- Store and retrieve: `app[APP_MY_KEY] = value` and `data = app[APP_MY_KEY]`
* **Request Keys**: Use `web.AppKey` for request storage as well for consistency and type safety
* **Middleware**: Follow the repository's middleware patterns for cross-cutting concerns
* **Error Handling**: Use the established exception handling decorators and patterns
* **Route Definitions**: Use `web.RouteTableDef()` and organize routes logically within modules

### 8. **Running tests**
* Use `--keep-docker-up` flag when testing to keep docker containers up between sessions.
* Always activate the python virtual environment before running pytest.
19 changes: 11 additions & 8 deletions packages/service-library/src/servicelib/aiohttp/aiopg_utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
""" Holderplace for random helpers using aiopg
"""Holderplace for random helpers using aiopg

- Drop here functions/constants that at that time does
not fit in any of the setups. Then, they can be moved and
refactor when new abstractions are used in place.
- Drop here functions/constants that at that time does
not fit in any of the setups. Then, they can be moved and
refactor when new abstractions are used in place.

- aiopg is used as a client sdk to interact asynchronously with postgres service
- aiopg is used as a client sdk to interact asynchronously with postgres service

SEE for aiopg: https://aiopg.readthedocs.io/en/stable/sa.html
SEE for underlying psycopg: http://initd.org/psycopg/docs/module.html
SEE for extra keywords: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
SEE for aiopg: https://aiopg.readthedocs.io/en/stable/sa.html
SEE for underlying psycopg: http://initd.org/psycopg/docs/module.html
SEE for extra keywords: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
"""

# TODO: Towards implementing https://github.com/ITISFoundation/osparc-simcore/issues/1195
# TODO: deprecate this module. Move utils into retry_policies, simcore_postgres_database.utils_aiopg

import logging
from typing import Final

import sqlalchemy as sa
from aiohttp import web
Expand All @@ -31,6 +32,8 @@

log = logging.getLogger(__name__)

APP_AIOPG_ENGINE_KEY: Final = web.AppKey("APP_AIOPG_ENGINE_KEY", Engine)


async def raise_if_not_responsive(engine: Engine):
async with engine.acquire() as conn:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" Namespace to keep all application storage keys
"""Namespace to keep all application storage keys

Unique keys to identify stored data
Naming convention accounts for the storage scope: application, request, response, configuration and/or resources
Expand All @@ -8,22 +8,25 @@

See https://aiohttp.readthedocs.io/en/stable/web_advanced.html#data-sharing-aka-no-singletons-please
"""

from typing import Final

# REQUIREMENTS:
# - guarantees all keys are unique
# - one place for all common keys
# - hierarchical classification
from aiohttp import ClientSession, web

# APPLICATION's CONTEXT KEYS

# NOTE: use these keys to store/retrieve data from aiohttp.web.Application
# SEE https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-app-key

#
# web.Application keys, i.e. app[APP_*_KEY]
#
APP_CONFIG_KEY: Final[str] = f"{__name__ }.config"
APP_SETTINGS_KEY: Final[str] = f"{__name__ }.settings"
APP_CONFIG_KEY = web.AppKey("APP_CONFIG_KEY", dict[str, object])

APP_AIOPG_ENGINE_KEY: Final[str] = f"{__name__ }.aiopg_engine"

APP_CLIENT_SESSION_KEY: Final[str] = f"{__name__ }.session"
APP_CLIENT_SESSION_KEY: web.AppKey[ClientSession] = web.AppKey("APP_CLIENT_SESSION_KEY")


APP_FIRE_AND_FORGET_TASKS_KEY: Final[str] = f"{__name__}.tasks"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
TypedDict,
)

from .application_keys import APP_CONFIG_KEY, APP_SETTINGS_KEY
from .application_keys import APP_CONFIG_KEY

_logger = logging.getLogger(__name__)

APP_SETUP_COMPLETED_KEY: Final[str] = f"{__name__ }.setup"
APP_SETUP_COMPLETED_KEY: Final[web.AppKey] = web.AppKey("setup_completed", list[str])


class _SetupFunc(Protocol):
Expand Down Expand Up @@ -115,13 +115,14 @@ def _get_app_settings_and_field_name(
arg_settings_name: str | None,
setup_func_name: str,
logger: logging.Logger,
app_settings_key: web.AppKey,
) -> tuple[_ApplicationSettings | None, str | None]:
app_settings: _ApplicationSettings | None = app.get(APP_SETTINGS_KEY)
app_settings: _ApplicationSettings | None = app.get(app_settings_key)
settings_field_name = arg_settings_name

if app_settings:
if not settings_field_name:
# FIXME: hard-coded WEBSERVER_ temporary
# NOTE: hard-coded WEBSERVER_ temporary
settings_field_name = f"WEBSERVER_{arg_module_name.split('.')[-1].upper()}"

logger.debug("Checking addon's %s ", f"{settings_field_name=}")
Expand Down Expand Up @@ -246,6 +247,7 @@ def app_module_setup(
module_name: str,
category: ModuleCategory,
*,
app_settings_key: web.AppKey,
settings_name: str | None = None,
depends: list[str] | None = None,
logger: logging.Logger = _logger,
Expand Down Expand Up @@ -336,6 +338,7 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
settings_name,
setup_func.__name__,
logger,
app_settings_key,
)

if (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from collections.abc import AsyncGenerator
from typing import cast

from aiohttp import ClientSession, ClientTimeout, web
from common_library.json_serialization import json_dumps
Expand Down Expand Up @@ -41,10 +40,11 @@ async def persistent_client_session(app: web.Application) -> AsyncGenerator[None
def get_client_session(app: web.Application) -> ClientSession:
"""Refers to the one-and-only client in the app"""
assert APP_CLIENT_SESSION_KEY in app # nosec
return cast(ClientSession, app[APP_CLIENT_SESSION_KEY])
return app[APP_CLIENT_SESSION_KEY]


__all__: tuple[str, ...] = (
"APP_CLIENT_SESSION_KEY",
"get_client_session",
"persistent_client_session",
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import logging
from typing import Final

import aiodocker
import aiohttp
from aiohttp import web
from models_library.docker import DockerGenericTag
from pydantic import TypeAdapter, ValidationError
from settings_library.docker_registry import RegistrySettings
Expand All @@ -18,6 +21,8 @@

_logger = logging.getLogger(__name__)

APP_DOCKER_ENGINE_KEY: Final = web.AppKey("APP_DOCKER_ENGINE_KEY", aiodocker.Docker)


async def retrieve_image_layer_information(
image: DockerGenericTag, registry_settings: RegistrySettings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@
running task.
"""

from typing import Final

from aiohttp import web

from ._manager import get_long_running_manager
from ._server import setup, start_long_running_task

APP_LONG_RUNNING_TASKS_KEY: Final = web.AppKey(
"APP_LONG_RUNNING_TASKS_KEY", dict[str, object]
)

__all__: tuple[str, ...] = (
"get_long_running_manager",
"setup",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import asyncio.events
import sys
import time
from typing import Final

from aiohttp import web
from pyinstrument import Profiler

from .incidents import LimitedOrderedStack, SlowCallback

APP_SLOW_CALLBACKS_MONITOR_KEY: Final = web.AppKey(
"APP_SLOW_CALLBACKS_MONITOR_KEY", LimitedOrderedStack[SlowCallback]
)


def enable(
slow_duration_secs: float, incidents: LimitedOrderedStack[SlowCallback]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
_logger = logging.getLogger(__name__)

_PROMETHEUS_METRICS: Final[str] = f"{__name__}.prometheus_metrics" # noqa: N816
APP_MONITORING_NAMESPACE_KEY: Final = web.AppKey("APP_MONITORING_NAMESPACE_KEY", str)


def get_collector_registry(app: web.Application) -> CollectorRegistry:
Expand Down
4 changes: 4 additions & 0 deletions packages/service-library/src/servicelib/aiohttp/observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
from collections import defaultdict
from collections.abc import Callable
from typing import Final

from aiohttp import web

Expand All @@ -16,6 +17,9 @@


_APP_OBSERVER_EVENTS_REGISTRY_KEY = "{__name__}.event_registry"
APP_FIRE_AND_FORGET_TASKS_KEY: Final = web.AppKey(
"APP_FIRE_AND_FORGET_TASKS_KEY", set[object]
)


class ObserverRegistryNotFoundError(RuntimeError): ...
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
""" Storage keys in requests
"""Storage keys in requests"""

"""
from typing import Final

# RQT=request
RQT_USERID_KEY: Final[str] = f"{__name__}.userid"
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import json.decoder
from collections.abc import Iterator
from contextlib import contextmanager
from typing import TypeVar
from typing import Final, TypeVar

from aiohttp import web
from common_library.user_messages import user_message
Expand All @@ -23,6 +23,10 @@
ModelClass = TypeVar("ModelClass", bound=BaseModel)
ModelOrListOrDictType = TypeVar("ModelOrListOrDictType", bound=BaseModel | list | dict)

APP_JSON_SCHEMA_SPECS_KEY: Final = web.AppKey(
"APP_JSON_SCHEMA_SPECS_KEY", dict[str, object]
)


@contextmanager
def handle_validation_as_http_error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import logging
from collections.abc import Awaitable, Callable
from typing import Any
from typing import Any, Final

from aiohttp import web
from aiohttp.web_exceptions import HTTPError
Expand Down Expand Up @@ -298,3 +298,8 @@ def append_rest_middlewares(
"""Helper that appends rest-middlewares in the correct order"""
app.middlewares.append(error_middleware_factory(api_version))
app.middlewares.append(envelope_middleware_factory(api_version))


APP_JSONSCHEMA_SPECS_KEY: Final = web.AppKey(
"APP_JSONSCHEMA_SPECS_KEY", dict[str, object]
)
6 changes: 6 additions & 0 deletions packages/service-library/src/servicelib/aiohttp/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

from __future__ import annotations

from typing import Final

from aiohttp import web

__all__ = (
"HTTP_100_CONTINUE",
"HTTP_101_SWITCHING_PROTOCOLS",
Expand Down Expand Up @@ -146,3 +150,5 @@
HTTP_508_LOOP_DETECTED = 508
HTTP_510_NOT_EXTENDED = 510
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511

APP_HEALTH_KEY: Final = web.AppKey("APP_HEALTH_KEY", str)
5 changes: 5 additions & 0 deletions packages/service-library/src/servicelib/aiohttp/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
from collections.abc import AsyncIterator, Callable
from typing import Final

from aiohttp import web
from opentelemetry import trace
Expand Down Expand Up @@ -59,6 +60,10 @@
except ImportError:
HAS_AIO_PIKA = False

APP_OPENTELEMETRY_INSTRUMENTOR_KEY: Final = web.AppKey(
"APP_OPENTELEMETRY_INSTRUMENTOR_KEY", dict[str, object]
)


def _create_span_processor(tracing_destination: str) -> SpanProcessor:
otlp_exporter = OTLPSpanExporterHTTP(
Expand Down
Loading
Loading