microbootstrap 1.5.4__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/PKG-INFO +64 -6
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/README.md +59 -1
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/__init__.py +20 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/fastapi.py +42 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/faststream.py +11 -1
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/litestar.py +117 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/helpers.py +1 -1
- microbootstrap-1.6.0/microbootstrap/instruments/openapi_security_schemes.py +127 -0
- microbootstrap-1.6.0/microbootstrap/instruments/openapi_version_docs.py +86 -0
- microbootstrap-1.6.0/microbootstrap/instruments/swagger_instrument.py +121 -0
- microbootstrap-1.6.0/microbootstrap/middlewares/faststream.py +66 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/pyproject.toml +5 -5
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/pyproject.toml.orig +5 -5
- microbootstrap-1.5.4/microbootstrap/instruments/swagger_instrument.py +0 -30
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/__init__.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/base.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/bootstrappers/fastmcp.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/config/__init__.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/config/fastapi.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/config/fastmcp.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/config/faststream.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/config/litestar.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/console_writer.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/exceptions.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/granian_server.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/__init__.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/base.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/cors_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/health_checks_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/instrument_box.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/logging_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/opentelemetry_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/prometheus_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/pyroscope_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/sentry_instrument.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments_setupper.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/middlewares/__init__.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/middlewares/fastapi.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/middlewares/fastmcp.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/middlewares/litestar.py +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/py.typed +0 -0
- {microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: microbootstrap
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Package for bootstrapping new micro-services
|
|
5
5
|
Keywords: python,microservice,bootstrap,opentelemetry,logging,error-tracing,litestar,fastapi,fastmcp,mcp
|
|
6
6
|
Author: community-of-python
|
|
@@ -43,16 +43,16 @@ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.54b1
|
|
|
43
43
|
Requires-Dist: opentelemetry-instrumentation-asyncio>=0.54b1
|
|
44
44
|
Requires-Dist: opentelemetry-instrumentation-asgi>=0.46b0
|
|
45
45
|
Requires-Dist: orjson>=3.10.18
|
|
46
|
-
Requires-Dist: fastapi>=0.
|
|
46
|
+
Requires-Dist: fastapi>=0.110.1 ; extra == 'fastapi'
|
|
47
47
|
Requires-Dist: fastapi-offline-docs>=1 ; extra == 'fastapi'
|
|
48
48
|
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.46b0 ; extra == 'fastapi'
|
|
49
|
-
Requires-Dist: prometheus-fastapi-instrumentator>=
|
|
49
|
+
Requires-Dist: prometheus-fastapi-instrumentator>=7.1 ; extra == 'fastapi'
|
|
50
50
|
Requires-Dist: fastmcp>=2,<4 ; extra == 'fastmcp'
|
|
51
51
|
Requires-Dist: prometheus-client>=0.20 ; extra == 'fastmcp'
|
|
52
|
-
Requires-Dist: faststream
|
|
52
|
+
Requires-Dist: faststream>=0.6.7,<0.8 ; extra == 'faststream'
|
|
53
53
|
Requires-Dist: prometheus-client>=0.20 ; extra == 'faststream'
|
|
54
54
|
Requires-Dist: granian[reload]>=1 ; extra == 'granian'
|
|
55
|
-
Requires-Dist: litestar>=2.
|
|
55
|
+
Requires-Dist: litestar>=2.21.1 ; extra == 'litestar'
|
|
56
56
|
Requires-Dist: litestar-offline-docs>=1 ; extra == 'litestar'
|
|
57
57
|
Requires-Dist: prometheus-client>=0.20 ; extra == 'litestar'
|
|
58
58
|
Requires-Python: >=3.10, <4
|
|
@@ -105,7 +105,7 @@ With <b>microbootstrap</b>, you receive an application with lightweight built-in
|
|
|
105
105
|
- `opentelemetry`
|
|
106
106
|
- `logging`
|
|
107
107
|
- `cors`
|
|
108
|
-
- `swagger` -
|
|
108
|
+
- `swagger` - offline UI assets, OpenAPI security definitions, and optional Accept-version documentation
|
|
109
109
|
- `health-checks`
|
|
110
110
|
|
|
111
111
|
Those instruments can be bootstrapped for:
|
|
@@ -659,6 +659,64 @@ Parameter descriptions:
|
|
|
659
659
|
- `swagger_offline_docs` - A boolean value that, when set to True, allows the Swagger JS bundles to be accessed offline. This is because the service starts to host via static.
|
|
660
660
|
- `swagger_extra_params` - Additional parameters to pass into the OpenAPI configuration.
|
|
661
661
|
|
|
662
|
+
#### OpenAPI security schemes
|
|
663
|
+
|
|
664
|
+
Security schemes are disabled by default. They add reusable OpenAPI definitions under
|
|
665
|
+
`components.securitySchemes`; they do not authenticate requests or add global or operation-level security requirements.
|
|
666
|
+
Keep requirements and authentication in your application routes and dependencies.
|
|
667
|
+
|
|
668
|
+
```python
|
|
669
|
+
from microbootstrap import (
|
|
670
|
+
LitestarSettings,
|
|
671
|
+
OpenApiHttpSecurityScheme,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class Settings(LitestarSettings):
|
|
676
|
+
security_schemes: dict[str, OpenApiHttpSecurityScheme] = {
|
|
677
|
+
"serviceAuth": OpenApiHttpSecurityScheme(scheme="bearer", bearer_format="JWT"),
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
HTTP, API key, OAuth 2.0, and OpenID Connect definitions are supported by `SwaggerConfig`. Annotating a consumer
|
|
682
|
+
setting with a concrete scheme class intentionally rejects other kinds for that consumer. Python field names and
|
|
683
|
+
OpenAPI aliases are accepted; output uses canonical names such as `bearerFormat`, `in`, `tokenUrl`, and
|
|
684
|
+
`openIdConnectUrl`. A same-named definition must be identical to the service-owned definition or schema generation
|
|
685
|
+
raises `ValueError`.
|
|
686
|
+
|
|
687
|
+
The definitions are added to the framework's normal schema. Repeated schema reads remain stable; after correcting
|
|
688
|
+
a configuration conflict, rebuild the application.
|
|
689
|
+
|
|
690
|
+
#### API-version documentation
|
|
691
|
+
|
|
692
|
+
Version documentation is disabled by default. It describes supported versions in OpenAPI without implementing
|
|
693
|
+
runtime version negotiation.
|
|
694
|
+
|
|
695
|
+
```python
|
|
696
|
+
from microbootstrap import LitestarSettings, OpenApiOperationVersionOverride, OpenApiVersionDocsConfig
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
class Settings(LitestarSettings):
|
|
700
|
+
openapi_version_docs: OpenApiVersionDocsConfig | None = OpenApiVersionDocsConfig(
|
|
701
|
+
vendor_media_type="application/vnd.example+json",
|
|
702
|
+
supported_versions=("1.0",),
|
|
703
|
+
operation_versions=(
|
|
704
|
+
OpenApiOperationVersionOverride(path="/widgets", method="post", supported_versions=("2.0",)),
|
|
705
|
+
OpenApiOperationVersionOverride(path="/internal/widgets", method="get", supported_versions=()),
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Set `openapi_version_docs` to `None` to disable version documentation. A configured non-empty global
|
|
711
|
+
`supported_versions` list adds an `x-accept-versioning` extension and matching description text to each operation.
|
|
712
|
+
`operation_versions` replaces that list for one exact path and lower-case HTTP method; an explicit empty tuple skips
|
|
713
|
+
microbootstrap's additions for that operation without asserting that no service-owned version metadata exists. This does
|
|
714
|
+
not negotiate requests, add an `Accept` parameter, change response media types, or provide a Swagger UI version selector.
|
|
715
|
+
|
|
716
|
+
Repeated schema reads remain stable. A conflicting service-owned `x-accept-versioning` extension raises `ValueError`;
|
|
717
|
+
correct the configuration and rebuild the application. Litestar supports its standard `Operation` type for version
|
|
718
|
+
documentation and rejects other custom operation subclasses.
|
|
719
|
+
|
|
662
720
|
#### FastStream AsyncAPI documentation
|
|
663
721
|
|
|
664
722
|
AsyncAPI documentation is available by default under `/asyncapi` route. You can change that by setting `asyncapi_path`:
|
|
@@ -40,7 +40,7 @@ With <b>microbootstrap</b>, you receive an application with lightweight built-in
|
|
|
40
40
|
- `opentelemetry`
|
|
41
41
|
- `logging`
|
|
42
42
|
- `cors`
|
|
43
|
-
- `swagger` -
|
|
43
|
+
- `swagger` - offline UI assets, OpenAPI security definitions, and optional Accept-version documentation
|
|
44
44
|
- `health-checks`
|
|
45
45
|
|
|
46
46
|
Those instruments can be bootstrapped for:
|
|
@@ -594,6 +594,64 @@ Parameter descriptions:
|
|
|
594
594
|
- `swagger_offline_docs` - A boolean value that, when set to True, allows the Swagger JS bundles to be accessed offline. This is because the service starts to host via static.
|
|
595
595
|
- `swagger_extra_params` - Additional parameters to pass into the OpenAPI configuration.
|
|
596
596
|
|
|
597
|
+
#### OpenAPI security schemes
|
|
598
|
+
|
|
599
|
+
Security schemes are disabled by default. They add reusable OpenAPI definitions under
|
|
600
|
+
`components.securitySchemes`; they do not authenticate requests or add global or operation-level security requirements.
|
|
601
|
+
Keep requirements and authentication in your application routes and dependencies.
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
from microbootstrap import (
|
|
605
|
+
LitestarSettings,
|
|
606
|
+
OpenApiHttpSecurityScheme,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class Settings(LitestarSettings):
|
|
611
|
+
security_schemes: dict[str, OpenApiHttpSecurityScheme] = {
|
|
612
|
+
"serviceAuth": OpenApiHttpSecurityScheme(scheme="bearer", bearer_format="JWT"),
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
HTTP, API key, OAuth 2.0, and OpenID Connect definitions are supported by `SwaggerConfig`. Annotating a consumer
|
|
617
|
+
setting with a concrete scheme class intentionally rejects other kinds for that consumer. Python field names and
|
|
618
|
+
OpenAPI aliases are accepted; output uses canonical names such as `bearerFormat`, `in`, `tokenUrl`, and
|
|
619
|
+
`openIdConnectUrl`. A same-named definition must be identical to the service-owned definition or schema generation
|
|
620
|
+
raises `ValueError`.
|
|
621
|
+
|
|
622
|
+
The definitions are added to the framework's normal schema. Repeated schema reads remain stable; after correcting
|
|
623
|
+
a configuration conflict, rebuild the application.
|
|
624
|
+
|
|
625
|
+
#### API-version documentation
|
|
626
|
+
|
|
627
|
+
Version documentation is disabled by default. It describes supported versions in OpenAPI without implementing
|
|
628
|
+
runtime version negotiation.
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
from microbootstrap import LitestarSettings, OpenApiOperationVersionOverride, OpenApiVersionDocsConfig
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class Settings(LitestarSettings):
|
|
635
|
+
openapi_version_docs: OpenApiVersionDocsConfig | None = OpenApiVersionDocsConfig(
|
|
636
|
+
vendor_media_type="application/vnd.example+json",
|
|
637
|
+
supported_versions=("1.0",),
|
|
638
|
+
operation_versions=(
|
|
639
|
+
OpenApiOperationVersionOverride(path="/widgets", method="post", supported_versions=("2.0",)),
|
|
640
|
+
OpenApiOperationVersionOverride(path="/internal/widgets", method="get", supported_versions=()),
|
|
641
|
+
),
|
|
642
|
+
)
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Set `openapi_version_docs` to `None` to disable version documentation. A configured non-empty global
|
|
646
|
+
`supported_versions` list adds an `x-accept-versioning` extension and matching description text to each operation.
|
|
647
|
+
`operation_versions` replaces that list for one exact path and lower-case HTTP method; an explicit empty tuple skips
|
|
648
|
+
microbootstrap's additions for that operation without asserting that no service-owned version metadata exists. This does
|
|
649
|
+
not negotiate requests, add an `Accept` parameter, change response media types, or provide a Swagger UI version selector.
|
|
650
|
+
|
|
651
|
+
Repeated schema reads remain stable. A conflicting service-owned `x-accept-versioning` extension raises `ValueError`;
|
|
652
|
+
correct the configuration and rebuild the application. Litestar supports its standard `Operation` type for version
|
|
653
|
+
documentation and rejects other custom operation subclasses.
|
|
654
|
+
|
|
597
655
|
#### FastStream AsyncAPI documentation
|
|
598
656
|
|
|
599
657
|
AsyncAPI documentation is available by default under `/asyncapi` route. You can change that by setting `asyncapi_path`:
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
from microbootstrap.instruments.cors_instrument import CorsConfig
|
|
2
2
|
from microbootstrap.instruments.health_checks_instrument import HealthChecksConfig
|
|
3
3
|
from microbootstrap.instruments.logging_instrument import LoggingConfig
|
|
4
|
+
from microbootstrap.instruments.openapi_security_schemes import (
|
|
5
|
+
OpenApiApiKeySecurityScheme,
|
|
6
|
+
OpenApiHttpSecurityScheme,
|
|
7
|
+
OpenApiOAuth2SecurityScheme,
|
|
8
|
+
OpenApiOAuthFlow,
|
|
9
|
+
OpenApiOAuthFlows,
|
|
10
|
+
OpenApiOpenIdConnectSecurityScheme,
|
|
11
|
+
)
|
|
12
|
+
from microbootstrap.instruments.openapi_version_docs import (
|
|
13
|
+
OpenApiOperationVersionOverride,
|
|
14
|
+
OpenApiVersionDocsConfig,
|
|
15
|
+
)
|
|
4
16
|
from microbootstrap.instruments.opentelemetry_instrument import (
|
|
5
17
|
FastStreamOpentelemetryConfig,
|
|
6
18
|
FastStreamTelemetryMiddlewareProtocol,
|
|
@@ -42,6 +54,14 @@ __all__ = (
|
|
|
42
54
|
"LitestarPrometheusConfig",
|
|
43
55
|
"LitestarSettings",
|
|
44
56
|
"LoggingConfig",
|
|
57
|
+
"OpenApiApiKeySecurityScheme",
|
|
58
|
+
"OpenApiHttpSecurityScheme",
|
|
59
|
+
"OpenApiOAuth2SecurityScheme",
|
|
60
|
+
"OpenApiOAuthFlow",
|
|
61
|
+
"OpenApiOAuthFlows",
|
|
62
|
+
"OpenApiOpenIdConnectSecurityScheme",
|
|
63
|
+
"OpenApiOperationVersionOverride",
|
|
64
|
+
"OpenApiVersionDocsConfig",
|
|
45
65
|
"OpentelemetryConfig",
|
|
46
66
|
"PyroscopeConfig",
|
|
47
67
|
"SentryConfig",
|
|
@@ -12,6 +12,8 @@ from microbootstrap.config.fastapi import FastApiConfig
|
|
|
12
12
|
from microbootstrap.instruments.cors_instrument import CorsInstrument
|
|
13
13
|
from microbootstrap.instruments.health_checks_instrument import HealthChecksInstrument, HealthCheckTypedDict
|
|
14
14
|
from microbootstrap.instruments.logging_instrument import LoggingInstrument
|
|
15
|
+
from microbootstrap.instruments.openapi_security_schemes import serialize_security_schemes
|
|
16
|
+
from microbootstrap.instruments.openapi_version_docs import SUPPORTED_HTTP_METHODS
|
|
15
17
|
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
|
|
16
18
|
from microbootstrap.instruments.prometheus_instrument import FastApiPrometheusConfig, PrometheusInstrument
|
|
17
19
|
from microbootstrap.instruments.pyroscope_instrument import PyroscopeInstrument
|
|
@@ -67,8 +69,48 @@ class FastApiSwaggerInstrument(SwaggerInstrument):
|
|
|
67
69
|
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
68
70
|
if self.instrument_config.swagger_offline_docs:
|
|
69
71
|
enable_offline_docs(application, static_files_handler=self.instrument_config.service_static_path)
|
|
72
|
+
if self.instrument_config.openapi_version_docs is None and not self.instrument_config.security_schemes:
|
|
73
|
+
return application
|
|
74
|
+
|
|
75
|
+
original_openapi: typing.Final = application.openapi
|
|
76
|
+
|
|
77
|
+
def documented_openapi() -> dict[str, typing.Any]:
|
|
78
|
+
openapi_schema: typing.Final = original_openapi()
|
|
79
|
+
if self.instrument_config.security_schemes:
|
|
80
|
+
self._merge_security_schemes(openapi_schema)
|
|
81
|
+
if self.instrument_config.openapi_version_docs is not None:
|
|
82
|
+
self._document_operations(openapi_schema)
|
|
83
|
+
return openapi_schema
|
|
84
|
+
|
|
85
|
+
application.openapi = documented_openapi # type: ignore[method-assign] # FastAPI's public custom OpenAPI hook.
|
|
70
86
|
return application
|
|
71
87
|
|
|
88
|
+
def _merge_security_schemes(self, openapi_schema: dict[str, typing.Any]) -> None:
|
|
89
|
+
expected_schemes: typing.Final = serialize_security_schemes(self.instrument_config.security_schemes)
|
|
90
|
+
components = openapi_schema.setdefault("components", {})
|
|
91
|
+
security_schemes = components.setdefault("securitySchemes", {})
|
|
92
|
+
self._validate_security_scheme_conflicts(security_schemes, expected_schemes)
|
|
93
|
+
security_schemes.update(
|
|
94
|
+
{name: scheme for name, scheme in expected_schemes.items() if name not in security_schemes}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _document_operations(self, openapi_schema: dict[str, typing.Any]) -> None:
|
|
98
|
+
for path, path_item in openapi_schema.get("paths", {}).items():
|
|
99
|
+
if path.startswith("x-"):
|
|
100
|
+
continue
|
|
101
|
+
for method, operation in path_item.items():
|
|
102
|
+
if method not in SUPPORTED_HTTP_METHODS:
|
|
103
|
+
continue
|
|
104
|
+
documentation = self._build_version_documentation(
|
|
105
|
+
path,
|
|
106
|
+
method,
|
|
107
|
+
operation.get("description"),
|
|
108
|
+
operation.get("x-accept-versioning"),
|
|
109
|
+
has_existing_extension="x-accept-versioning" in operation,
|
|
110
|
+
)
|
|
111
|
+
if documentation is not None:
|
|
112
|
+
operation["x-accept-versioning"], operation["description"] = documentation
|
|
113
|
+
|
|
72
114
|
|
|
73
115
|
@FastApiBootstrapper.use_instrument()
|
|
74
116
|
class FastApiCorsInstrument(CorsInstrument):
|
|
@@ -24,6 +24,7 @@ from microbootstrap.instruments.opentelemetry_instrument import (
|
|
|
24
24
|
from microbootstrap.instruments.prometheus_instrument import FastStreamPrometheusConfig, PrometheusInstrument
|
|
25
25
|
from microbootstrap.instruments.pyroscope_instrument import PyroscopeInstrument
|
|
26
26
|
from microbootstrap.instruments.sentry_instrument import SentryInstrument
|
|
27
|
+
from microbootstrap.middlewares.faststream import FastStreamOpenTelemetryBaggageMiddleware
|
|
27
28
|
from microbootstrap.settings import FastStreamSettings
|
|
28
29
|
|
|
29
30
|
|
|
@@ -56,7 +57,11 @@ def _isolate_faststream_subscribers(application: AsgiFastStream) -> None:
|
|
|
56
57
|
class KwargsAsgiFastStream(AsgiFastStream):
|
|
57
58
|
def __init__(self, **kwargs: typing.Any) -> None: # noqa: ANN401
|
|
58
59
|
# `broker` argument is positional-only
|
|
59
|
-
|
|
60
|
+
broker = kwargs.pop("broker", None)
|
|
61
|
+
if broker is None:
|
|
62
|
+
super().__init__(**kwargs)
|
|
63
|
+
else:
|
|
64
|
+
super().__init__(broker, **kwargs)
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
class FastStreamBootstrapper(ApplicationBootstrapper[FastStreamSettings, AsgiFastStream, FastStreamConfig]):
|
|
@@ -101,6 +106,11 @@ class FastStreamOpentelemetryInstrument(BaseOpentelemetryInstrument[FastStreamOp
|
|
|
101
106
|
application.broker.add_middleware(
|
|
102
107
|
self.instrument_config.opentelemetry_middleware_cls(tracer_provider=self.tracer_provider),
|
|
103
108
|
)
|
|
109
|
+
application.broker.add_middleware(
|
|
110
|
+
FastStreamOpenTelemetryBaggageMiddleware(
|
|
111
|
+
baggage_span_attributes=self.instrument_config.opentelemetry_baggage_span_attributes,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
104
114
|
return application
|
|
105
115
|
|
|
106
116
|
@classmethod
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import dataclasses
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
5
|
import litestar
|
|
@@ -25,6 +26,15 @@ from microbootstrap.instruments.health_checks_instrument import (
|
|
|
25
26
|
HealthCheckTypedDict,
|
|
26
27
|
)
|
|
27
28
|
from microbootstrap.instruments.logging_instrument import LoggingInstrument
|
|
29
|
+
from microbootstrap.instruments.openapi_security_schemes import (
|
|
30
|
+
OpenApiApiKeySecurityScheme,
|
|
31
|
+
OpenApiHttpSecurityScheme,
|
|
32
|
+
OpenApiOAuth2SecurityScheme,
|
|
33
|
+
OpenApiOpenIdConnectSecurityScheme,
|
|
34
|
+
_OpenApiSecurityScheme,
|
|
35
|
+
serialize_security_schemes,
|
|
36
|
+
)
|
|
37
|
+
from microbootstrap.instruments.openapi_version_docs import SUPPORTED_HTTP_METHODS
|
|
28
38
|
from microbootstrap.instruments.opentelemetry_instrument import OpentelemetryInstrument
|
|
29
39
|
from microbootstrap.instruments.prometheus_instrument import (
|
|
30
40
|
LitestarPrometheusConfig,
|
|
@@ -37,6 +47,17 @@ from microbootstrap.middlewares.litestar import build_litestar_logging_middlewar
|
|
|
37
47
|
from microbootstrap.settings import LitestarSettings
|
|
38
48
|
|
|
39
49
|
|
|
50
|
+
ApplicationT = typing.TypeVar("ApplicationT", bound=litestar.Litestar)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass
|
|
54
|
+
class AcceptVersionedOperation(openapi.spec.Operation):
|
|
55
|
+
accept_versioning: dict[str, str | list[str]] | None = dataclasses.field(
|
|
56
|
+
default=None,
|
|
57
|
+
metadata={"alias": "x-accept-versioning"},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
40
61
|
if typing.TYPE_CHECKING:
|
|
41
62
|
from litestar.contrib.opentelemetry import OpenTelemetryConfig
|
|
42
63
|
from litestar.types import ASGIApp, Scope
|
|
@@ -102,6 +123,102 @@ class LitestarSwaggerInstrument(SwaggerInstrument):
|
|
|
102
123
|
]
|
|
103
124
|
return bootstrap_result
|
|
104
125
|
|
|
126
|
+
def bootstrap_after(self, application: ApplicationT) -> ApplicationT:
|
|
127
|
+
if (self.instrument_config.openapi_version_docs is None and not self.instrument_config.security_schemes) or (
|
|
128
|
+
application.openapi_config is None
|
|
129
|
+
):
|
|
130
|
+
return application
|
|
131
|
+
if self.instrument_config.security_schemes:
|
|
132
|
+
self._merge_security_schemes(application.openapi_schema)
|
|
133
|
+
if self.instrument_config.openapi_version_docs is not None:
|
|
134
|
+
self._document_operations(application.openapi_schema)
|
|
135
|
+
return application
|
|
136
|
+
|
|
137
|
+
def _merge_security_schemes(self, openapi_schema: openapi.spec.OpenAPI) -> None:
|
|
138
|
+
expected_schemes: typing.Final = serialize_security_schemes(self.instrument_config.security_schemes)
|
|
139
|
+
security_schemes = openapi_schema.components.security_schemes
|
|
140
|
+
if security_schemes is not None:
|
|
141
|
+
canonical_schemes: typing.Final = {
|
|
142
|
+
name: scheme.to_schema() if isinstance(scheme, openapi.spec.SecurityScheme) else scheme
|
|
143
|
+
for name, scheme in security_schemes.items()
|
|
144
|
+
}
|
|
145
|
+
self._validate_security_scheme_conflicts(canonical_schemes, expected_schemes)
|
|
146
|
+
configured_schemes = {
|
|
147
|
+
scheme_name: self._build_litestar_security_scheme(security_scheme)
|
|
148
|
+
for scheme_name, security_scheme in self.instrument_config.security_schemes.items()
|
|
149
|
+
}
|
|
150
|
+
if security_schemes is None:
|
|
151
|
+
openapi_schema.components.security_schemes = typing.cast(
|
|
152
|
+
"dict[str, openapi.spec.SecurityScheme | openapi.spec.Reference]",
|
|
153
|
+
configured_schemes,
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
security_schemes.update(
|
|
157
|
+
{name: scheme for name, scheme in configured_schemes.items() if name not in security_schemes}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _document_operations(self, openapi_schema: openapi.spec.OpenAPI) -> None:
|
|
161
|
+
if openapi_schema.paths is None:
|
|
162
|
+
return
|
|
163
|
+
for path, path_item in openapi_schema.paths.items():
|
|
164
|
+
for method in SUPPORTED_HTTP_METHODS:
|
|
165
|
+
operation = getattr(path_item, method)
|
|
166
|
+
if operation is None:
|
|
167
|
+
continue
|
|
168
|
+
existing_extension = (
|
|
169
|
+
operation.accept_versioning if isinstance(operation, AcceptVersionedOperation) else None
|
|
170
|
+
)
|
|
171
|
+
documentation = self._build_version_documentation(
|
|
172
|
+
path,
|
|
173
|
+
method,
|
|
174
|
+
operation.description,
|
|
175
|
+
existing_extension,
|
|
176
|
+
has_existing_extension=existing_extension is not None,
|
|
177
|
+
)
|
|
178
|
+
if documentation is None:
|
|
179
|
+
continue
|
|
180
|
+
extension, description = documentation
|
|
181
|
+
if type(operation) is openapi.spec.Operation:
|
|
182
|
+
init_fields = {
|
|
183
|
+
field.name: getattr(operation, field.name)
|
|
184
|
+
for field in dataclasses.fields(openapi.spec.Operation)
|
|
185
|
+
if field.init
|
|
186
|
+
}
|
|
187
|
+
operation = AcceptVersionedOperation(**init_fields, accept_versioning=extension)
|
|
188
|
+
setattr(path_item, method, operation)
|
|
189
|
+
elif type(operation) is not AcceptVersionedOperation:
|
|
190
|
+
message = (
|
|
191
|
+
f"OpenAPI operation {type(operation).__name__} is not supported "
|
|
192
|
+
"for Accept version documentation."
|
|
193
|
+
)
|
|
194
|
+
raise TypeError(message)
|
|
195
|
+
operation.accept_versioning = extension
|
|
196
|
+
operation.description = description
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _build_litestar_security_scheme(cls, security_scheme: _OpenApiSecurityScheme) -> openapi.spec.SecurityScheme:
|
|
200
|
+
if not isinstance(
|
|
201
|
+
security_scheme,
|
|
202
|
+
(
|
|
203
|
+
OpenApiHttpSecurityScheme,
|
|
204
|
+
OpenApiApiKeySecurityScheme,
|
|
205
|
+
OpenApiOAuth2SecurityScheme,
|
|
206
|
+
OpenApiOpenIdConnectSecurityScheme,
|
|
207
|
+
),
|
|
208
|
+
):
|
|
209
|
+
raise AssertionError("Unsupported OpenAPI security scheme.") # noqa: TRY004
|
|
210
|
+
scheme_data = security_scheme.model_dump(by_alias=False, exclude_none=True)
|
|
211
|
+
if "location" in scheme_data:
|
|
212
|
+
scheme_data["security_scheme_in"] = scheme_data.pop("location")
|
|
213
|
+
if "flows" in scheme_data:
|
|
214
|
+
flow_data = scheme_data["flows"]
|
|
215
|
+
if "resource_owner" in flow_data:
|
|
216
|
+
flow_data["password"] = flow_data.pop("resource_owner")
|
|
217
|
+
scheme_data["flows"] = openapi.spec.OAuthFlows(
|
|
218
|
+
**{flow_name: openapi.spec.OAuthFlow(**flow) for flow_name, flow in flow_data.items()}
|
|
219
|
+
)
|
|
220
|
+
return openapi.spec.SecurityScheme(**scheme_data)
|
|
221
|
+
|
|
105
222
|
|
|
106
223
|
@LitestarBootstrapper.use_instrument()
|
|
107
224
|
class LitestarCorsInstrument(CorsInstrument):
|
|
@@ -26,7 +26,7 @@ def dataclass_to_dict_no_defaults(dataclass_to_convert: "_DataclassT") -> dict[s
|
|
|
26
26
|
if dataclass_field.default != value and isinstance(dataclass_field.default_factory, _MISSING_TYPE):
|
|
27
27
|
conversion_result[dataclass_field.name] = value
|
|
28
28
|
continue
|
|
29
|
-
if value != dataclass_field.default and value != dataclass_field.default_factory(): # type: ignore[
|
|
29
|
+
if value != dataclass_field.default and value != dataclass_field.default_factory(): # type: ignore[operator]
|
|
30
30
|
conversion_result[dataclass_field.name] = value
|
|
31
31
|
|
|
32
32
|
return conversion_result
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
import unicodedata
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
OpenApiApiKeyLocation: typing.TypeAlias = typing.Literal["header", "query", "cookie"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenApiSecuritySchemeModel(pydantic.BaseModel):
|
|
12
|
+
model_config = pydantic.ConfigDict(extra="forbid", populate_by_name=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenApiHttpSecurityScheme(OpenApiSecuritySchemeModel):
|
|
16
|
+
type: typing.Literal["http"] = "http"
|
|
17
|
+
scheme: str
|
|
18
|
+
bearer_format: str | None = pydantic.Field(default=None, alias="bearerFormat")
|
|
19
|
+
description: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OpenApiApiKeySecurityScheme(OpenApiSecuritySchemeModel):
|
|
23
|
+
type: typing.Literal["apiKey"] = "apiKey"
|
|
24
|
+
name: str
|
|
25
|
+
location: OpenApiApiKeyLocation = pydantic.Field(alias="in")
|
|
26
|
+
description: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenApiOAuthFlow(OpenApiSecuritySchemeModel):
|
|
30
|
+
authorization_url: str | None = pydantic.Field(default=None, alias="authorizationUrl")
|
|
31
|
+
token_url: str | None = pydantic.Field(default=None, alias="tokenUrl")
|
|
32
|
+
refresh_url: str | None = pydantic.Field(default=None, alias="refreshUrl")
|
|
33
|
+
scopes: dict[str, str] = pydantic.Field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
@pydantic.field_validator("authorization_url", "token_url", "refresh_url")
|
|
36
|
+
@classmethod
|
|
37
|
+
def validate_url(cls, value: str | None) -> str | None:
|
|
38
|
+
return validate_openapi_url(value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OpenApiOAuthFlows(OpenApiSecuritySchemeModel):
|
|
42
|
+
implicit: OpenApiOAuthFlow | None = None
|
|
43
|
+
resource_owner: OpenApiOAuthFlow | None = pydantic.Field(default=None, alias="password")
|
|
44
|
+
client_credentials: OpenApiOAuthFlow | None = pydantic.Field(default=None, alias="clientCredentials")
|
|
45
|
+
authorization_code: OpenApiOAuthFlow | None = pydantic.Field(default=None, alias="authorizationCode")
|
|
46
|
+
|
|
47
|
+
@pydantic.model_validator(mode="after")
|
|
48
|
+
def validate_required_urls(self) -> OpenApiOAuthFlows:
|
|
49
|
+
if all(
|
|
50
|
+
flow is None
|
|
51
|
+
for flow in (self.implicit, self.resource_owner, self.client_credentials, self.authorization_code)
|
|
52
|
+
):
|
|
53
|
+
message = "OAuth2 flows must configure at least one grant type."
|
|
54
|
+
raise ValueError(message)
|
|
55
|
+
self._validate_flow_urls("implicit", self.implicit, requires_authorization_url=True)
|
|
56
|
+
self._validate_flow_urls("password", self.resource_owner, requires_token_url=True)
|
|
57
|
+
self._validate_flow_urls("client credentials", self.client_credentials, requires_token_url=True)
|
|
58
|
+
self._validate_flow_urls(
|
|
59
|
+
"authorization code",
|
|
60
|
+
self.authorization_code,
|
|
61
|
+
requires_authorization_url=True,
|
|
62
|
+
requires_token_url=True,
|
|
63
|
+
)
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _validate_flow_urls(
|
|
68
|
+
flow_name: str,
|
|
69
|
+
flow: OpenApiOAuthFlow | None,
|
|
70
|
+
*,
|
|
71
|
+
requires_authorization_url: bool = False,
|
|
72
|
+
requires_token_url: bool = False,
|
|
73
|
+
) -> None:
|
|
74
|
+
if flow is None:
|
|
75
|
+
return
|
|
76
|
+
if requires_authorization_url and flow.authorization_url is None:
|
|
77
|
+
message = f"OAuth2 {flow_name} flow requires authorizationUrl."
|
|
78
|
+
raise ValueError(message)
|
|
79
|
+
if requires_token_url and flow.token_url is None:
|
|
80
|
+
message = f"OAuth2 {flow_name} flow requires tokenUrl."
|
|
81
|
+
raise ValueError(message)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OpenApiOAuth2SecurityScheme(OpenApiSecuritySchemeModel):
|
|
85
|
+
type: typing.Literal["oauth2"] = "oauth2"
|
|
86
|
+
flows: OpenApiOAuthFlows
|
|
87
|
+
description: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class OpenApiOpenIdConnectSecurityScheme(OpenApiSecuritySchemeModel):
|
|
91
|
+
type: typing.Literal["openIdConnect"] = "openIdConnect"
|
|
92
|
+
open_id_connect_url: str = pydantic.Field(alias="openIdConnectUrl")
|
|
93
|
+
description: str | None = None
|
|
94
|
+
|
|
95
|
+
@pydantic.field_validator("open_id_connect_url")
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate_url(cls, value: str) -> str:
|
|
98
|
+
validated_value = validate_openapi_url(value)
|
|
99
|
+
assert validated_value is not None # noqa: S101 - this field is required.
|
|
100
|
+
return validated_value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_OpenApiSecurityScheme: typing.TypeAlias = typing.Annotated[
|
|
104
|
+
OpenApiHttpSecurityScheme
|
|
105
|
+
| OpenApiApiKeySecurityScheme
|
|
106
|
+
| OpenApiOAuth2SecurityScheme
|
|
107
|
+
| OpenApiOpenIdConnectSecurityScheme,
|
|
108
|
+
pydantic.Field(discriminator="type"),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_openapi_url(value: str | None) -> str | None:
|
|
113
|
+
if value is None:
|
|
114
|
+
return value
|
|
115
|
+
if not value or any(character.isspace() or unicodedata.category(character) == "Cc" for character in value):
|
|
116
|
+
message = "OpenAPI URL values must be non-empty and contain no whitespace or control characters."
|
|
117
|
+
raise ValueError(message)
|
|
118
|
+
return value
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def serialize_security_schemes(
|
|
122
|
+
security_schemes: typing.Mapping[str, _OpenApiSecurityScheme],
|
|
123
|
+
) -> dict[str, dict[str, typing.Any]]:
|
|
124
|
+
return {
|
|
125
|
+
scheme_name: security_scheme.model_dump(by_alias=True, exclude_none=True)
|
|
126
|
+
for scheme_name, security_scheme in security_schemes.items()
|
|
127
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SUPPORTED_HTTP_METHODS: typing.Final = frozenset({"delete", "get", "head", "options", "patch", "post", "put", "trace"})
|
|
9
|
+
SAFE_MEDIA_TYPE_TOKEN: typing.Final[re.Pattern[str]] = re.compile(r"[!#$%&'*+\-.^_|~0-9A-Za-z]+\Z")
|
|
10
|
+
VENDOR_MEDIA_TYPE: typing.Final = re.compile(r"application/vnd\.([!#$%&'*+\-.^_|~0-9A-Za-z]+)\+json\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenApiOperationVersionOverride(pydantic.BaseModel):
|
|
14
|
+
model_config = pydantic.ConfigDict(extra="forbid")
|
|
15
|
+
|
|
16
|
+
path: str
|
|
17
|
+
method: str
|
|
18
|
+
supported_versions: tuple[str, ...]
|
|
19
|
+
|
|
20
|
+
@pydantic.field_validator("path")
|
|
21
|
+
@classmethod
|
|
22
|
+
def validate_path(cls, value: str) -> str:
|
|
23
|
+
if not value.startswith("/") or "?" in value or "#" in value:
|
|
24
|
+
message = "Operation path must be an absolute path without a query string or fragment."
|
|
25
|
+
raise ValueError(message)
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
@pydantic.field_validator("method")
|
|
29
|
+
@classmethod
|
|
30
|
+
def validate_method(cls, value: str) -> str:
|
|
31
|
+
if value not in SUPPORTED_HTTP_METHODS:
|
|
32
|
+
message = f"Operation method must be one of: {', '.join(sorted(SUPPORTED_HTTP_METHODS))}."
|
|
33
|
+
raise ValueError(message)
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
@pydantic.field_validator("supported_versions")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_supported_versions(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
39
|
+
return validate_versions(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OpenApiVersionDocsConfig(pydantic.BaseModel):
|
|
43
|
+
model_config = pydantic.ConfigDict(extra="forbid")
|
|
44
|
+
|
|
45
|
+
vendor_media_type: str
|
|
46
|
+
supported_versions: tuple[str, ...]
|
|
47
|
+
operation_versions: tuple[OpenApiOperationVersionOverride, ...] = ()
|
|
48
|
+
|
|
49
|
+
@pydantic.field_validator("vendor_media_type")
|
|
50
|
+
@classmethod
|
|
51
|
+
def validate_vendor_media_type(cls, value: str) -> str:
|
|
52
|
+
if VENDOR_MEDIA_TYPE.fullmatch(value) is None:
|
|
53
|
+
message = "Vendor media type must use the application/vnd.<name>+json form."
|
|
54
|
+
raise ValueError(message)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
@pydantic.field_validator("supported_versions")
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_supported_versions(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
60
|
+
validated_versions = validate_versions(value)
|
|
61
|
+
if not validated_versions:
|
|
62
|
+
message = "OpenAPI version documentation requires at least one supported API version."
|
|
63
|
+
raise ValueError(message)
|
|
64
|
+
return validated_versions
|
|
65
|
+
|
|
66
|
+
@pydantic.field_validator("operation_versions")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_operation_versions(
|
|
69
|
+
cls,
|
|
70
|
+
value: tuple[OpenApiOperationVersionOverride, ...],
|
|
71
|
+
) -> tuple[OpenApiOperationVersionOverride, ...]:
|
|
72
|
+
selectors = {(override.path, override.method) for override in value}
|
|
73
|
+
if len(value) != len(selectors):
|
|
74
|
+
message = "Operation version overrides must not contain duplicate path and method pairs."
|
|
75
|
+
raise ValueError(message)
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def validate_versions(value: tuple[str, ...]) -> tuple[str, ...]:
|
|
80
|
+
if len(value) != len(set(value)):
|
|
81
|
+
message = "Supported API versions must not contain duplicates."
|
|
82
|
+
raise ValueError(message)
|
|
83
|
+
if any(SAFE_MEDIA_TYPE_TOKEN.fullmatch(version) is None for version in value):
|
|
84
|
+
message = "Each supported API version must be a non-empty safe media-type token."
|
|
85
|
+
raise ValueError(message)
|
|
86
|
+
return value
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
from microbootstrap.helpers import is_valid_path
|
|
8
|
+
from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
|
|
9
|
+
from microbootstrap.instruments.openapi_security_schemes import _OpenApiSecurityScheme # noqa: TC001
|
|
10
|
+
from microbootstrap.instruments.openapi_version_docs import (
|
|
11
|
+
SUPPORTED_HTTP_METHODS,
|
|
12
|
+
OpenApiVersionDocsConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SECURITY_SCHEME_NAME_PATTERN: typing.Final = re.compile(r"^[a-zA-Z0-9._-]+$")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SwaggerConfig(BaseInstrumentConfig):
|
|
20
|
+
service_name: str = "micro-service"
|
|
21
|
+
service_description: str = "Micro service description"
|
|
22
|
+
service_version: str = "1.0.0"
|
|
23
|
+
|
|
24
|
+
service_static_path: str = "/static"
|
|
25
|
+
swagger_path: str = "/docs"
|
|
26
|
+
swagger_offline_docs: bool = False
|
|
27
|
+
swagger_extra_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
|
28
|
+
security_schemes: typing.Mapping[str, _OpenApiSecurityScheme] = pydantic.Field(default_factory=dict)
|
|
29
|
+
openapi_version_docs: OpenApiVersionDocsConfig | None = None
|
|
30
|
+
|
|
31
|
+
@pydantic.field_validator("security_schemes")
|
|
32
|
+
@classmethod
|
|
33
|
+
def validate_security_scheme_names(
|
|
34
|
+
cls,
|
|
35
|
+
security_schemes: typing.Mapping[str, _OpenApiSecurityScheme],
|
|
36
|
+
) -> typing.Mapping[str, _OpenApiSecurityScheme]:
|
|
37
|
+
for scheme_name in security_schemes:
|
|
38
|
+
if SECURITY_SCHEME_NAME_PATTERN.fullmatch(scheme_name) is None:
|
|
39
|
+
message = "OpenAPI security scheme names must match ^[a-zA-Z0-9._-]+$."
|
|
40
|
+
raise ValueError(message)
|
|
41
|
+
return security_schemes
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SwaggerInstrument(Instrument[SwaggerConfig]):
|
|
45
|
+
instrument_name = "Swagger"
|
|
46
|
+
ready_condition = "Provide valid swagger_path"
|
|
47
|
+
|
|
48
|
+
def is_ready(self) -> bool:
|
|
49
|
+
return bool(self.instrument_config.swagger_path) and is_valid_path(self.instrument_config.swagger_path)
|
|
50
|
+
|
|
51
|
+
def _build_version_documentation(
|
|
52
|
+
self,
|
|
53
|
+
path: str,
|
|
54
|
+
method: str,
|
|
55
|
+
description: object,
|
|
56
|
+
existing_extension: object,
|
|
57
|
+
*,
|
|
58
|
+
has_existing_extension: bool,
|
|
59
|
+
) -> tuple[dict[str, str | list[str]], str] | None:
|
|
60
|
+
configuration = self.instrument_config.openapi_version_docs
|
|
61
|
+
if configuration is None or method not in SUPPORTED_HTTP_METHODS:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
supported_versions = next(
|
|
65
|
+
(
|
|
66
|
+
override.supported_versions
|
|
67
|
+
for override in configuration.operation_versions
|
|
68
|
+
if override.path == path and override.method == method
|
|
69
|
+
),
|
|
70
|
+
configuration.supported_versions,
|
|
71
|
+
)
|
|
72
|
+
if not supported_versions:
|
|
73
|
+
return None
|
|
74
|
+
if description is not None and not isinstance(description, str):
|
|
75
|
+
message = f"OpenAPI operation {method.upper()} {path} has a non-string description."
|
|
76
|
+
raise ValueError(message)
|
|
77
|
+
|
|
78
|
+
expected_extension: typing.Final[dict[str, str | list[str]]] = {
|
|
79
|
+
"header": "Accept",
|
|
80
|
+
"mediaType": configuration.vendor_media_type,
|
|
81
|
+
"parameter": "version",
|
|
82
|
+
"supportedVersions": list(supported_versions),
|
|
83
|
+
}
|
|
84
|
+
if has_existing_extension and existing_extension != expected_extension:
|
|
85
|
+
message = "OpenAPI operation x-accept-versioning conflicts with configured Accept version documentation."
|
|
86
|
+
raise ValueError(message)
|
|
87
|
+
return expected_extension, self._format_version_documentation(description, configuration, supported_versions)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _format_version_documentation(
|
|
91
|
+
description: str | None,
|
|
92
|
+
configuration: OpenApiVersionDocsConfig,
|
|
93
|
+
supported_versions: tuple[str, ...],
|
|
94
|
+
) -> str:
|
|
95
|
+
media_types: typing.Final = tuple(
|
|
96
|
+
f"{configuration.vendor_media_type}; version={version}" for version in supported_versions
|
|
97
|
+
)
|
|
98
|
+
version_documentation: typing.Final = (
|
|
99
|
+
f"Supported API version: `{media_types[0]}`."
|
|
100
|
+
if len(media_types) == 1
|
|
101
|
+
else "Supported API versions:\n" + "\n".join(f"- `{media_type}`." for media_type in media_types)
|
|
102
|
+
)
|
|
103
|
+
if not description:
|
|
104
|
+
return version_documentation
|
|
105
|
+
if version_documentation in description:
|
|
106
|
+
return description
|
|
107
|
+
return f"{description}\n\n{version_documentation}"
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _validate_security_scheme_conflicts(
|
|
111
|
+
existing_schemes: typing.Mapping[str, object],
|
|
112
|
+
expected_schemes: typing.Mapping[str, object],
|
|
113
|
+
) -> None:
|
|
114
|
+
for scheme_name, expected_scheme in expected_schemes.items():
|
|
115
|
+
if scheme_name in existing_schemes and existing_schemes[scheme_name] != expected_scheme:
|
|
116
|
+
message = f"OpenAPI security scheme '{scheme_name}' conflicts with the configured security scheme."
|
|
117
|
+
raise ValueError(message)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_config_type(cls) -> type[SwaggerConfig]:
|
|
121
|
+
return SwaggerConfig
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from faststream._internal.middlewares import BaseMiddleware
|
|
5
|
+
from opentelemetry import baggage, propagate
|
|
6
|
+
|
|
7
|
+
from microbootstrap.instruments.opentelemetry_instrument import opentelemetry_baggage_scope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from faststream._internal.basic_types import AsyncFuncAny
|
|
12
|
+
from faststream._internal.context import ContextRepo
|
|
13
|
+
from faststream.message import StreamMessage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FastStreamOpenTelemetryBaggageMiddleware:
|
|
17
|
+
def __init__(self, *, baggage_span_attributes: typing.Mapping[str, str]) -> None:
|
|
18
|
+
self.baggage_span_attributes = baggage_span_attributes
|
|
19
|
+
|
|
20
|
+
def __call__(
|
|
21
|
+
self,
|
|
22
|
+
msg: typing.Any, # noqa: ANN401
|
|
23
|
+
/,
|
|
24
|
+
*,
|
|
25
|
+
context: ContextRepo,
|
|
26
|
+
) -> _FastStreamOpenTelemetryBaggageMiddleware:
|
|
27
|
+
return _FastStreamOpenTelemetryBaggageMiddleware(
|
|
28
|
+
msg,
|
|
29
|
+
context=context,
|
|
30
|
+
baggage_span_attributes=self.baggage_span_attributes,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class _FastStreamOpenTelemetryBaggageMiddleware(BaseMiddleware[typing.Any, typing.Any]):
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
msg: typing.Any, # noqa: ANN401
|
|
38
|
+
/,
|
|
39
|
+
*,
|
|
40
|
+
context: ContextRepo,
|
|
41
|
+
baggage_span_attributes: typing.Mapping[str, str],
|
|
42
|
+
) -> None:
|
|
43
|
+
super().__init__(msg, context=context)
|
|
44
|
+
self.baggage_span_attributes = baggage_span_attributes
|
|
45
|
+
|
|
46
|
+
async def consume_scope(
|
|
47
|
+
self,
|
|
48
|
+
call_next: AsyncFuncAny,
|
|
49
|
+
msg: StreamMessage[typing.Any],
|
|
50
|
+
) -> typing.Any: # noqa: ANN401
|
|
51
|
+
extracted_baggage: typing.Final = baggage.get_all(propagate.extract(msg.headers))
|
|
52
|
+
with opentelemetry_baggage_scope(
|
|
53
|
+
extracted_baggage,
|
|
54
|
+
current_span_attributes=self.baggage_span_attributes,
|
|
55
|
+
):
|
|
56
|
+
return await call_next(msg)
|
|
57
|
+
|
|
58
|
+
async def publish_scope(
|
|
59
|
+
self,
|
|
60
|
+
call_next: typing.Callable[[typing.Any], typing.Awaitable[typing.Any]],
|
|
61
|
+
cmd: typing.Any, # noqa: ANN401
|
|
62
|
+
) -> typing.Any: # noqa: ANN401
|
|
63
|
+
for field in propagate.get_global_textmap().fields:
|
|
64
|
+
cmd.headers.pop(field, None)
|
|
65
|
+
propagate.inject(cmd.headers)
|
|
66
|
+
return await call_next(cmd)
|
|
@@ -58,26 +58,26 @@ dependencies = [
|
|
|
58
58
|
"opentelemetry-instrumentation-asgi>=0.46b0",
|
|
59
59
|
"orjson>=3.10.18",
|
|
60
60
|
]
|
|
61
|
-
version = "1.
|
|
61
|
+
version = "1.6.0"
|
|
62
62
|
|
|
63
63
|
[[project.authors]]
|
|
64
64
|
name = "community-of-python"
|
|
65
65
|
|
|
66
66
|
[project.optional-dependencies]
|
|
67
67
|
fastapi = [
|
|
68
|
-
"fastapi>=0.
|
|
68
|
+
"fastapi>=0.110.1",
|
|
69
69
|
"fastapi-offline-docs>=1",
|
|
70
70
|
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
|
71
|
-
"prometheus-fastapi-instrumentator>=
|
|
71
|
+
"prometheus-fastapi-instrumentator>=7.1",
|
|
72
72
|
]
|
|
73
73
|
litestar = [
|
|
74
|
-
"litestar>=2.
|
|
74
|
+
"litestar>=2.21.1",
|
|
75
75
|
"litestar-offline-docs>=1",
|
|
76
76
|
"prometheus-client>=0.20",
|
|
77
77
|
]
|
|
78
78
|
granian = ["granian[reload]>=1"]
|
|
79
79
|
faststream = [
|
|
80
|
-
"faststream
|
|
80
|
+
"faststream>=0.6.7,<0.8",
|
|
81
81
|
"prometheus-client>=0.20",
|
|
82
82
|
]
|
|
83
83
|
fastmcp = [
|
|
@@ -58,23 +58,23 @@ dependencies = [
|
|
|
58
58
|
"opentelemetry-instrumentation-asgi>=0.46b0",
|
|
59
59
|
"orjson>=3.10.18",
|
|
60
60
|
]
|
|
61
|
-
version = "1.
|
|
61
|
+
version = "1.6.0"
|
|
62
62
|
authors = [{ name = "community-of-python" }]
|
|
63
63
|
|
|
64
64
|
[project.optional-dependencies]
|
|
65
65
|
fastapi = [
|
|
66
|
-
"fastapi>=0.
|
|
66
|
+
"fastapi>=0.110.1",
|
|
67
67
|
"fastapi-offline-docs>=1",
|
|
68
68
|
"opentelemetry-instrumentation-fastapi>=0.46b0",
|
|
69
|
-
"prometheus-fastapi-instrumentator>=
|
|
69
|
+
"prometheus-fastapi-instrumentator>=7.1",
|
|
70
70
|
]
|
|
71
71
|
litestar = [
|
|
72
|
-
"litestar>=2.
|
|
72
|
+
"litestar>=2.21.1",
|
|
73
73
|
"litestar-offline-docs>=1",
|
|
74
74
|
"prometheus-client>=0.20",
|
|
75
75
|
]
|
|
76
76
|
granian = ["granian[reload]>=1"]
|
|
77
|
-
faststream = ["faststream
|
|
77
|
+
faststream = ["faststream>=0.6.7,<0.8", "prometheus-client>=0.20"]
|
|
78
78
|
fastmcp = ["fastmcp>=2,<4", "prometheus-client>=0.20"]
|
|
79
79
|
|
|
80
80
|
[dependency-groups]
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
import pydantic
|
|
5
|
-
|
|
6
|
-
from microbootstrap.helpers import is_valid_path
|
|
7
|
-
from microbootstrap.instruments.base import BaseInstrumentConfig, Instrument
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class SwaggerConfig(BaseInstrumentConfig):
|
|
11
|
-
service_name: str = "micro-service"
|
|
12
|
-
service_description: str = "Micro service description"
|
|
13
|
-
service_version: str = "1.0.0"
|
|
14
|
-
|
|
15
|
-
service_static_path: str = "/static"
|
|
16
|
-
swagger_path: str = "/docs"
|
|
17
|
-
swagger_offline_docs: bool = False
|
|
18
|
-
swagger_extra_params: dict[str, typing.Any] = pydantic.Field(default_factory=dict)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class SwaggerInstrument(Instrument[SwaggerConfig]):
|
|
22
|
-
instrument_name = "Swagger"
|
|
23
|
-
ready_condition = "Provide valid swagger_path"
|
|
24
|
-
|
|
25
|
-
def is_ready(self) -> bool:
|
|
26
|
-
return bool(self.instrument_config.swagger_path) and is_valid_path(self.instrument_config.swagger_path)
|
|
27
|
-
|
|
28
|
-
@classmethod
|
|
29
|
-
def get_config_type(cls) -> type[SwaggerConfig]:
|
|
30
|
-
return SwaggerConfig
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/health_checks_instrument.py
RENAMED
|
File without changes
|
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/logging_instrument.py
RENAMED
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/opentelemetry_instrument.py
RENAMED
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/prometheus_instrument.py
RENAMED
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/pyroscope_instrument.py
RENAMED
|
File without changes
|
{microbootstrap-1.5.4 → microbootstrap-1.6.0}/microbootstrap/instruments/sentry_instrument.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|