csrd-versioning 0.1.0__py3-none-any.whl

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.
Files changed (48) hide show
  1. csrd/versioning/__init__.py +99 -0
  2. csrd/versioning/_constants.py +28 -0
  3. csrd/versioning/_core.py +240 -0
  4. csrd/versioning/_dependencies.py +67 -0
  5. csrd/versioning/_dependency_wiring.py +272 -0
  6. csrd/versioning/_dispatch.py +203 -0
  7. csrd/versioning/_docs.py +495 -0
  8. csrd/versioning/_fastapi_types.py +44 -0
  9. csrd/versioning/_helpers.py +149 -0
  10. csrd/versioning/_orchestration.py +399 -0
  11. csrd/versioning/_redoc.py +98 -0
  12. csrd/versioning/_settings.py +88 -0
  13. csrd/versioning/_swagger_ui_version.py +12 -0
  14. csrd/versioning/_types.py +14 -0
  15. csrd/versioning/actuator/README.md +261 -0
  16. csrd/versioning/actuator/__init__.py +3 -0
  17. csrd/versioning/actuator/actuator.py +111 -0
  18. csrd/versioning/actuator/plugins/__init__.py +65 -0
  19. csrd/versioning/actuator/plugins/base.py +77 -0
  20. csrd/versioning/actuator/plugins/env/__init__.py +31 -0
  21. csrd/versioning/actuator/plugins/env/plugin.py +145 -0
  22. csrd/versioning/actuator/plugins/env/providers.py +105 -0
  23. csrd/versioning/actuator/plugins/env/registry.py +211 -0
  24. csrd/versioning/actuator/plugins/health/__init__.py +37 -0
  25. csrd/versioning/actuator/plugins/health/auto.py +244 -0
  26. csrd/versioning/actuator/plugins/health/indicators.py +137 -0
  27. csrd/versioning/actuator/plugins/health/plugin.py +271 -0
  28. csrd/versioning/actuator/plugins/info.py +120 -0
  29. csrd/versioning/actuator/tools/README.md +54 -0
  30. csrd/versioning/actuator/tools/__init__.py +0 -0
  31. csrd/versioning/actuator/tools/generate_service_info_from_git.py +126 -0
  32. csrd/versioning/exception_handlers.py +126 -0
  33. csrd/versioning/py.typed +0 -0
  34. csrd/versioning/swagger_plugins/__init__.py +15 -0
  35. csrd/versioning/swagger_plugins/_base.py +153 -0
  36. csrd/versioning/swagger_plugins/file_upload/__init__.py +35 -0
  37. csrd/versioning/swagger_plugins/file_upload/_body_factory.py +121 -0
  38. csrd/versioning/swagger_plugins/file_upload/_schema_patcher.py +165 -0
  39. csrd/versioning/swagger_plugins/file_upload/file_upload_plugin.css +181 -0
  40. csrd/versioning/swagger_plugins/file_upload/file_upload_plugin.js +529 -0
  41. csrd/versioning/templates/__init__.py +0 -0
  42. csrd/versioning/templates/favicon.png +0 -0
  43. csrd/versioning/templates/swagger_ui.css +616 -0
  44. csrd/versioning/templates/swagger_ui.html +43 -0
  45. csrd/versioning/templates/swagger_ui.js +131 -0
  46. csrd_versioning-0.1.0.dist-info/METADATA +15 -0
  47. csrd_versioning-0.1.0.dist-info/RECORD +48 -0
  48. csrd_versioning-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,145 @@
