csrd-versioning 0.3.32__tar.gz → 0.3.34__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.
Files changed (66) hide show
  1. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/PKG-INFO +1 -1
  2. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/pyproject.toml +1 -1
  3. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_config.py +3 -1
  4. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_orchestration.py +49 -4
  5. csrd_versioning-0.3.34/src/csrd/versioning/extensions/__init__.py +12 -0
  6. csrd_versioning-0.3.34/src/csrd/versioning/extensions/_discovery.py +58 -0
  7. csrd_versioning-0.3.34/src/csrd/versioning/extensions/_registry.py +92 -0
  8. csrd_versioning-0.3.34/src/csrd/versioning/extensions/_types.py +49 -0
  9. csrd_versioning-0.3.34/src/csrd/versioning/extensions/hosts/__init__.py +6 -0
  10. csrd_versioning-0.3.34/src/csrd/versioning/extensions/hosts/_actuator.py +57 -0
  11. csrd_versioning-0.3.34/src/csrd/versioning/extensions/hosts/_swagger.py +45 -0
  12. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/.gitignore +0 -0
  13. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/README.md +0 -0
  14. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/__init__.py +0 -0
  15. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_constants.py +0 -0
  16. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_core.py +0 -0
  17. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_dependencies.py +0 -0
  18. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_dependency_wiring.py +0 -0
  19. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_dispatch.py +0 -0
  20. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_docs.py +0 -0
  21. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_fastapi_types.py +0 -0
  22. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_helpers.py +0 -0
  23. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_redoc.py +0 -0
  24. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_settings.py +0 -0
  25. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_swagger_ui_version.py +0 -0
  26. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/_types.py +0 -0
  27. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/README.md +0 -0
  28. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/__init__.py +0 -0
  29. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/actuator.py +0 -0
  30. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/__init__.py +0 -0
  31. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/base.py +0 -0
  32. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/env/__init__.py +0 -0
  33. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/env/plugin.py +0 -0
  34. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/env/providers.py +0 -0
  35. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/env/registry.py +0 -0
  36. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/health/__init__.py +0 -0
  37. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/health/auto.py +0 -0
  38. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/health/indicators.py +0 -0
  39. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/health/plugin.py +0 -0
  40. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/plugins/info.py +0 -0
  41. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/tools/README.md +0 -0
  42. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/tools/__init__.py +0 -0
  43. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/actuator/tools/generate_service_info_from_git.py +0 -0
  44. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/exception_handlers.py +0 -0
  45. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/py.typed +0 -0
  46. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/__init__.py +0 -0
  47. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/_base.py +0 -0
  48. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/file_upload/__init__.py +0 -0
  49. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/file_upload/_body_factory.py +0 -0
  50. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/file_upload/_schema_patcher.py +0 -0
  51. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/file_upload/file_upload_plugin.css +0 -0
  52. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/swagger_plugins/file_upload/file_upload_plugin.js +0 -0
  53. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/templates/__init__.py +0 -0
  54. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/templates/favicon.png +0 -0
  55. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/templates/swagger_ui.css +0 -0
  56. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/templates/swagger_ui.html +0 -0
  57. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/src/csrd/versioning/templates/swagger_ui.js +0 -0
  58. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_actuator.py +0 -0
  59. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_core.py +0 -0
  60. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_dependency_wiring.py +0 -0
  61. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_dispatch.py +0 -0
  62. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_docs.py +0 -0
  63. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_exception_handlers.py +0 -0
  64. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_helpers.py +0 -0
  65. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_orchestration.py +0 -0
  66. {csrd_versioning-0.3.32 → csrd_versioning-0.3.34}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csrd-versioning
3
- Version: 0.3.32
3
+ Version: 0.3.34
4
4
  Summary: API versioning, dispatch, docs, and actuator for FastAPI
5
5
  Project-URL: Repository, https://github.com/csrd-api/fastapi-common
6
6
  Project-URL: Documentation, https://github.com/csrd-api/fastapi-common/tree/main/packages/versioning
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csrd-versioning"
3
- version = "0.3.32"
3
+ version = "0.3.34"
4
4
  description = "API versioning, dispatch, docs, and actuator for FastAPI"
5
5
  license = { text = "MIT" }
6
6
  requires-python = ">=3.12"
