8000 Merge pull request #24 from modern-python/feature/refactor-extras · modern-python/lite-bootstrap@2cdc1e9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2cdc1e9

Browse files
authored
Merge pull request #24 from modern-python/feature/refactor-extras
check if required packages are installed for instruments and bootstrappers
2 parents f6d087a + 0d64a8a commit 2cdc1e9

16 files changed

+220
-90
lines changed

lite_bootstrap/bootstrappers/base.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22
import typing
3+
import warnings
34

45
from lite_bootstrap.instruments.base import BaseConfig, BaseInstrument
56
from lite_bootstrap.types import ApplicationT< 10000 /span>
@@ -14,16 +15,28 @@ class BaseBootstrapper(abc.ABC, typing.Generic[ApplicationT]):
1415
bootstrap_config: BaseConfig
1516

1617
def __init__(self, bootstrap_config: BaseConfig) -> None:
18+
if not self.is_ready():
19+
raise RuntimeError(self.not_ready_message)
20+
1721
self.bootstrap_config = bootstrap_config
1822
self.instruments = []
1923
for instrument_type in self.instruments_types:
2024
instrument = instrument_type(bootstrap_config=bootstrap_config)
2125
if instrument.is_ready():
2226
self.instruments.append(instrument)
27+
else:
28+
warnings.warn(instrument.not_ready_message, stacklevel=2)
29+
30+
@property
31+
@abc.abstractmethod
32+
def not_ready_message(self) -> str: ...
2333

2434
@abc.abstractmethod
2535
def _prepare_application(self) -> ApplicationT: ...
2636

37+
@abc.abstractmethod
38+
def is_ready(self) -> bool: ...
39+
2740
def bootstrap(self) -> ApplicationT:
2841
for one_instrument in self.instruments:
2942
one_instrument.bootstrap()

lite_bootstrap/bootstrappers/fastapi_bootstrapper.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import contextlib
21
import dataclasses
32
import typing
43

