Skip to content

Commit 911bcdf

Browse files
author
Andrei Neagu
committed
Merge remote-tracking branch 'upstream/master' into pr-osparc-docker-socket-via-http-interface
2 parents bd7e1c7 + d4d8e65 commit 911bcdf

File tree

120 files changed

+2440
-1010
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+2440
-1010
lines changed

.github/workflows/ci-testing-pull-request.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ jobs:
3131
enable-cache: false
3232
- name: checkout source branch
3333
uses: actions/checkout@v4
34-
- name: Regenerate specs and check
34+
- name: Generate openapi specs
3535
run: |
3636
make devenv
3737
source .venv/bin/activate
3838
make openapi-specs
39+
- name: Check openapi specs are up to date
40+
run: |
3941
if ! ./ci/github/helpers/openapi-specs-diff.bash diff \
40-
https://raw.githubusercontent.com/${{ github.event.pull_request.head.repo.full_name }}/refs/heads/${{ github.event.pull_request.head.ref }} \
42+
https://raw.githubusercontent.com/${{ github.event.pull_request.head.repo.full_name }}/${{ github.event.after }} \
4143
.; then \
4244
echo "::error:: OAS are not up to date. Run 'make openapi-specs' to update them"; exit 1; \
4345
fi
@@ -57,7 +59,7 @@ jobs:
5759
- name: check api-server backwards compatibility
5860
run: |
5961
./scripts/openapi-diff.bash breaking --fail-on ERR\
60-
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/refs/heads/${{ github.event.pull_request.base.ref }}/services/api-server/openapi.json \
62+
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/${{ github.event.after }}/services/api-server/openapi.json \
6163
/specs/services/api-server/openapi.json
6264
6365
all-oas-breaking:
@@ -76,5 +78,5 @@ jobs:
7678
- name: Check openapi-specs backwards compatibility
7779
run: |
7880
./ci/github/helpers/openapi-specs-diff.bash breaking \
79-
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/refs/heads/${{ github.event.pull_request.base.ref }} \
81+
https://raw.githubusercontent.com/${{ github.event.pull_request.base.repo.full_name }}/${{ github.event.after }} \
8082
.

packages/service-integration/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,11 @@ This is the o2sparc's service integration library or ``ooil`` in short
55

66

77
SEE how is it used in Makefiles in https://github.com/ITISFoundation/cookiecutter-osparc-service
8+
9+
10+
#### What is the .osparc folder and its content?
11+
'osparc config' is a set of stardard file forms (yaml) that the user fills provides in order to describe how her service works and integrates with osparc. It may contain:
12+
- config files are stored under '.osparc/' folder in the root repo folder (analogous to other configs like .github, .vscode, etc)
13+
- configs are parsed and validated into pydantic models
14+
- models can be serialized/deserialized into label annotations on images. This way, the config is attached to the service during it's entire lifetime.
15+
- config should provide enough information about that context to allow building an image and running a container on a single command call.

packages/service-integration/src/service_integration/cli/_compose_spec.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ def create_compose(
192192
if file_path.exists():
193193
configs_kwargs_map[config_name][arg_name] = file_path
194194

195+
# warn about subfolders without metadata.yml
196+
for subdir in filter(lambda p: p.is_dir(), basedir.rglob("*")):
197+
if not (subdir / "metadata.yml").exists():
198+
relative_subdir = subdir.relative_to(basedir)
199+
rich.print(
200+
f"[warning] Subfolder '{relative_subdir}' does not contain a 'metadata.yml' file. Skipping."
201+
)
202+
195203
if not configs_kwargs_map:
196204
rich.print(f"[warning] No config files were found in '{config_path}'")
197205

packages/service-library/requirements/_fastapi.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
fastapi
10+
fastapi-lifespan-manager
1011
httpx
1112
opentelemetry-instrumentation-fastapi
1213
opentelemetry-instrumentation-httpx

packages/service-library/requirements/_fastapi.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ deprecated==1.2.17
2323
# opentelemetry-api
2424
# opentelemetry-semantic-conventions
2525
fastapi==0.115.7
26+
# via
27+
# -r requirements/_fastapi.in
28+
# fastapi-lifespan-manager
29+
fastapi-lifespan-manager==0.1.4
2630
# via -r requirements/_fastapi.in
2731
h11==0.14.0
2832
# via

packages/service-library/src/servicelib/docker_utils.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import asyncio
22
import logging
33
from collections.abc import AsyncIterator, Awaitable, Callable
4-
from contextlib import AsyncExitStack, asynccontextmanager
4+
from contextlib import AsyncExitStack
55
from dataclasses import dataclass
66
from datetime import datetime
77
from functools import cached_property
8-
from typing import Any, AsyncContextManager, Final, Literal
8+
from typing import Any, Final, Literal
99

1010
import aiodocker
1111
import aiohttp
1212
import arrow
1313
import tenacity
1414
from aiohttp import ClientSession
1515
from fastapi import FastAPI
16+
from fastapi_lifespan_manager import State
1617
from models_library.docker import DockerGenericTag
1718
from models_library.generated_models.docker_rest_api import ProgressDetail
1819
from models_library.utils.change_case import snake_to_camel
@@ -24,6 +25,7 @@
2425
TypeAdapter,
2526
ValidationError,
2627
)
28+
from servicelib.fastapi.lifespan_utils import LifespanGenerator
2729
from settings_library.docker_api_proxy import DockerApiProxysettings
2830
from settings_library.docker_registry import RegistrySettings
2931
from yarl import URL
@@ -300,7 +302,7 @@ async def pull_image(
300302

301303
def get_lifespan_remote_docker_client(
302304
docker_api_proxy_settings_property_name: str,
303-
) -> Callable[[FastAPI], AsyncContextManager[None]]:
305+
) -> LifespanGenerator:
304306
"""Ensures `setup` and `teardown` for the remote docker client.
305307
306308
Arguments:
@@ -311,8 +313,7 @@ def get_lifespan_remote_docker_client(
311313
docker client lifespan manager
312314
"""
313315

