Skip to content

Commit 9326643

Browse files
authored
Merge pull request #11 from community-of-python/feature/add-offline-docs-and-cors
Add offline swagger and CORS support
2 parents db2a0a0 + b3139d2 commit 9326643

21 files changed

+572
-208
lines changed

README.md

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ from your_application.settings import settings
3434
application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()
3535
```
3636

37-
With <b>microbootstrap</b>, you get an application with built-in support for:
37+
Only `litestar` is supported yet.
38+
With <b>microbootstrap</b>, you get an application with lightweight built-in support for:
3839

3940
- `sentry`
4041
- `prometheus`
4142
- `opentelemetry`
4243
- `logging`
43-
44-
<b>microbootstrap</b> supports only `litestar` framework for now.
44+
- `cors`
45+
- `swagger` - additional offline version support
4546

4647
Interested? Let's jump right into it ⚡
4748

@@ -58,7 +59,7 @@ $ poetry add microbootstrap -E litestar
5859
pip:
5960

6061
```bash
61-
$ poetry add microbootstrap[litestar]
62+
$ pip install microbootstrap[litestar]
6263
```
6364

6465
## Quickstart
@@ -163,6 +164,8 @@ Currently, these instruments are already supported for bootstrapping:
163164
- `prometheus`
164165
- `opentelemetry`
165166
- `logging`
167+
- `cors`
168+
- `swagger`
166169

167170
Let's make it clear, what it takes to bootstrap them.
168171

@@ -247,7 +250,7 @@ class YourSettings(BaseBootstrapSettings):
247250

248251
All these settings are then passed to [opentelemetry](https://opentelemetry.io/), completing your Opentelemetry integration.
249252

250-
## Logging
253+
### Logging
251254

252255
<b>microbootstrap</b> provides in-memory json logging using [structlog](https://pypi.org/project/structlog/).
253256
To learn more about in-memory logging, check out [MemoryHandler](https://docs.python.org/3/library/logging.handlers.html#memoryhandler)
@@ -280,6 +283,59 @@ Parameters description:
280283
- `logging_extra_processors` - set additional structlog processors if you have some.
281284
- `logging_exclude_endpoints` - remove logging on certain endpoints.
282285

286+
### Swagger
287+
288+
```python
289+
from microbootstrap.bootstrappers.litestar import BaseBootstrapSettings
290+
291+
292+
class YourSettings(BaseBootstrapSettings):
293+
service_name: str = "micro-service"
294+
service_description: str = "Micro service description"
295+
service_version: str = "1.0.0"
296+
service_static_path: str = "/static"
297+
298+
swagger_path: str = "/docs"
299+
swagger_offline_docs: bool = False
300+
swagger_extra_params: dict[str, Any] = {}
301+
```
302+
303+
Parameters description:
304+
305+
- `service_environment` - will be displayed in docs.
306+
- `service_name` - will be displayed in docs.
307+
- `service_description` - will be displayed in docs.
308+
- `service_static_path` - set additional structlog processors if you have some.
309+
- `swagger_path` - path of the docs.
310+
- `swagger_offline_docs` - makes swagger js bundles access offline, because service starts to host via static.
311+
- `swagger_extra_params` - additional params to pass into openapi config.
312+
313+
### Cors
314+
315+
```python
316+
from microbootstrap.bootstrappers.litestar import BaseBootstrapSettings
317+
318+
319+
class YourSettings(BaseBootstrapSettings):
320+
cors_allowed_origins: list[str] = pydantic.Field(default_factory=list)
321+
cors_allowed_methods: list[str] = pydantic.Field(default_factory=list)
322+
cors_allowed_headers: list[str] = pydantic.Field(default_factory=list)
323+
cors_exposed_headers: list[str] = pydantic.Field(default_factory=list)
324+
cors_allowed_credentials: bool = False
325+
cors_allowed_origin_regex: str | None = None
326+
cors_max_age: int = 600
327+
```
328+
329+
Parameters description:
330+
331+
- `cors_allowed_origins` - list of origins that are allowed.
332+
- `cors_allowed_methods` - list of allowed HTTP methods.
333+
- `cors_allowed_headers` - list of allowed headers.
334+
- `cors_exposed_headers` - list of headers that are exposed via the 'Access-Control-Expose-Headers' header.
335+
- `cors_allowed_credentials` - boolean dictating whether or not to set the 'Access-Control-Allow-Credentials' header.
336+
- `cors_allowed_origin_regex` - regex to match origins against.
337+
- `cors_max_age` - response caching TTL in seconds, defaults to 600.
338+
283339
## Configuration
284340

285341
Despite settings being pretty convenient mechanism, it's not always possible to store everything in settings.
@@ -294,6 +350,8 @@ To configure instruemt manually, you have to import one of available configs fro
294350
- `OpentelemetryConfig`
295351
- `PrometheusConfig`
296352
- `LoggingConfig`
353+
- `SwaggerConfig`
354+
- `CorsConfig`
297355

298356
And pass them into `.configure_instrument` or `.configure_instruments` bootstrapper method.
299357

@@ -428,8 +486,8 @@ from microbootstrap.instruments.base import Instrument
428486
429487
430488
class MyInstrument(Instrument[MyInstrumentConfig]):
431-
def write_status(self, console_writer: ConsoleWriter) -> None:
432-
pass
489+
instrument_name: str
490+
ready_condition: str
433491
434492
def is_ready(self) -> bool:
435493
pass
@@ -447,10 +505,16 @@ class MyInstrument(Instrument[MyInstrumentConfig]):
447505
448506
And now you can define behaviour of your instrument
449507
450-
- `write_status` - writes status to console, indicating, is instrument bootstrapped.
451-
- `is_ready` - defines ready for bootstrapping state of instrument, based on it's config values.
452-
- `teardown` - graceful shutdown for instrument during application shutdown.
453-
- `bootstrap` - main instrument's logic.
508+
Attributes:
509+
510+
- `instrument_name` - Will be displayed in your console during bootstrap.
511+
- `ready_condition` - Will be displayed in your console during bootstrap if instument is not ready.
512+
513+
Methods:
514+
515+
- `is_ready` - defines ready for bootstrapping state of instrument, based on it's config values. Required.
516+
- `teardown` - graceful shutdown for instrument during application shutdown. Not required.
517+
- `bootstrap` - main instrument's logic. Not required.
454518
455519
When you have a carcass of instrument, you can adapt it for every framework existing.
456520
Let's adapt it for litestar for example

microbootstrap/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from microbootstrap.instruments.cors_instrument import CorsConfig
12
from microbootstrap.instruments.logging_instrument import LoggingConfig
23
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig
34
from microbootstrap.instruments.prometheus_instrument import PrometheusConfig
45
from microbootstrap.instruments.sentry_instrument import SentryConfig
6+
from microbootstrap.instruments.swagger_instrument import SwaggerConfig
57
from microbootstrap.settings import LitestarSettings
68

79

@@ -12,4 +14,6 @@
1214
"LoggingConfig",
1315
"LitestarBootstrapper",
1416
"LitestarSettings",
17+
"CorsConfig",
18+
"SwaggerConfig",
1519
)

microbootstrap/bootstrappers/litestar.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55
import litestar.types
66
import sentry_sdk
77
import typing_extensions
8-
from litestar import status_codes
8+
from litestar import openapi, status_codes
99
from litestar.config.app import AppConfig as LitestarConfig
10+
from litestar.config.cors import CORSConfig as LitestarCorsConfig
1011
from litestar.contrib.opentelemetry.config import OpenTelemetryConfig as LitestarOpentelemetryConfig
1112
from litestar.contrib.prometheus import PrometheusConfig as LitestarPrometheusConfig
1213
from litestar.contrib.prometheus import PrometheusController
1314
from litestar.exceptions.http_exceptions import HTTPException
15+
from litestar_offline_docs import generate_static_files_config
1416

1517
from microbootstrap.bootstrappers.base import ApplicationBootstrapper
18+
from microbootstrap.instruments.cors_instrument import CorsInstrument
1619
from microbootstrap.instruments.logging_instrument import LoggingInstrument
1720
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
1821
from microbootstrap.instruments.prometheus_instrument import PrometheusInstrument
1922
from microbootstrap.instruments.sentry_instrument import SentryInstrument
23+
from microbootstrap.instruments.swagger_instrument import SwaggerInstrument
2024
from microbootstrap.middlewares.litestar import build_litestar_logging_middleware
2125
from microbootstrap.settings import LitestarSettings
2226

@@ -52,6 +56,53 @@ def bootstrap_before(self) -> dict[str, typing.Any]:
5256
return {"after_exception": [self.sentry_exception_catcher_hook]}
5357

5458

59+
@LitestarBootstrapper.use_instrument()
60+
class LitestarSwaggerInstrument(SwaggerInstrument):
61+
def bootstrap_before(self) -> dict[str, typing.Any]:
62+
class LitestarOpenAPIController(openapi.OpenAPIController):
63+
path = self.instrument_config.swagger_path
64+
if self.instrument_config.swagger_offline_docs:
65+
swagger_ui_standalone_preset_js_url = (
66+
f"{self.instrument_config.service_static_path}/swagger-ui-standalone-preset.js"
67+
)
68+
swagger_bundle_path: str = f"{self.instrument_config.service_static_path}/swagger-ui-bundle.js"
69+
swagger_css_url: str = f"{self.instrument_config.service_static_path}/swagger-ui.css"
70+
71+
openapi_config: typing.Final = openapi.OpenAPIConfig(
72+
title=self.instrument_config.service_name,
73+
version=self.instrument_config.service_version,
74+
description=self.instrument_config.service_description,
75+
openapi_controller=LitestarOpenAPIController,
76+
**self.instrument_config.swagger_extra_params,
77+
)
78+
79+
bootstrap_result = {}
80+
if self.instrument_config.swagger_offline_docs:
81+
bootstrap_result["static_files_config"] = [
82+
generate_static_files_config(static_files_handler_path=self.instrument_config.service_static_path),
83+
]
84+
return {
85+
**bootstrap_result,
86+
"openapi_config": openapi_config,
87+
}
88+
89+
90+
@LitestarBootstrapper.use_instrument()
91+
class LitestarCorsInstrument(CorsInstrument):
92+
def bootstrap_before(self) -> dict[str, typing.Any]:
93+
return {
94+
"cors_config": LitestarCorsConfig(
95+
allow_origins=self.instrument_config.cors_allowed_origins,
96+
allow_methods=self.instrument_config.cors_allowed_methods,
97+
allow_headers=self.instrument_config.cors_allowed_headers,
98+
allow_credentials=self.instrument_config.cors_allowed_credentials,
99+
allow_origin_regex=self.instrument_config.cors_allowed_origin_regex,
100+
expose_headers=self.instrument_config.cors_exposed_headers,
101+
max_age=self.instrument_config.cors_max_age,
102+
),
103+
}
104+
105+
55106
@LitestarBootstrapper.use_instrument()
56107
class LitetstarOpentelemetryInstrument(OpentelemetryInstrument):
57108
def bootstrap_before(self) -> dict[str, typing.Any]:
Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +0,0 @@
1-
from microbootstrap.instruments.logging_instrument import LoggingConfig
2-
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryConfig
3-
from microbootstrap.instruments.prometheus_instrument import PrometheusConfig
4-
from microbootstrap.instruments.sentry_instrument import SentryConfig
5-
6-
7-
__all__ = ("SentryConfig", "OpentelemetryConfig", "PrometheusConfig", "LoggingConfig")

microbootstrap/instruments/base.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,37 @@ class BaseInstrumentConfig(pydantic.BaseModel):
2323
@dataclasses.dataclass
2424
class Instrument(abc.ABC, typing.Generic[InstrumentConfigT]):
2525
instrument_config: InstrumentConfigT
26+
instrument_name: typing.ClassVar[str]
27+
ready_condition: typing.ClassVar[str]
2628

2729
def configure_instrument(
2830
self,
2931
incoming_config: InstrumentConfigT,
3032
) -> None:
3133
self.instrument_config = merge_pydantic_configs(self.instrument_config, incoming_config)
3234

33-
@abc.abstractmethod
3435
def write_status(self, console_writer: ConsoleWriter) -> None:
35-
raise NotImplementedError
36+
console_writer.write_instrument_status(
37+
self.instrument_name,
38+
is_enabled=self.is_ready(),
39+
disable_reason=None if self.is_ready() else self.ready_condition,
40+
)
3641

3742
@abc.abstractmethod
3843
def is_ready(self) -> bool:
3944
raise NotImplementedError
4045

41-
@abc.abstractmethod
42-
def bootstrap(self) -> None:
43-
raise NotImplementedError
44-
45-
@abc.abstractmethod
46-
def teardown(self) -> None:
47-
raise NotImplementedError
48-
4946
@classmethod
5047
@abc.abstractmethod
5148
def get_config_type(cls) -> type[InstrumentConfigT]:
5249
raise NotImplementedError
5350

51+
def bootstrap(self) -> None:
52+
return None
53+
54+
def teardown(self) -> None:
55+
return None
56+
5457
def bootstrap_before(self) -> dict[str, typing.Any]:
5558
"""Add some framework-related parameters to final bootstrap result before application creation."""
5659
return {}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
import pydantic
4+
5+
from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
6+
7+
8+
class CorsConfig(BaseInstrumentConfig):
9+
cors_allowed_origins: list[str] = pydantic.Field(default_factory=list)
10+
cors_allowed_methods: list[str] = pydantic.Field(default_factory=list)
11+
cors_allowed_headers: list[str] = pydantic.Field(default_factory=list)
12+
cors_exposed_headers: list[str] = pydantic.Field(default_factory=list)
13+
cors_allowed_credentials: bool = False
14+
cors_allowed_origin_regex: str | None = None
15+
cors_max_age: int = 600
16+
17+
18+
class CorsInstrument(Instrument[CorsConfig]):
19+
instrument_name = "Cors"
20+
ready_condition = "Provide allowed origins or regex"
21+
22+
def is_ready(self) -> bool:
23+
return bool(self.instrument_config.cors_allowed_origins) or bool(
24+
self.instrument_config.cors_allowed_origin_regex,
25+
)
26+
27+
@classmethod
28+
def get_config_type(cls) -> type[CorsConfig]:
29+
return CorsConfig

microbootstrap/instruments/logging_instrument.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import litestar
1818
from structlog.typing import EventDict, WrappedLogger
1919

20-
from microbootstrap.console_writer import ConsoleWriter
21-
2220

2321
ScopeType = typing.MutableMapping[str, typing.Any]
2422

@@ -120,26 +118,19 @@ def __call__(self, *args: typing.Any) -> logging.Logger: # noqa: ANN401
120118
class LoggingConfig(BaseInstrumentConfig):
121119
service_debug: bool = True
122120

123-
logging_log_level: int = pydantic.Field(default=logging.INFO)
124-
logging_flush_level: int = pydantic.Field(default=logging.ERROR)
125-
logging_buffer_capacity: int = pydantic.Field(default=10)
121+
logging_log_level: int = logging.INFO
122+
logging_flush_level: int = logging.ERROR
123+
logging_buffer_capacity: int = 10
126124
logging_extra_processors: list[typing.Any] = pydantic.Field(default_factory=list)
127125
logging_unset_handlers: list[str] = pydantic.Field(
128126
default_factory=lambda: ["uvicorn", "uvicorn.access"],
129127
)
130-
logging_exclude_endpoints: list[str] = pydantic.Field(default_factory=lambda: ["/health"])
128+
logging_exclude_endpoints: list[str] = pydantic.Field(default_factory=list)
131129

132130

133131
class LoggingInstrument(Instrument[LoggingConfig]):
134-
def write_status(self, console_writer: ConsoleWriter) -> None:
135-
if self.is_ready():
136-
console_writer.write_instrument_status("Logging", is_enabled=True)
137-
else:
138-
console_writer.write_instrument_status(
139-
"Logging",
140-
is_enabled=False,
141-
disable_reason="Works only in non-debug mode",
142-
)
132+
instrument_name = "Logging"
133+
ready_condition = "Works only in non-debug mode"
143134

144135
def is_ready(self) -> bool:
145136
return not self.instrument_config.service_debug

0 commit comments

Comments
 (0)