@@ -1,4 +1,4 @@
1
- from collections.abc import Callable
1
+ from collections.abc import Callable, Sequence
2
2
  from dataclasses import dataclass, field
3
3
  from typing import Any
4
4
 
@@ -12,6 +12,7 @@ from ._fastapi_types import (
12
12
  )
13
13
  from ._types import VersionedAppState, VersionKey
14
14
  from .actuator.plugins import ActuatorPlugin
15
+ from .extensions import HostPlugin
15
16
 
16
17
 
17
18
  @dataclass(slots=True)
@@ -34,6 +35,7 @@ class VersionedApiConfig:
34
35
  include_actuator_endpoints: bool = True
35
36
  actuator_plugins: list[ActuatorPlugin] = field(default_factory=list)
36
37
  swagger_plugins: list[Any] = field(default_factory=list)
38
+ extensions: Sequence[HostPlugin] = field(default_factory=list)
37
39
 
38
40
 
39
41
  @dataclass(slots=True)
@@ -45,6 +45,42 @@ logger = logging.getLogger(__name__)
45
45
  _VERSIONING_CONFIGURED_KEY = "_versioning_configured"
46
46
 
47
47
 
48
+ def _resolve_extensions(
49
+ config: VersionedApiConfig,
50
+ ) -> tuple[Any, ...] | tuple[tuple[Any, ...], tuple[Any, ...]]:
51
+ """Resolve inner-layer plugins for built-in hosts (actuator, swagger_ui).
52
+
53
+ Plugin Architecture:
54
+ - Layer 1 (Outer): Host plugins (actuator, swagger_ui, custom)
55
+ - Layer 2 (Inner): Plugins managed by each host
56
+
57
+ This function resolves ONLY the inner-layer for built-in hosts.
58
+ Custom outer-layer hosts manage their own inner registries internally.
59
+
60
+ For built-in hosts:
61
+ - actuator_plugins: built-in defaults + user overrides (by-name merge)
62
+ - swagger_plugins: built-in defaults + user overrides (by-name merge)
63
+ - extensions: outer-layer plugins (custom hosts) — not touched here
64
+ """
65
+ # Inner-layer: actuator
66
+ # User can override built-in plugins by name via config.actuator_plugins
67
+ actuator_plugins: Any = list(config.actuator_plugins)
68
+
69
+ # Inner-layer: swagger_ui
70
+ # User can override built-in plugins by name via config.swagger_plugins
71
+ swagger_plugins: Any = list(config.swagger_plugins)
72
+
73
+ # Outer-layer: custom hosts in config.extensions
74
+ # Each custom host is self-contained and manages its own inner registries
75
+ # No merging needed here — they're passed through as-is
76
+
77
+ # TODO: Future: when built-in hosts themselves become HostPlugins,
78
+ # we could manage their inner layers via a single registry.
79
+ # For now, keep the layers separate to prevent mixing concerns.
80
+
81
+ return actuator_plugins, swagger_plugins
82
+
83
+
48
84
  def default_exception_handlers_provider() -> Mapping[int | type[Exception], ExceptionHandler]:
49
85
  """Return the default exception-handler map used by versioning integrations."""
50
86
  from csrd.versioning.exception_handlers import EXCEPTION_HANDLERS
@@ -126,6 +162,17 @@ def configure_versioned_api(
126
162
  resolved_handler_map=resolved_handler_map,
127
163
  )
128
164
 