314-
@asynccontextmanager
315-
async def _(app: FastAPI) -> AsyncIterator[None]:
316+
async def _(app: FastAPI) -> AsyncIterator[State]:
316317
settings: DockerApiProxysettings = getattr(
317318
app.state.settings, docker_api_proxy_settings_property_name
318319
)
@@ -345,7 +346,7 @@ async def _(app: FastAPI) -> AsyncIterator[None]:
345346

346347
await wait_till_docker_api_proxy_is_responsive(app)
347348

348-
yield
349+
yield {}
349350

350351
return _
351352

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
1-
from collections.abc import AsyncGenerator, Callable
2-
from contextlib import AsyncExitStack, asynccontextmanager
3-
from typing import AsyncContextManager, TypeAlias
1+
from collections.abc import AsyncIterator
2+
from typing import Protocol
43

54
from fastapi import FastAPI
5+
from fastapi_lifespan_manager import LifespanManager, State
66

7-
LifespanContextManager: TypeAlias = Callable[[FastAPI], AsyncContextManager[None]]
87

8+
class LifespanGenerator(Protocol):
9+
def __call__(self, app: FastAPI) -> AsyncIterator["State"]:
10+
...
911

10-
def combine_lfiespan_context_managers(
11-
*context_managers: LifespanContextManager,
12-
) -> LifespanContextManager:
13-
"""the first entry has its `setup` called first and its `teardown` called last
14-
With `setup` and `teardown` referring to the code before and after the `yield`
15-
"""
1612

17-
@asynccontextmanager
18-
async def _(app: FastAPI) -> AsyncGenerator[None, None]:
19-
async with AsyncExitStack() as stack:
20-
for context_manager in context_managers:
21-
await stack.enter_async_context(context_manager(app))
22-
yield
13+
def combine_lifespans(*generators: LifespanGenerator) -> LifespanManager:
2314

24-
return _
15+
manager = LifespanManager()
16+
17+
for generator in generators:
18+
manager.add(generator)
19+
20+
return manager

packages/service-library/src/servicelib/fastapi/profiler_middleware.py renamed to packages/service-library/src/servicelib/fastapi/profiler.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Any, Final
22

3+
from fastapi import FastAPI
34
from servicelib.aiohttp import status
45
from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
56
from starlette.requests import Request
@@ -13,7 +14,7 @@
1314
)
1415

1516