4+
from lite_bootstrap import import_checker
55
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
66
from lite_bootstrap.instruments.healthchecks_instrument import (
77
HealthChecksConfig,
@@ -14,16 +14,20 @@
1414
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
1515

1616

17-
with contextlib.suppress(ImportError):
17+
if import_checker.is_fastapi_installed:
1818
import fastapi
19+
20+
if import_checker.is_opentelemetry_installed:
1921
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
2022
from opentelemetry.trace import get_tracer_provider
23+
24+
if import_checker.is_prometheus_fastapi_instrumentator_installed:
2125
from prometheus_fastapi_instrumentator import Instrumentator
2226

2327

2428
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
2529
class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusConfig, SentryConfig):
26-
application: fastapi.FastAPI = dataclasses.field(default_factory=fastapi.FastAPI)
30+
application: "fastapi.FastAPI" = dataclasses.field(default_factory=fastapi.FastAPI)
2731
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
2832
prometheus_instrumentator_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
2933
prometheus_instrument_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
@@ -34,7 +38,7 @@ class FastAPIConfig(HealthChecksConfig, LoggingConfig, OpentelemetryConfig, Prom
3438
class FastAPIHealthChecksInstrument(HealthChecksInstrument):
3539
bootstrap_config: FastAPIConfig
3640

37-
def build_fastapi_health_check_router(self) -> fastapi.APIRouter:
41+
def build_fastapi_health_check_router(self) -> "fastapi.APIRouter":
3842
fastapi_router = fastapi.APIRouter(
3943
tags=["probes"],
4044
include_in_schema=self.bootstrap_config.health_checks_include_in_schema,
@@ -87,6 +91,12 @@ class FastAPISentryInstrument(SentryInstrument):
8791
@dataclasses.dataclass(kw_only=True, frozen=True)
8892
class FastAPIPrometheusInstrument(PrometheusInstrument):
8993
bootstrap_config: FastAPIConfig
94+
not_ready_message = (
95+
PrometheusInstrument.not_ready_message + " or prometheus_fastapi_instrumentator is not installed"
96+
)
97+
98+
def is_ready(self) -> bool:
99+
return super().is_ready() and import_checker.is_prometheus_fastapi_instrumentator_installed
90100

91101
def bootstrap(self) -> None:
92102
Instrumentator(**self.bootstrap_config.prometheus_instrument_params).instrument(
@@ -101,6 +111,8 @@ def bootstrap(self) -> None:
101111

102112

103113
class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]):
114+
__slots__ = "bootstrap_config", "instruments"
115+
104116
instruments_types: typing.ClassVar = [
105117
FastAPIOpenTelemetryInstrument,
106118
FastAPISentryInstrument,
@@ -109,7 +121,10 @@ class FastAPIBootstrapper(BaseBootstrapper[fastapi.FastAPI]):
109121
FastAPIPrometheusInstrument,
110122
]
111123
bootstrap_config: FastAPIConfig
112-
__slots__ = "bootstrap_config", "instruments"
124+
not_ready_message = "fastapi is not installed"
125+
126+
def is_ready(self) -> bool:
127+
return import_checker.is_fastapi_installed
113128

114129
def _prepare_application(self) -> fastapi.FastAPI:
115130
return self.bootstrap_config.application

lite_bootstrap/bootstrappers/faststream_bootstrapper.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from __future__ import annotations
2-
import contextlib
32
import dataclasses
43
import json
54
import typing
65

6+
from lite_bootstrap import import_checker
77
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
88
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksConfig, HealthChecksInstrument
99
from lite_bootstrap.instruments.logging_instrument import LoggingConfig, LoggingInstrument
@@ -12,12 +12,16 @@
1212
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
1313

1414

15-
with contextlib.suppress(ImportError):
15+
if import_checker.is_faststream_installed:
1616
import faststream
17-
import prometheus_client
1817
from faststream.asgi import AsgiFastStream, AsgiResponse
1918
from faststream.asgi import get as handle_get
2019
from faststream.broker.core.usecase import BrokerUsecase
20+
21+
if import_checker.is_prometheus_client_installed:
22+
import prometheus_client
23+
24+
if import_checker.is_opentelemetry_installed:
2125
from opentelemetry.metrics import Meter, MeterProvider
2226
from opentelemetry.trace import TracerProvider, get_tracer_provider
2327

@@ -87,9 +91,10 @@ class FastStreamLoggingInstrument(LoggingInstrument):
8791
@dataclasses.dataclass(kw_only=True, frozen=True)
8892
class FastStreamOpenTelemetryInstrument(OpenTelemetryInstrument):
8993
bootstrap_config: FastStreamConfig
94+
not_ready_message = OpenTelemetryInstrument.not_ready_message + " or opentelemetry_middleware_cls is empty"
9095

9196
def is_ready(self) -> bool:
92-
return bool(self.bootstrap_config.opentelemetry_middleware_cls and super().is_ready())
97+
return super().is_ready() and bool(self.bootstrap_config.opentelemetry_middleware_cls)
9398

9499
def bootstrap(self) -> None:
95100
if self.bootstrap_config.opentelemetry_middleware_cls and self.bootstrap_config.application.broker:
@@ -109,9 +114,18 @@ class FastStreamPrometheusInstrument(PrometheusInstrument):
109114
collector_registry: prometheus_client.CollectorRegistry = dataclasses.field(
110115
default_factory=prometheus_client.CollectorRegistry, init=False
111116
)
117+
not_ready_message = (
118+
PrometheusInstrument.not_ready_message
119+
+ " or prometheus_middleware_cls is missing or prometheus_client is not installed"
120+
)
112121

113122
def is_ready(self) -> bool:
114-
return bool(self.bootstrap_config.prometheus_middleware_cls and super().is_ready())
123+
return (
124+
super().is_ready()
125+
and import_checker.is_prometheus_client_installed
126+
and bool(self.bootstrap_config.prometheus_middleware_cls)
127+
and import_checker.is_prometheus_client_installed
128+
)
115129

116130
def bootstrap(self) -> None:
117131
self.bootstrap_config.application.mount(
@@ -124,6 +138,8 @@ def bootstrap(self) -> None:
124138

125139

126140
class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]):
141+
__slots__ = "bootstrap_config", "instruments"
142+
127143
instruments_types: typing.ClassVar = [
128144
FastStreamOpenTelemetryInstrument,
129145
FastStreamSentryInstrument,
@@ -132,7 +148,10 @@ class FastStreamBootstrapper(BaseBootstrapper[AsgiFastStream]):
132148
FastStreamPrometheusInstrument,
133149
]
134150
bootstrap_config: FastStreamConfig
135-
__slots__ = "bootstrap_config", "instruments"
151+
not_ready_message = "faststream is not installed"
152+
153+
def is_ready(self) -> bool:
154+
return import_checker.is_faststream_installed
136155

137156
def __init__(self, bootstrap_config: FastStreamConfig) -> None:
138157
super().__init__(bootstrap_config)

lite_bootstrap/bootstrappers/free_bootstrapper.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ class FreeBootstrapperConfig(LoggingConfig, OpentelemetryConfig, SentryConfig):
1212

1313

1414
class FreeBootstrapper(BaseBootstrapper[None]):
15+
__slots__ = "bootstrap_config", "instruments"
16+
1517
instruments_types: typing.ClassVar = [
1618
OpenTelemetryInstrument,
1719
SentryInstrument,
1820
LoggingInstrument,
1921
]
2022
bootstrap_config: FreeBootstrapperConfig
21-
__slots__ = "bootstrap_config", "instruments"
23+
not_ready_message = ""
24+
25+
def is_ready(self) -> bool:
26+
return True
2227

2328
def __init__(self, bootstrap_config: FreeBootstrapperConfig) -> None:
2429
super().__init__(bootstrap_config)