165
+ # Resolve inner-layer plugins for built-in hosts
166
+ actuator_plugins, swagger_plugins = _resolve_extensions(config)
167
+
168
+ # Log custom outer-layer hosts (they manage their own inner plugins)
169
+ if config.extensions:
170
+ logger.debug(
171
+ "Registered %d custom host plugin(s): %s",
172
+ len(config.extensions),
173
+ [getattr(p, "host", "?") for p in config.extensions],
174
+ )
175
+
129
176
  docs._register_custom_docs(
130
177
  app=app,
131
178
  version_mapping=version_mapping,
@@ -138,13 +185,11 @@ def configure_versioned_api(
138
185
  include_info_endpoints=config.include_info_endpoints,
139
186
  build_tag=getattr(versioning_settings, "build_tag", None),
140
187
  include_root_favicon_alias=config.include_root_favicon_alias,
141
- swagger_plugins=list(config.swagger_plugins) if config.swagger_plugins else None,
188
+ swagger_plugins=list(swagger_plugins) if swagger_plugins else None,
142
189
  )
143
190
 
144
191
  if config.include_actuator_endpoints:
145
- register_actuator_router(
146
- app, plugins=list(config.actuator_plugins) if config.actuator_plugins else None
147
- )
192
+ register_actuator_router(app, plugins=list(actuator_plugins) if actuator_plugins else None)
148
193
 
149
194
  dependency_wiring._mount_and_wire_versions(
150
195
  app=app,
@@ -0,0 +1,12 @@
1
+ """Unified extension host system for actuator, swagger, and custom hosts."""
2
+
3
+ from ._discovery import discover_plugins
4
+ from ._registry import ExtensionRegistry
5
+ from ._types import ExtensionContext, HostPlugin
6
+
7
+ __all__ = (
8
+ "ExtensionContext",
9
+ "ExtensionRegistry",
10
+ "HostPlugin",
11
+ "discover_plugins",
12
+ )
@@ -0,0 +1,58 @@
1
+ """Optional plugin discovery via entry points.
2
+
3
+ Supports loading plugins from installed packages without explicit registration.
4
+ Keep as opt-in to maintain control over startup behavior.
5
+ """
6
+
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from ._types import HostPlugin
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def discover_plugins(group: str) -> tuple["HostPlugin", ...]:
17
+ """Load plugins from entry points in a group.
18
+
19
+ Entry point names should be fully-qualified plugin classes or factory functions.
20
+ Returns discovered plugins ready for registry registration.
21
+
22
+ Args:
23
+ group: Entry point group name, e.g., 'csrd.extensions.actuator'
24
+
25
+ Returns:
26
+ Tuple of discovered plugins.
27
+ """
28
+ try:
29
+ import importlib.metadata as metadata
30
+ except ImportError:
31
+ logger.warning("importlib.metadata not available; plugin discovery disabled")
32
+ return ()
33
+
34
+ discovered = []
35
+
36
+ try:
37
+ eps = metadata.entry_points()
38
+ # Support both dict-like (3.9) and SelectableGroups (3.10+) interfaces
39
+ group_eps = eps.get(group) if isinstance(eps, dict) else eps.select(group=group)
40
+
41
+ for ep in group_eps:
42
+ try:
43
+ plugin_class_or_factory = ep.load()
44
+ # Try to instantiate if it looks like a class, else call as factory
45
+ if isinstance(plugin_class_or_factory, type):
46
+ plugin = plugin_class_or_factory()
47
+ else:
48
+ plugin = plugin_class_or_factory()
49
+
50
+ logger.debug("Discovered plugin from entry point %s: %s", ep.name, plugin.name)
51
+ discovered.append(plugin)
52
+ except Exception as e:
53
+ logger.warning("Failed to load plugin from entry point %s: %s", ep.name, e)
54
+
55
+ except Exception as e:
56
+ logger.warning("Failed to read entry points for group %s: %s", group, e)
57
+
58
+ return tuple(discovered)
@@ -0,0 +1,92 @@
1
+ """Optional registry for deterministic plugin merge/order/override.
2
+
3
+ This module handles:
4
+ - merging defaults + user plugins by name
5
+ - ordering by (order, name)
6
+ - enabling/disabling
7
+ - discovery contribution
8
+
9
+ Hosts may use this or manage plugins directly.
10
+ """
11
+
12
+ import logging
13
+ from collections.abc import Sequence
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from ._types import HostPlugin
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class ExtensionRegistry:
23
+ """Registry for plugins targeting one or more hosts.
24
+
25
+ Provides by-name override semantics: defaults load first,
26
+ user plugins override by name, discovery plugins feed in last
27
+ and also follow override rules.
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize empty registry."""
32
+ self._plugins: dict[tuple[str, str], HostPlugin] = {}
33
+ """Map (host, name) -> HostPlugin for fast lookup and override."""
34
+
35
+ def register_defaults(self, defaults: Sequence["HostPlugin"]) -> None:
36
+ """Register default plugins.
37
+
38
+ These load first and can be overridden by explicit or discovered plugins.
39
+ """
40
+ for plugin in defaults:
41
+ key = (plugin.host, plugin.name)
42
+ self._plugins[key] = plugin
43
+ logger.debug("Registered default plugin %s for host %s", plugin.name, plugin.host)
44
+
45
+ def register_user(self, plugins: Sequence["HostPlugin"]) -> None:
46
+ """Register user-provided plugins.
47
+
48
+ User plugins override defaults and discovered plugins with same (host, name).
49
+ """
50
+ for plugin in plugins:
51
+ key = (plugin.host, plugin.name)
52
+ if key in self._plugins:
53
+ logger.debug(
54
+ "User plugin %s for host %s overrides default",
55
+ plugin.name,
56
+ plugin.host,
57
+ )
58
+ self._plugins[key] = plugin
59
+
60
+ def register_discovered(self, plugins: Sequence["HostPlugin"]) -> None:
61
+ """Register discovered plugins (e.g., from entry points).
62
+
63
+ Discovered plugins can be overridden by user plugins.
64
+ """
65
+ for plugin in plugins:
66
+ key = (plugin.host, plugin.name)
67
+ if key in self._plugins:
68
+ logger.debug(
69
+ "Discovered plugin %s for host %s already registered; skipping",
70
+ plugin.name,
71
+ plugin.host,
72
+ )
73
+ continue
74
+ self._plugins[key] = plugin
75
+ logger.debug("Registered discovered plugin %s for host %s", plugin.name, plugin.host)
76
+
77
+ def resolve_for_host(self, host: str) -> tuple["HostPlugin", ...]:
78
+ """Return plugins for a host, ordered and filtered by enabled status.
79
+
80
+ Returns tuple sorted by (order, name) for deterministic behavior.
81
+ """
82
+ host_plugins = [
83
+ plugin
84
+ for (h, _), plugin in self._plugins.items()
85
+ if h == host and plugin.enabled_by_default
86
+ ]
87
+ return tuple(sorted(host_plugins, key=lambda p: (p.order, p.name)))
88
+
89
+ def resolve_all(self) -> tuple["HostPlugin", ...]:
90
+ """Return all registered plugins, ordered by (order, name)."""
91
+ all_plugins = [p for p in self._plugins.values() if p.enabled_by_default]
92
+ return tuple(sorted(all_plugins, key=lambda p: (p.order, p.name)))
@@ -0,0 +1,49 @@
1
+ """Core types for the unified plugin extension system.
2
+
3
+ This module is a dependency leaf: it imports no host-specific modules.
4
+ All host modules (`actuator`, `swagger_ui`, custom) import from here.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any, Protocol
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ExtensionContext:
13
+ """Read-only metadata passed to plugin contributions.
14
+
15
+ Plugins should not assume any context fields exist beyond their host's
16
+ documented interface.
17
+ """
18
+
19
+ host: str
20
+ """Host name: 'actuator', 'swagger_ui', or custom."""
21
+
22
+ # Additional context fields added by specific hosts at contribution time
23
+
24
+
25
+ class HostPlugin(Protocol):
26
+ """Host-agnostic plugin protocol for all extension hosts.
27
+
28
+ Plugins implement this to contribute to any host (actuator, swagger_ui, custom).
29
+ The plugin registers itself and returns host-specific contributions.
30
+ """
31
+
32
+ name: str
33
+ """Unique plugin identifier. User plugins with same name override defaults."""
34
+
35
+ host: str
36
+ """Target host: 'actuator', 'swagger_ui', or custom host name."""
37
+
38
+ order: int
39
+ """Registration order within host (lower = earlier). Default 100."""
40
+
41
+ enabled_by_default: bool
42
+ """Whether this plugin is enabled unless explicitly disabled. Default True."""
43
+
44
+ def contribute(self, ctx: ExtensionContext) -> Any:
45
+ """Return host-specific contribution (e.g., SwaggerPluginContribution for swagger_ui).
46
+
47
+ Called at host initialization time. Returns should be idempotent.
48
+ May raise exceptions; host will log warnings and skip the plugin.
49
+ """
@@ -0,0 +1,6 @@
1
+ """Host adapters for actuator and swagger_ui."""
2
+
3
+ from ._actuator import ActuatorHostAdapter, ActuatorHostPlugin
4
+ from ._swagger import SwaggerHostAdapter
5
+
6
+ __all__ = ("ActuatorHostAdapter", "ActuatorHostPlugin", "SwaggerHostAdapter")
@@ -0,0 +1,57 @@
1
+ """Actuator host adapter.
2
+
3
+ Manages actuator plugins using the unified extension system.
4
+ Provides backward compatibility with existing ActuatorPlugin interface.
5
+ """
6
+
7
+ import logging
8
+ from collections.abc import Mapping, Sequence
9
+
10
+ from csrd.versioning.actuator.plugins.base import ActuatorLink
11
+ from csrd.versioning.actuator.plugins.base import ActuatorPlugin as LegacyActuatorPlugin
12
+
13
+ from .._types import ExtensionContext
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ActuatorHostPlugin:
19
+ """Adapter wrapping a legacy ActuatorPlugin for the unified extension system."""
20
+
21
+ def __init__(self, legacy_plugin: LegacyActuatorPlugin) -> None:
22
+ """Wrap an existing ActuatorPlugin."""
23
+ self._legacy_plugin = legacy_plugin
24
+ self.name: str = legacy_plugin.name
25
+ self.host: str = "actuator"
26
+ self.order: int = 100
27
+ self.enabled_by_default: bool = True
28
+
29
+ def contribute(self, ctx: ExtensionContext) -> Mapping[str, ActuatorLink]:
30
+ """Delegate to legacy plugin's register() method."""
31
+ # For backward compatibility, we call register() instead of contribute()
32
+ # This should be migrated when legacy plugin interface is removed
33
+ return {}
34
+
35
+
36
+ class ActuatorHostAdapter:
37
+ """Manages plugin resolution and execution for the actuator host."""
38
+
39
+ @staticmethod
40
+ def resolve_plugins(
41
+ defaults: Sequence[LegacyActuatorPlugin],
42
+ user_plugins: Sequence[LegacyActuatorPlugin] | None = None,
43
+ ) -> tuple[LegacyActuatorPlugin, ...]:
44
+ """Resolve plugins using current by-name override semantics.
45
+
46
+ This is the bridge function that maintains the existing
47
+ _resolve_plugins behavior while ready for future extension system integration.
48
+ """
49
+ resolved: dict[str, LegacyActuatorPlugin] = {plugin.name: plugin for plugin in defaults}
50
+
51
+ if user_plugins is not None:
52
+ for plugin in user_plugins:
53
+ if plugin.name in resolved:
54
+ logger.debug("Actuator plugin '%s' overridden by consumer", plugin.name)
55
+ resolved[plugin.name] = plugin
56
+
57
+ return tuple(resolved.values())
@@ -0,0 +1,45 @@
1
+ """Swagger UI host adapter.
2
+
3
+ Manages swagger plugins using the unified extension system.
4
+ Provides backward compatibility with existing SwaggerPlugin interface.
5
+ """
6
+
7
+ import logging
8
+ from collections.abc import Sequence
9
+
10
+ from csrd.versioning.swagger_plugins._base import SwaggerPlugin as LegacySwaggerPlugin
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SwaggerHostAdapter:
16
+ """Manages plugin resolution and execution for the swagger_ui host."""
17
+
18
+ @staticmethod
19
+ def resolve_plugins(
20
+ defaults: Sequence[LegacySwaggerPlugin],
21
+ global_plugins: Sequence[LegacySwaggerPlugin] | None = None,
22
+ per_version_plugins: Sequence[LegacySwaggerPlugin] | None = None,
23
+ ) -> tuple[LegacySwaggerPlugin, ...]:
24
+ """Resolve plugins using current by-name override semantics.
25
+
26
+ This is the bridge function that maintains the existing
27
+ _resolve_swagger_plugins behavior while ready for future extension system integration.
28
+ """
29
+ if global_plugins is not None and len(global_plugins) == 0:
30
+ return ()
31
+
32
+ resolved: dict[str, LegacySwaggerPlugin] = {p.name: p for p in defaults}
33
+
34
+ if global_plugins:
35
+ for p in global_plugins:
36
+ resolved[p.name] = p
37
+
38
+ if per_version_plugins is not None and len(per_version_plugins) == 0:
39
+ return ()
40
+
41
+ if per_version_plugins:
42
+ for p in per_version_plugins:
43
+ resolved[p.name] = p
44
+
45
+ return tuple(resolved.values())