16-
def is_last_response(response_headers: dict[bytes, bytes], message: dict[str, Any]):
17+
def _is_last_response(response_headers: dict[bytes, bytes], message: dict[str, Any]):
1718
if (
1819
content_type := response_headers.get(b"content-type")
1920
) and content_type == MIMETYPE_APPLICATION_JSON.encode():
@@ -79,7 +80,7 @@ async def _send_wrapper(message):
7980
response_headers = dict(message.get("headers"))
8081
message["headers"] = check_response_headers(response_headers)
8182
elif message["type"] == "http.response.body":
82-
if is_last_response(response_headers, message):
83+
if _is_last_response(response_headers, message):
8384
_profiler.stop()
8485
profile_text = _profiler.output_text(
8586
unicode=True, color=True, show_all=True
@@ -96,3 +97,8 @@ async def _send_wrapper(message):
9697

9798
finally:
9899
_profiler.reset()
100+
101+
102+
def initialize_profiler(app: FastAPI) -> None:
103+
# NOTE: this cannot be ran once the application is started
104+
app.add_middleware(ProfilerMiddleware)
Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,62 @@
11
# pylint: disable=protected-access
22

33

4+
from collections.abc import AsyncIterator
5+
46
from fastapi import FastAPI
7+
from fastapi_lifespan_manager import State
58
from prometheus_client import CollectorRegistry
69
from prometheus_fastapi_instrumentator import Instrumentator
710

811

9-
def setup_prometheus_instrumentation(app: FastAPI) -> Instrumentator:
12+
def initialize_prometheus_instrumentation(app: FastAPI) -> None:
13+
# NOTE: this cannot be ran once the application is started
14+
1015
# NOTE: use that registry to prevent having a global one
1116
app.state.prometheus_registry = registry = CollectorRegistry(auto_describe=True)
12-
instrumentator = Instrumentator(
17+
app.state.prometheus_instrumentator = Instrumentator(
1318
should_instrument_requests_inprogress=False, # bug in https://github.com/trallnag/prometheus-fastapi-instrumentator/issues/317
1419
inprogress_labels=False,
1520
registry=registry,
16-
).instrument(app)
21+
)
22+
app.state.prometheus_instrumentator.instrument(app)
23+
24+
25+
def _startup(app: FastAPI) -> None:
26+
assert isinstance(app.state.prometheus_instrumentator, Instrumentator) # nosec
27+
app.state.prometheus_instrumentator.expose(app, include_in_schema=False)
28+
29+
30+
def _shutdown(app: FastAPI) -> None:
31+
assert isinstance(app.state.prometheus_registry, CollectorRegistry) # nosec
32+
registry = app.state.prometheus_registry
33+
for collector in list(registry._collector_to_names.keys()): # noqa: SLF001
34+
registry.unregister(collector)
35+
36+
37+
def get_prometheus_instrumentator(app: FastAPI) -> Instrumentator:
38+
assert isinstance(app.state.prometheus_instrumentator, Instrumentator) # nosec
39+
return app.state.prometheus_instrumentator
40+
41+
42+
def setup_prometheus_instrumentation(app: FastAPI) -> Instrumentator:
43+
initialize_prometheus_instrumentation(app)
1744

1845
async def _on_startup() -> None:
19-
instrumentator.expose(app, include_in_schema=False)
46+
_startup(app)
2047

21-
def _unregister() -> None:
22-
# NOTE: avoid registering collectors multiple times when running unittests consecutively (https://stackoverflow.com/a/62489287)
23-
for collector in list(registry._collector_to_names.keys()): # noqa: SLF001
24-
registry.unregister(collector)
48+
def _on_shutdown() -> None:
49+
_shutdown(app)
2550

2651
app.add_event_handler("startup", _on_startup)
27-
app.add_event_handler("shutdown", _unregister)
28-
return instrumentator
52+
app.add_event_handler("shutdown", _on_shutdown)
53+
54+
return get_prometheus_instrumentator(app)
55+
56+
57+
async def lifespan_prometheus_instrumentation(app: FastAPI) -> AsyncIterator[State]:
58+
# NOTE: requires ``initialize_prometheus_instrumentation`` to be called before the
59+
# lifespan of the applicaiton runs, usually rigth after the ``FastAPI`` instance is created
60+
_startup(app)
61+
yield {}
62+
_shutdown(app)

packages/service-library/src/servicelib/fastapi/tracing.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
"""
44

55
import logging
6+
from collections.abc import AsyncIterator
67

78
from fastapi import FastAPI
9+
from fastapi_lifespan_manager import State
810
from httpx import AsyncClient, Client
911
from opentelemetry import trace
1012
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
@@ -15,6 +17,7 @@
1517
from opentelemetry.sdk.resources import Resource
1618
from opentelemetry.sdk.trace import TracerProvider
1719
from opentelemetry.sdk.trace.export import BatchSpanProcessor
20+
from servicelib.fastapi.lifespan_utils import LifespanGenerator
1821
from servicelib.logging_utils import log_context
1922
from settings_library.tracing import TracingSettings
2023
from yarl import URL
@@ -131,5 +134,15 @@ def setup_tracing(
131134
RequestsInstrumentor().instrument()
132135

133136

137+
def get_lifespan_tracing(
138+
tracing_settings: TracingSettings, service_name: str
139+
) -> LifespanGenerator:
140+
async def _(app: FastAPI) -> AsyncIterator[State]:
141+
setup_tracing(app, tracing_settings, service_name)
142+
yield {}
143+
144+
return _
145+
146+
134147
def setup_httpx_client_tracing(client: AsyncClient | Client):
135148
HTTPXClientInstrumentor.instrument_client(client)

0 commit comments

Comments
 (0)