lite_bootstrap/bootstrappers/litestar_bootstrapper.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import contextlib
21
import dataclasses
32
import typing
43

5-
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
6-
4+
from lite_bootstrap import import_checker
75
from lite_bootstrap.bootstrappers.base import BaseBootstrapper
86
from lite_bootstrap.instruments.healthchecks_instrument import (
97
HealthChecksConfig,
@@ -21,18 +19,21 @@
2119
from lite_bootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument
2220

2321

24-
with contextlib.suppress(ImportError):
22+
if import_checker.is_litestar_installed:
2523
import litestar
2624
from litestar.config.app import AppConfig
2725
from litestar.contrib.opentelemetry import OpenTelemetryConfig
26+
from litestar.plugins.prometheus import PrometheusConfig, PrometheusController
27+
28+
if import_checker.is_opentelemetry_installed:
2829
from opentelemetry.trace import get_tracer_provider
2930

3031

3132
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
3233
class LitestarConfig(
3334
HealthChecksConfig, LoggingConfig, OpentelemetryConfig, PrometheusBootstrapperConfig, SentryConfig
3435
):
35-
application_config: AppConfig = dataclasses.field(default_factory=AppConfig)
36+
application_config: "AppConfig" = dataclasses.field(default_factory=AppConfig)
3637
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)
3738
prometheus_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
3839

@@ -41,7 +42,7 @@ class LitestarConfig(
4142
class LitestarHealthChecksInstrument(HealthChecksInstrument):
4243
bootstrap_config: LitestarConfig
4344

44-
def build_litestar_health_check_router(self) -> litestar.Router:
45+
def build_litestar_health_check_router(self) -> "litestar.Router":
4546
@litestar.get(media_type=litestar.MediaType.JSON)
4647
async def health_check_handler() -> HealthCheckTypedDict:
4748
return self.render_health_check_data()
@@ -91,6 +92,10 @@ class LitestarSentryInstrument(SentryInstrument):
9192
@dataclasses.dataclass(kw_only=True, frozen=True)
9293
class LitestarPrometheusInstrument(PrometheusInstrument):
9394
bootstrap_config: LitestarConfig
95+
not_ready_message = PrometheusInstrument.not_ready_message + " or prometheus_client is not installed"
96+
97+
def is_ready(self) -> bool:
98+
return super().is_ready() and import_checker.is_prometheus_client_installed
9499

95100
def bootstrap(self) -> None:
96101
class LitestarPrometheusController(PrometheusController):
@@ -108,6 +113,8 @@ class LitestarPrometheusController(PrometheusController):
108113

109114

110115
class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]):
116+
__slots__ = "bootstrap_config", "instruments"
117+
111118
instruments_types: typing.ClassVar = [
112119
LitestarOpenTelemetryInstrument,
113120
LitestarSentryInstrument,
@@ -116,7 +123,10 @@ class LitestarBootstrapper(BaseBootstrapper[litestar.Litestar]):
116123
LitestarPrometheusInstrument,
117124
]
118125
bootstrap_config: LitestarConfig
119-
__slots__ = "bootstrap_config", "instruments"
126+
not_ready_message = "litestar is not installed"
127+
128+
def is_ready(self) -> bool:
129+
return import_checker.is_litestar_installed
120130

121131
def __init__(self, bootstrap_config: LitestarConfig) -> None:
122132
super().__init__(bootstrap_config)

lite_bootstrap/import_checker.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from importlib.util import find_spec
2+
3+
4+
is_opentelemetry_installed = find_spec("opentelemetry") is not None
5+
is_sentry_installed = find_spec("sentry_sdk") is not None
6+
is_structlog_installed = find_spec("structlog") is not None
7+
is_prometheus_client_installed = find_spec("prometheus_client") is not None
8+
is_fastapi_installed = find_spec("fastapi") is not None
9+
is_litestar_installed = find_spec("litestar") is not None
10+
is_faststream_installed = find_spec("faststream") is not None
11+
is_prometheus_fastapi_instrumentator_installed = find_spec("prometheus_fastapi_instrumentator") is not None

lite_bootstrap/instruments/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class BaseConfig:
1414
class BaseInstrument(abc.ABC):
1515
bootstrap_config: BaseConfig
1616

17+
@property
18+
@abc.abstractmethod
19+
def not_ready_message(self) -> str: ...
20+
1721
def bootstrap(self) -> None: ... # noqa: B027
1822

1923
def teardown(self) -> None: ... # noqa: B027

lite_bootstrap/instruments/healthchecks_instrument.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class HealthChecksConfig(BaseConfig):
2121
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
2222
class HealthChecksInstrument(BaseInstrument):
2323
bootstrap_config: HealthChecksConfig
24+
not_ready_message = "health_checks_enabled is False"
2425

2526
def is_ready(self) -> bool:
2627
return self.bootstrap_config.health_checks_enabled

0 commit comments

Comments
 (0)
0