1
+ """Spring Boot-style env actuator plugin."""
2
+
3
+ import os
4
+ from collections.abc import Mapping
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, FastAPI
8
+
9
+ from ..base import ActuatorLink, BaseActuatorPlugin
10
+ from .registry import (
11
+ PropertySourceRegistry,
12
+ SanitizationConfig,
13
+ create_property_source_registry,
14
+ )
15
+
16
+
17
+ def _active_profiles() -> list[str]:
18
+ """Parse SPRING_PROFILES_ACTIVE environment variable."""
19
+ return [
20
+ profile.strip()
21
+ for profile in os.environ.get("SPRING_PROFILES_ACTIVE", "").split(",")
22
+ if profile.strip()
23
+ ]
24
+
25
+
26
+ def _default_profiles() -> list[str]:
27
+ """Parse SPRING_PROFILES_DEFAULT environment variable, defaulting to 'default'."""
28
+ return [
29
+ profile.strip()
30
+ for profile in os.environ.get("SPRING_PROFILES_DEFAULT", "default").split(",")
31
+ if profile.strip()
32
+ ]
33
+
34
+
35
+ class EnvActuatorPlugin(BaseActuatorPlugin):
36
+ """Expose Spring-style ``/actuator/env`` runtime environment metadata.
37
+
38
+ The plugin aggregates property sources (environment variables, system info,
39
+ Docker config) and exposes them via the /env and /env/{toMatch} endpoints.
40
+ """
41
+
42
+ name = "env"
43
+
44
+ def __init__(
45
+ self,
46
+ registry: PropertySourceRegistry | None = None,
47
+ *,
48
+ sanitization: SanitizationConfig | None = None,
49
+ ) -> None:
50
+ """Initialize the env plugin.
51
+
52
+ Args:
53
+ registry: PropertySourceRegistry for aggregating property sources.
54
+ If None, creates a default registry with built-in providers.
55
+ sanitization: Redaction configuration passed to the default registry.
56
+ Ignored when ``registry`` is provided explicitly.
57
+
58
+ Note:
59
+ Runtime environment values may change during process lifetime,
60
+ so caching is disabled (cache_enabled=False).
61
+ """
62
+ super().__init__(cache_enabled=False)
63
+ self.registry = registry or create_property_source_registry(
64
+ sanitization=sanitization,
65
+ )
66
+
67
+ @classmethod
68
+ def with_providers(
69
+ cls,
70
+ *,
71
+ sanitization: SanitizationConfig | None = None,
72
+ **providers: Any,
73
+ ) -> "EnvActuatorPlugin":
74
+ """Create an EnvActuatorPlugin with custom property source providers.
75
+
76
+ Args:
77
+ sanitization: Redaction configuration for the registry.
78
+ **providers: Named property source providers to register.
79
+ Each keyword argument becomes a provider name.
80
+
81
+ Returns:
82
+ A new EnvActuatorPlugin instance with the providers registered.
83
+
84
+ Example:
85
+ >>> plugin = EnvActuatorPlugin.with_providers(
86
+ ... sanitization=SanitizationConfig(show_values=ShowValues.ALWAYS),
87
+ ... database=DatabasePropertiesProvider(),
88
+ ... features=FeatureFlagsProvider(),
89
+ ... )
90
+ """
91
+ plugin = cls(sanitization=sanitization)
92
+ plugin.register_property_source_providers(providers)
93
+ return plugin
94
+
95
+ def register_property_source_providers(
96
+ self,
97
+ providers: Mapping[str, Any],
98
+ ) -> None:
99
+ """Register or override property source providers in the plugin registry."""
100
+ for name, provider in providers.items():
101
+ self.registry.register(name, provider)
102
+
103
+ def register(
104
+ self,
105
+ router: APIRouter,
106
+ *,
107
+ app: FastAPI,
108
+ prefix: str,
109
+ ) -> Mapping[str, ActuatorLink]:
110
+ """Register env endpoints on the router.
111
+
112
+ Args:
113
+ router: APIRouter to register endpoints on.
114
+ app: FastAPI application instance.
115
+ prefix: URL prefix for actuator endpoints (e.g., '/actuator').
116
+
117
+ Returns:
118
+ Mapping of link names to ActuatorLink objects for the root endpoint.
119
+ """
120
+
121
+ @router.get("/env", include_in_schema=False)
122
+ async def env() -> dict[str, Any]:
123
+ """List all property sources and active profiles."""
124
+ return {
125
+ "activeProfiles": _active_profiles(),
126
+ "defaultProfiles": _default_profiles(),
127
+ "propertySources": self.registry.get_all_sources(),
128
+ }
129
+
130
+ @router.get("/env/{toMatch}", include_in_schema=False)
131
+ async def env_to_match(toMatch: str) -> dict[str, Any]:
132
+ """List property sources filtered by key, and active profiles."""
133
+ return {
134
+ "activeProfiles": _active_profiles(),
135
+ "defaultProfiles": _default_profiles(),
136
+ "propertySources": self.registry.get_all_sources(to_match=toMatch),
137
+ }
138
+
139
+ return {
140
+ "env": self.build_link(prefix=prefix, route_path="/env"),
141
+ "env-toMatch": ActuatorLink(
142
+ href=f"{prefix}/env/{{toMatch}}",
143
+ templated=True,
144
+ ),
145
+ }
@@ -0,0 +1,105 @@
1
+ """Property source providers for the env actuator."""
2
+
3
+ import os
4
+ import platform
5
+ import sys
6
+ from typing import Any, Protocol
7
+
8
+
9
+ class PropertySourceProvider(Protocol):
10
+ """Contract for objects that provide property sources.
11
+
12
+ Property sources are aggregated by the EnvActuatorPlugin and exposed
13
+ via the /actuator/env and /actuator/env/{toMatch} endpoints.
14
+ """
15
+
16
+ def get_sources(self, to_match: str | None = None) -> list[dict[str, Any]]:
17
+ """Return property sources, optionally filtered by to_match key.
18
+
19
+ Args:
20
+ to_match: Optional case-insensitive substring to filter property keys.
21
+ If provided, only properties with matching keys are returned.
22
+
23
+ Returns:
24
+ List of property source dicts with 'name' and 'properties' keys.
25
+ Empty list if no sources match the filter.
26
+ """
27
+
28
+
29
+ class SystemEnvironmentProvider:
30
+ """Expose OS environment variables as a property source."""
31
+
32
+ def get_sources(self, to_match: str | None = None) -> list[dict[str, Any]]:
33
+ """Return environment variables, optionally filtered by key."""
34
+ if not to_match:
35
+ # Return all env vars with Spring-style {"value": ...} wrapper
36
+ properties = {key: {"value": value} for key, value in os.environ.items()}
37
+ return [{"name": "systemEnvironment", "properties": properties}]
38
+
39
+ # Filter by to_match
40
+ needle = to_match.strip().lower()
41
+ matched = {
42
+ key: {"value": value} for key, value in os.environ.items() if needle in key.lower()
43
+ }
44
+
45
+ if not matched:
46
+ return []
47
+
48
+ return [{"name": "systemEnvironment", "properties": matched}]
49
+
50
+
51
+ class SystemPropertiesProvider:
52
+ """Expose system information as a property source."""
53
+
54
+ def get_sources(self, to_match: str | None = None) -> list[dict[str, Any]]:
55
+ """Return system properties (Python version, platform, etc)."""
56
+ raw = {
57
+ "python.version": sys.version,
58
+ "python.implementation": sys.implementation.name,
59
+ "platform.system": platform.system(),
60
+ "platform.release": platform.release(),
61
+ "platform.machine": platform.machine(),
62
+ "os.name": os.name,
63
+ "process.pid": str(os.getpid()),
64
+ }
65
+
66
+ if not to_match:
67
+ properties = {k: {"value": v} for k, v in raw.items()}
68
+ return [{"name": "systemProperties", "properties": properties}]
69
+
70
+ # Filter by to_match
71
+ needle = to_match.strip().lower()
72
+ matched = {k: {"value": v} for k, v in raw.items() if needle in k.lower()}
73
+
74
+ if not matched:
75
+ return []
76
+
77
+ return [{"name": "systemProperties", "properties": matched}]
78
+
79
+
80
+ class DockerConfigProvider:
81
+ """Expose Docker environment variables as a property source."""
82
+
83
+ def get_sources(self, to_match: str | None = None) -> list[dict[str, Any]]:
84
+ """Return Docker-related environment variables."""
85
+ docker_vars = {
86
+ key: value
87
+ for key, value in os.environ.items()
88
+ if key.startswith(("DOCKER_", "CONTAINER_"))
89
+ }
90
+
91
+ if not docker_vars:
92
+ return []
93
+
94
+ if not to_match:
95
+ properties = {k: {"value": v} for k, v in docker_vars.items()}
96
+ return [{"name": "dockerConfig", "properties": properties}]
97
+
98
+ # Filter by to_match
99
+ needle = to_match.strip().lower()
100
+ matched = {k: {"value": v} for k, v in docker_vars.items() if needle in k.lower()}
101
+
102
+ if not matched:
103
+ return []
104
+
105
+ return [{"name": "dockerConfig", "properties": matched}]
@@ -0,0 +1,211 @@
1
+ """Property source registry for aggregating environment information."""
2
+
3
+ import logging
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from .providers import (
10
+ DockerConfigProvider,
11
+ PropertySourceProvider,
12
+ SystemEnvironmentProvider,
13
+ SystemPropertiesProvider,
14
+ )
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Spring Boot's default keys-to-sanitize patterns.
19
+ DEFAULT_KEYS_TO_SANITIZE: tuple[str, ...] = (
20
+ "password",
21
+ "secret",
22
+ "key",
23
+ "token",
24
+ ".*credentials.*",
25
+ "vcap_services",
26
+ )
27
+
28
+ REDACTED_VALUE = "******"
29
+
30
+
31
+ class ShowValues(Enum):
32
+ """Controls when property values are visible vs redacted.
33
+
34
+ Mirrors Spring Boot 3's ``management.endpoint.env.show-values`` setting.
35
+
36
+ Members:
37
+ NEVER: Redact ALL values — nothing is shown in the clear.
38
+ ALWAYS: Show benign values in the clear; sensitive keys are still redacted.
39
+ WHEN_AUTHORIZED: Reserved for future use with authentication checks.
40
+ Currently behaves like NEVER (safe default until auth is wired).
41
+ """
42
+
43
+ NEVER = "NEVER"
44
+ ALWAYS = "ALWAYS"
45
+ WHEN_AUTHORIZED = "WHEN_AUTHORIZED"
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class SanitizationConfig:
50
+ """Configuration for property value redaction.
51
+
52
+ Args:
53
+ keys_to_sanitize: Regex patterns for keys that are **always** redacted,
54
+ regardless of ``show_values``. Defaults to Spring Boot's default set.
55
+ additional_keys_to_sanitize: Extra patterns appended to the defaults.
56
+ show_values: Controls visibility of *non-sensitive* values.
57
+ ``NEVER`` redacts everything; ``ALWAYS`` shows benign values.
58
+ """
59
+
60
+ keys_to_sanitize: tuple[str, ...] = DEFAULT_KEYS_TO_SANITIZE
61
+ additional_keys_to_sanitize: tuple[str, ...] = ()
62
+ show_values: ShowValues = ShowValues.NEVER
63
+
64
+ def __post_init__(self) -> None:
65
+ all_patterns = self.keys_to_sanitize + self.additional_keys_to_sanitize
66
+ pattern = re.compile("|".join(all_patterns), re.IGNORECASE) if all_patterns else None
67
+ # frozen=True requires object.__setattr__ for post-init assignment
68
+ object.__setattr__(self, "_sensitive_pattern", pattern)
69
+
70
+ _sensitive_pattern: re.Pattern[str] | None = field(init=False, default=None)
71
+
72
+ @property
73
+ def sensitive_pattern(self) -> re.Pattern[str] | None:
74
+ """Return the compiled sensitive-key regex (computed once at init)."""
75
+ return self._sensitive_pattern
76
+
77
+
78
+ class PropertySourceRegistry:
79
+ """Manages all property source providers with built-in defaults.
80
+
81
+ The registry maintains a set of named property source providers and can
82
+ aggregate their outputs. It comes with sensible defaults but allows
83
+ registration and override of providers.
84
+
85
+ Sanitization is controlled via ``SanitizationConfig``:
86
+
87
+ - ``keys_to_sanitize`` — base patterns (default: password, secret, key, …)
88
+ - ``additional_keys_to_sanitize`` — extra patterns on top of defaults
89
+ - ``show_values`` — ``NEVER`` (default), ``ALWAYS``, or ``WHEN_AUTHORIZED``
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ with_defaults: bool = True,
95
+ sanitization: SanitizationConfig | None = None,
96
+ ) -> None:
97
+ """Initialize the registry.
98
+
99
+ Args:
100
+ with_defaults: If True, register built-in providers (systemEnvironment,
101
+ systemProperties, dockerConfig). If False, start empty.
102
+ sanitization: Redaction configuration. If None, uses default config
103
+ (redact keys matching password, secret, key, token, etc.).
104
+ """
105
+ self._providers: dict[str, PropertySourceProvider] = {}
106
+ self._sanitization = sanitization or SanitizationConfig()
107
+ if with_defaults:
108
+ self._init_defaults()
109
+
110
+ def _init_defaults(self) -> None:
111
+ """Register built-in providers that are always included."""
112
+ self.register("systemEnvironment", SystemEnvironmentProvider())
113
+ self.register("systemProperties", SystemPropertiesProvider())
114
+ self.register("dockerConfig", DockerConfigProvider())
115
+
116
+ def register(self, name: str, provider: PropertySourceProvider) -> None:
117
+ """Register or override a property source provider.
118
+
119
+ If a provider with the given name already exists, it is replaced.
120
+
121
+ Args:
122
+ name: Unique identifier for the provider.
123
+ provider: Object implementing the PropertySourceProvider protocol.
124
+ """
125
+ self._providers[name] = provider
126
+
127
+ def unregister(self, name: str) -> None:
128
+ """Remove a provider by name.
129
+
130
+ Args:
131
+ name: Name of the provider to remove.
132
+ """
133
+ self._providers.pop(name, None)
134
+
135
+ def get_all_sources(self, to_match: str | None = None) -> list[dict[str, Any]]:
136
+ """Aggregate property sources from all registered providers.
137
+
138
+ Redaction is two-tier:
139
+ - Sensitive keys (matching ``keys_to_sanitize``) are **always** redacted.
140
+ - When ``show_values`` is ``NEVER``, *all* values are redacted.
141
+ - When ``show_values`` is ``ALWAYS``, only
142
+ sensitive keys are redacted and benign values are shown.
143
+
144
+ Args:
145
+ to_match: Optional case-insensitive substring filter applied to
146
+ each provider's output.
147
+
148
+ Returns:
149
+ List of property source dicts aggregated from all providers.
150
+ """
151
+ sources = []
152
+ for name, provider in self._providers.items():
153
+ try:
154
+ sources.extend(provider.get_sources(to_match))
155
+ except Exception:
156
+ logger.exception("Provider '%s' failed in get_sources()", name)
157
+
158
+ redact_all = self._sanitization.show_values != ShowValues.ALWAYS
159
+ sensitive = self._sanitization.sensitive_pattern
160
+ return [
161
+ _sanitize_source(source, sensitive=sensitive, redact_all=redact_all)
162
+ for source in sources
163
+ ]
164
+
165
+
166
+ def _sanitize_source(
167
+ source: dict[str, Any],
168
+ *,
169
+ sensitive: re.Pattern[str] | None,
170
+ redact_all: bool,
171
+ ) -> dict[str, Any]:
172
+ """Replace values of sensitive (or all) keys with the redacted placeholder."""
173
+ properties = source.get("properties")
174
+ if not isinstance(properties, dict):
175
+ return source
176
+
177
+ if not redact_all and sensitive is None:
178
+ return source
179
+
180
+ sanitized: dict[str, Any] = {}
181
+ for prop_key, prop_value in properties.items():
182
+ is_sensitive = sensitive is not None and sensitive.search(prop_key)
183
+ if redact_all or is_sensitive:
184
+ # Preserve the Spring-style {"value": ...} wrapper if present
185
+ if isinstance(prop_value, dict) and "value" in prop_value:
186
+ sanitized[prop_key] = {"value": REDACTED_VALUE}
187
+ else:
188
+ sanitized[prop_key] = REDACTED_VALUE
189
+ else:
190
+ sanitized[prop_key] = prop_value
191
+
192
+ return {**source, "properties": sanitized}
193
+
194
+
195
+ def create_property_source_registry(
196
+ with_defaults: bool = True,
197
+ sanitization: SanitizationConfig | None = None,
198
+ ) -> PropertySourceRegistry:
199
+ """Create a new property source registry.
200
+
201
+ Args:
202
+ with_defaults: If True, include built-in providers. If False, start empty.
203
+ sanitization: Redaction configuration. If None, uses default config.
204
+
205
+ Returns:
206
+ A new PropertySourceRegistry instance.
207
+ """
208
+ return PropertySourceRegistry(
209
+ with_defaults=with_defaults,
210
+ sanitization=sanitization,
211
+ )
@@ -0,0 +1,37 @@
1
+ """Spring Boot-style health actuator with composable indicators."""
2
+
3
+ from .auto import (
4
+ AppStateHealthIndicator,
5
+ KafkaHealthIndicator,
6
+ LaunchDarklyHealthIndicator,
7
+ MongoHealthIndicator,
8
+ RabbitHealthIndicator,
9
+ discover_indicators,
10
+ )
11
+ from .indicators import (
12
+ BaseHealthIndicator,
13
+ DiskSpaceHealthIndicator,
14
+ HealthIndicator,
15
+ LivenessIndicator,
16
+ PingHealthIndicator,
17
+ ReadinessIndicator,
18
+ )
19
+ from .plugin import HealthActuatorPlugin, ShowDetails, Status
20
+
21
+ __all__ = (
22
+ "AppStateHealthIndicator",
23
+ "BaseHealthIndicator",
24
+ "DiskSpaceHealthIndicator",
25
+ "HealthActuatorPlugin",
26
+ "HealthIndicator",
27
+ "KafkaHealthIndicator",
28
+ "LaunchDarklyHealthIndicator",
29
+ "LivenessIndicator",
30
+ "MongoHealthIndicator",
31
+ "PingHealthIndicator",
32
+ "RabbitHealthIndicator",
33
+ "ReadinessIndicator",
34
+ "ShowDetails",
35
+ "Status",
36
+ "discover_indicators",
37
+ )