provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
"""Command registration decorators."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any, TypeVar, overload
|
5
|
+
|
6
|
+
try:
|
7
|
+
import click
|
8
|
+
|
9
|
+
_HAS_CLICK = True
|
10
|
+
except ImportError:
|
11
|
+
click = None
|
12
|
+
_HAS_CLICK = False
|
13
|
+
|
14
|
+
|
15
|
+
# Defer click_builder import to avoid circular dependency
|
16
|
+
def _get_ensure_parent_groups():
|
17
|
+
if not _HAS_CLICK:
|
18
|
+
return None
|
19
|
+
from provide.foundation.hub.click_builder import ensure_parent_groups
|
20
|
+
|
21
|
+
return ensure_parent_groups
|
22
|
+
|
23
|
+
|
24
|
+
from provide.foundation.hub.info import CommandInfo
|
25
|
+
from provide.foundation.hub.registry import Registry, get_command_registry
|
26
|
+
from provide.foundation.logger import get_logger
|
27
|
+
|
28
|
+
log = get_logger(__name__)
|
29
|
+
|
30
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
31
|
+
|
32
|
+
|
33
|
+
@overload
|
34
|
+
def register_command(
|
35
|
+
name: str | None = None,
|
36
|
+
*,
|
37
|
+
description: str | None = None,
|
38
|
+
aliases: list[str] | None = None,
|
39
|
+
hidden: bool = False,
|
40
|
+
category: str | None = None,
|
41
|
+
group: bool = False,
|
42
|
+
replace: bool = False,
|
43
|
+
registry: Registry | None = None,
|
44
|
+
) -> Callable[[F], F]: ...
|
45
|
+
|
46
|
+
|
47
|
+
@overload
|
48
|
+
def register_command(
|
49
|
+
func: F,
|
50
|
+
/,
|
51
|
+
) -> F: ...
|
52
|
+
|
53
|
+
|
54
|
+
def register_command(
|
55
|
+
name_or_func: str | F | None = None,
|
56
|
+
*,
|
57
|
+
description: str | None = None,
|
58
|
+
aliases: list[str] | None = None,
|
59
|
+
hidden: bool = False,
|
60
|
+
category: str | None = None,
|
61
|
+
group: bool = False,
|
62
|
+
replace: bool = False,
|
63
|
+
registry: Registry | None = None,
|
64
|
+
**metadata: Any,
|
65
|
+
) -> Any:
|
66
|
+
"""
|
67
|
+
Register a CLI command in the hub.
|
68
|
+
|
69
|
+
Can be used as a decorator with or without arguments:
|
70
|
+
|
71
|
+
@register_command
|
72
|
+
def my_command():
|
73
|
+
pass
|
74
|
+
|
75
|
+
@register_command("custom-name", aliases=["cn"], category="utils")
|
76
|
+
def my_command():
|
77
|
+
pass
|
78
|
+
|
79
|
+
# Nested commands using dot notation - groups are auto-created
|
80
|
+
@register_command("container.status")
|
81
|
+
def container_status():
|
82
|
+
pass
|
83
|
+
|
84
|
+
@register_command("container.volumes.backup")
|
85
|
+
def container_volumes_backup():
|
86
|
+
pass
|
87
|
+
|
88
|
+
# Explicit group with custom description (optional)
|
89
|
+
@register_command("container", group=True, description="Container management")
|
90
|
+
def container_group():
|
91
|
+
pass
|
92
|
+
|
93
|
+
Args:
|
94
|
+
name_or_func: Command name using dot notation for nesting (e.g., "container.status")
|
95
|
+
description: Command description (defaults to docstring)
|
96
|
+
aliases: Alternative names for the command
|
97
|
+
hidden: Whether to hide from help listing
|
98
|
+
category: Command category for grouping
|
99
|
+
group: Whether this is a command group (not a command)
|
100
|
+
replace: Whether to replace existing registration
|
101
|
+
registry: Custom registry (defaults to global)
|
102
|
+
|
103
|
+
Returns:
|
104
|
+
Decorator function or decorated function
|
105
|
+
"""
|
106
|
+
# Handle @register_command (without parens)
|
107
|
+
if callable(name_or_func) and not isinstance(name_or_func, str):
|
108
|
+
func = name_or_func
|
109
|
+
return _register_command_func(
|
110
|
+
func,
|
111
|
+
name=None,
|
112
|
+
description=description,
|
113
|
+
aliases=aliases,
|
114
|
+
hidden=hidden,
|
115
|
+
category=category,
|
116
|
+
group=group,
|
117
|
+
replace=replace,
|
118
|
+
registry=registry,
|
119
|
+
**metadata,
|
120
|
+
)
|
121
|
+
|
122
|
+
# Handle @register_command(...) (with arguments)
|
123
|
+
def decorator(func: F) -> F:
|
124
|
+
return _register_command_func(
|
125
|
+
func,
|
126
|
+
name=name_or_func,
|
127
|
+
description=description,
|
128
|
+
aliases=aliases,
|
129
|
+
hidden=hidden,
|
130
|
+
category=category,
|
131
|
+
group=group,
|
132
|
+
replace=replace,
|
133
|
+
registry=registry,
|
134
|
+
**metadata,
|
135
|
+
)
|
136
|
+
|
137
|
+
return decorator
|
138
|
+
|
139
|
+
|
140
|
+
def _register_command_func(
|
141
|
+
func: F,
|
142
|
+
*,
|
143
|
+
name: str | None = None,
|
144
|
+
description: str | None = None,
|
145
|
+
aliases: list[str] | None = None,
|
146
|
+
hidden: bool = False,
|
147
|
+
category: str | None = None,
|
148
|
+
group: bool = False,
|
149
|
+
replace: bool = False,
|
150
|
+
registry: Registry | None = None,
|
151
|
+
**extra_metadata: Any,
|
152
|
+
) -> F:
|
153
|
+
"""Internal function to register a command."""
|
154
|
+
reg = registry or get_command_registry()
|
155
|
+
|
156
|
+
# Determine command name and parent from dot notation
|
157
|
+
if name:
|
158
|
+
parts = name.split(".")
|
159
|
+
if len(parts) > 1:
|
160
|
+
# Extract parent path and command name
|
161
|
+
parent = ".".join(parts[:-1])
|
162
|
+
command_name = parts[-1]
|
163
|
+
|
164
|
+
# Auto-create parent groups if they don't exist (click only)
|
165
|
+
if _HAS_CLICK:
|
166
|
+
ensure_parent_groups_fn = _get_ensure_parent_groups()
|
167
|
+
if ensure_parent_groups_fn:
|
168
|
+
ensure_parent_groups_fn(parent, reg)
|
169
|
+
else:
|
170
|
+
parent = None
|
171
|
+
command_name = name
|
172
|
+
else:
|
173
|
+
# Use function name as command name
|
174
|
+
parent = None
|
175
|
+
command_name = func.__name__
|
176
|
+
|
177
|
+
# Check if it's already a Click command
|
178
|
+
click_cmd = None
|
179
|
+
if isinstance(func, click.Command):
|
180
|
+
click_cmd = func
|
181
|
+
actual_func = func.callback
|
182
|
+
else:
|
183
|
+
actual_func = func
|
184
|
+
|
185
|
+
# Create command info
|
186
|
+
cmd_metadata = {"is_group": group}
|
187
|
+
cmd_metadata.update(extra_metadata)
|
188
|
+
|
189
|
+
info = CommandInfo(
|
190
|
+
name=command_name,
|
191
|
+
func=actual_func,
|
192
|
+
description=description or (actual_func.__doc__ if actual_func else None),
|
193
|
+
aliases=aliases or [],
|
194
|
+
hidden=hidden,
|
195
|
+
category=category,
|
196
|
+
metadata=cmd_metadata,
|
197
|
+
click_command=click_cmd,
|
198
|
+
parent=parent,
|
199
|
+
)
|
200
|
+
|
201
|
+
# Build full registry key
|
202
|
+
full_name = f"{parent}.{command_name}" if parent else command_name
|
203
|
+
|
204
|
+
# Build registry metadata
|
205
|
+
reg_metadata = {
|
206
|
+
"info": info,
|
207
|
+
"description": info.description,
|
208
|
+
"aliases": info.aliases,
|
209
|
+
"hidden": hidden,
|
210
|
+
"category": category,
|
211
|
+
"parent": parent,
|
212
|
+
"is_group": group,
|
213
|
+
"click_command": click_cmd,
|
214
|
+
}
|
215
|
+
reg_metadata.update(extra_metadata)
|
216
|
+
|
217
|
+
# Register in the registry
|
218
|
+
reg.register(
|
219
|
+
name=full_name,
|
220
|
+
value=func,
|
221
|
+
dimension="command",
|
222
|
+
metadata=reg_metadata,
|
223
|
+
aliases=aliases,
|
224
|
+
replace=replace,
|
225
|
+
)
|
226
|
+
|
227
|
+
# Add metadata to the function
|
228
|
+
func.__registry_name__ = command_name
|
229
|
+
func.__registry_dimension__ = "command"
|
230
|
+
func.__registry_info__ = info
|
231
|
+
|
232
|
+
log.info(
|
233
|
+
"Registered command",
|
234
|
+
name=command_name,
|
235
|
+
parent=parent,
|
236
|
+
aliases=aliases,
|
237
|
+
hidden=hidden,
|
238
|
+
category=category,
|
239
|
+
)
|
240
|
+
|
241
|
+
return func
|
242
|
+
|
243
|
+
|
244
|
+
__all__ = ["register_command"]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"""Command information and metadata structures."""
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from attrs import define, field
|
7
|
+
|
8
|
+
try:
|
9
|
+
import click
|
10
|
+
|
11
|
+
_HAS_CLICK = True
|
12
|
+
except ImportError:
|
13
|
+
click = None
|
14
|
+
_HAS_CLICK = False
|
15
|
+
|
16
|
+
|
17
|
+
@define(frozen=True, slots=True)
|
18
|
+
class CommandInfo:
|
19
|
+
"""Information about a registered command."""
|
20
|
+
|
21
|
+
name: str
|
22
|
+
func: Callable[..., Any]
|
23
|
+
description: str | None = None
|
24
|
+
aliases: list[str] = field(factory=lambda: [])
|
25
|
+
hidden: bool = False
|
26
|
+
category: str | None = None
|
27
|
+
metadata: dict[str, Any] = field(factory=lambda: {})
|
28
|
+
click_command: "click.Command | None" = None
|
29
|
+
parent: str | None = None # Parent path extracted from dot notation
|
30
|
+
|
31
|
+
|
32
|
+
__all__ = ["CommandInfo"]
|
@@ -0,0 +1,446 @@
|
|
1
|
+
"""
|
2
|
+
Hub manager - the main coordinator for components and commands.
|
3
|
+
|
4
|
+
This module provides the Hub class that coordinates component and command
|
5
|
+
registration, discovery, and access.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from collections.abc import Callable
|
11
|
+
import threading
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
try:
|
15
|
+
import click
|
16
|
+
|
17
|
+
_HAS_CLICK = True
|
18
|
+
except ImportError:
|
19
|
+
click = None
|
20
|
+
_HAS_CLICK = False
|
21
|
+
|
22
|
+
from provide.foundation.context import Context
|
23
|
+
from provide.foundation.errors.config import ValidationError
|
24
|
+
from provide.foundation.errors.decorators import with_error_handling
|
25
|
+
from provide.foundation.errors.resources import AlreadyExistsError
|
26
|
+
from provide.foundation.hub.commands import (
|
27
|
+
CommandInfo,
|
28
|
+
get_command_registry,
|
29
|
+
)
|
30
|
+
from provide.foundation.hub.components import (
|
31
|
+
ComponentInfo,
|
32
|
+
discover_components as _discover_components,
|
33
|
+
get_component_registry,
|
34
|
+
)
|
35
|
+
from provide.foundation.hub.registry import Registry
|
36
|
+
from provide.foundation.logger import get_logger
|
37
|
+
|
38
|
+
log = get_logger(__name__)
|
39
|
+
|
40
|
+
|
41
|
+
class Hub:
|
42
|
+
"""
|
43
|
+
Central hub for managing components and commands.
|
44
|
+
|
45
|
+
The Hub provides a unified interface for:
|
46
|
+
- Registering components and commands
|
47
|
+
- Discovering plugins via entry points
|
48
|
+
- Creating Click CLI applications
|
49
|
+
- Managing component lifecycle
|
50
|
+
|
51
|
+
Example:
|
52
|
+
>>> hub = Hub()
|
53
|
+
>>> hub.add_component(MyResource, "resource")
|
54
|
+
>>> hub.add_command(init_cmd, "init")
|
55
|
+
>>>
|
56
|
+
>>> # Create CLI with all commands
|
57
|
+
>>> cli = hub.create_cli()
|
58
|
+
>>> cli()
|
59
|
+
"""
|
60
|
+
|
61
|
+
def __init__(
|
62
|
+
self,
|
63
|
+
context: Context | None = None,
|
64
|
+
component_registry: Registry | None = None,
|
65
|
+
command_registry: Registry | None = None,
|
66
|
+
) -> None:
|
67
|
+
"""
|
68
|
+
Initialize the hub.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
context: Foundation context for configuration
|
72
|
+
component_registry: Custom component registry
|
73
|
+
command_registry: Custom command registry
|
74
|
+
"""
|
75
|
+
self.context = context or Context()
|
76
|
+
self._component_registry = component_registry or get_component_registry()
|
77
|
+
self._command_registry = command_registry or get_command_registry()
|
78
|
+
self._cli_group: click.Group | None = None
|
79
|
+
|
80
|
+
# Component Management
|
81
|
+
|
82
|
+
@with_error_handling(
|
83
|
+
context_provider=lambda: {"hub": "add_component"},
|
84
|
+
error_mapper=lambda e: ValidationError(
|
85
|
+
f"Failed to add component: {e}", code="HUB_COMPONENT_ADD_ERROR", cause=e
|
86
|
+
)
|
87
|
+
if not isinstance(e, AlreadyExistsError | ValidationError)
|
88
|
+
else e,
|
89
|
+
)
|
90
|
+
def add_component(
|
91
|
+
self,
|
92
|
+
component_class: type[Any],
|
93
|
+
name: str | None = None,
|
94
|
+
dimension: str = "component",
|
95
|
+
**metadata: Any,
|
96
|
+
) -> ComponentInfo:
|
97
|
+
"""
|
98
|
+
Add a component to the hub.
|
99
|
+
|
100
|
+
Args:
|
101
|
+
component_class: Component class to register
|
102
|
+
name: Optional name (defaults to class name)
|
103
|
+
dimension: Registry dimension
|
104
|
+
**metadata: Additional metadata
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
ComponentInfo for the registered component
|
108
|
+
|
109
|
+
Raises:
|
110
|
+
AlreadyExistsError: If component is already registered
|
111
|
+
ValidationError: If component class is invalid
|
112
|
+
"""
|
113
|
+
if not isinstance(component_class, type):
|
114
|
+
raise ValidationError(
|
115
|
+
f"Component must be a class, got {type(component_class).__name__}",
|
116
|
+
code="HUB_INVALID_COMPONENT",
|
117
|
+
component_type=type(component_class).__name__,
|
118
|
+
)
|
119
|
+
|
120
|
+
component_name = name or component_class.__name__
|
121
|
+
|
122
|
+
# Check if already exists
|
123
|
+
if self._component_registry.get_entry(component_name, dimension=dimension):
|
124
|
+
raise AlreadyExistsError(
|
125
|
+
f"Component '{component_name}' already registered in dimension '{dimension}'",
|
126
|
+
code="HUB_COMPONENT_EXISTS",
|
127
|
+
component_name=component_name,
|
128
|
+
dimension=dimension,
|
129
|
+
)
|
130
|
+
|
131
|
+
info = ComponentInfo(
|
132
|
+
name=component_name,
|
133
|
+
component_class=component_class,
|
134
|
+
dimension=dimension,
|
135
|
+
version=metadata.get("version"),
|
136
|
+
description=metadata.get("description", component_class.__doc__),
|
137
|
+
author=metadata.get("author"),
|
138
|
+
tags=metadata.get("tags", []),
|
139
|
+
metadata=metadata,
|
140
|
+
)
|
141
|
+
|
142
|
+
self._component_registry.register(
|
143
|
+
name=component_name,
|
144
|
+
value=component_class,
|
145
|
+
dimension=dimension,
|
146
|
+
metadata={"info": info, **metadata},
|
147
|
+
replace=False, # Don't allow replacement by default
|
148
|
+
)
|
149
|
+
|
150
|
+
log.info(
|
151
|
+
"Added component to hub",
|
152
|
+
name=component_name,
|
153
|
+
dimension=dimension,
|
154
|
+
)
|
155
|
+
|
156
|
+
return info
|
157
|
+
|
158
|
+
def get_component(
|
159
|
+
self,
|
160
|
+
name: str,
|
161
|
+
dimension: str | None = None,
|
162
|
+
) -> type[Any] | None:
|
163
|
+
"""
|
164
|
+
Get a component by name.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
name: Component name
|
168
|
+
dimension: Optional dimension filter
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
Component class or None
|
172
|
+
"""
|
173
|
+
return self._component_registry.get(name, dimension)
|
174
|
+
|
175
|
+
def list_components(
|
176
|
+
self,
|
177
|
+
dimension: str | None = None,
|
178
|
+
) -> list[str]:
|
179
|
+
"""
|
180
|
+
List component names.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
dimension: Optional dimension filter
|
184
|
+
|
185
|
+
Returns:
|
186
|
+
List of component names
|
187
|
+
"""
|
188
|
+
if dimension:
|
189
|
+
return self._component_registry.list_dimension(dimension)
|
190
|
+
|
191
|
+
# List all non-command dimensions
|
192
|
+
all_items = self._component_registry.list_all()
|
193
|
+
components = []
|
194
|
+
for dim, names in all_items.items():
|
195
|
+
if dim != "command":
|
196
|
+
components.extend(names)
|
197
|
+
return components
|
198
|
+
|
199
|
+
def discover_components(
|
200
|
+
self,
|
201
|
+
group: str,
|
202
|
+
dimension: str = "component",
|
203
|
+
) -> dict[str, type[Any]]:
|
204
|
+
"""
|
205
|
+
Discover and register components from entry points.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
group: Entry point group name
|
209
|
+
dimension: Dimension to register under
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
Dictionary of discovered components
|
213
|
+
"""
|
214
|
+
return _discover_components(group, dimension, self._component_registry)
|
215
|
+
|
216
|
+
# Command Management
|
217
|
+
|
218
|
+
def add_command(
|
219
|
+
self,
|
220
|
+
func: Callable[..., Any] | click.Command,
|
221
|
+
name: str | None = None,
|
222
|
+
**kwargs: Any,
|
223
|
+
) -> CommandInfo:
|
224
|
+
"""
|
225
|
+
Add a CLI command to the hub.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
func: Command function or Click command
|
229
|
+
name: Optional name (defaults to function name)
|
230
|
+
**kwargs: Additional command options
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
CommandInfo for the registered command
|
234
|
+
"""
|
235
|
+
if _HAS_CLICK and isinstance(func, click.Command):
|
236
|
+
command_name = name or func.name
|
237
|
+
command_func = func.callback
|
238
|
+
click_command = func
|
239
|
+
else:
|
240
|
+
command_name = name or func.__name__.replace("_", "-")
|
241
|
+
command_func = func
|
242
|
+
click_command = None
|
243
|
+
|
244
|
+
info = CommandInfo(
|
245
|
+
name=command_name,
|
246
|
+
func=command_func,
|
247
|
+
description=kwargs.get("description", getattr(func, "__doc__", None)),
|
248
|
+
aliases=kwargs.get("aliases", []),
|
249
|
+
hidden=kwargs.get("hidden", False),
|
250
|
+
category=kwargs.get("category"),
|
251
|
+
metadata=kwargs,
|
252
|
+
click_command=click_command,
|
253
|
+
)
|
254
|
+
|
255
|
+
self._command_registry.register(
|
256
|
+
name=command_name,
|
257
|
+
value=func,
|
258
|
+
dimension="command",
|
259
|
+
metadata={
|
260
|
+
"info": info,
|
261
|
+
"click_command": click_command,
|
262
|
+
**kwargs,
|
263
|
+
},
|
264
|
+
aliases=info.aliases,
|
265
|
+
)
|
266
|
+
|
267
|
+
# Add to CLI group if it exists
|
268
|
+
if self._cli_group and click_command:
|
269
|
+
self._cli_group.add_command(click_command)
|
270
|
+
|
271
|
+
log.info(
|
272
|
+
"Added command to hub",
|
273
|
+
name=command_name,
|
274
|
+
aliases=info.aliases,
|
275
|
+
)
|
276
|
+
|
277
|
+
return info
|
278
|
+
|
279
|
+
def get_command(self, name: str) -> Callable[..., Any] | None:
|
280
|
+
"""
|
281
|
+
Get a command by name.
|
282
|
+
|
283
|
+
Args:
|
284
|
+
name: Command name or alias
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
Command function or None
|
288
|
+
"""
|
289
|
+
return self._command_registry.get(name, dimension="command")
|
290
|
+
|
291
|
+
def list_commands(self) -> list[str]:
|
292
|
+
"""
|
293
|
+
List all command names.
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
List of command names
|
297
|
+
"""
|
298
|
+
return self._command_registry.list_dimension("command")
|
299
|
+
|
300
|
+
# CLI Integration
|
301
|
+
|
302
|
+
def create_cli(
|
303
|
+
self,
|
304
|
+
name: str = "cli",
|
305
|
+
version: str | None = None,
|
306
|
+
**kwargs: Any,
|
307
|
+
) -> click.Group:
|
308
|
+
"""
|
309
|
+
Create a Click CLI with all registered commands.
|
310
|
+
|
311
|
+
Requires click to be installed.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
name: CLI name
|
315
|
+
version: CLI version
|
316
|
+
**kwargs: Additional Click Group options
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
Click Group with registered commands
|
320
|
+
|
321
|
+
Example:
|
322
|
+
>>> hub = get_hub()
|
323
|
+
>>> cli = hub.create_cli("myapp", version="1.0.0")
|
324
|
+
>>>
|
325
|
+
>>> if __name__ == "__main__":
|
326
|
+
>>> cli()
|
327
|
+
"""
|
328
|
+
if not _HAS_CLICK:
|
329
|
+
raise ImportError(
|
330
|
+
"CLI creation requires: pip install 'provide-foundation[cli]'"
|
331
|
+
)
|
332
|
+
|
333
|
+
from provide.foundation.hub.commands import create_command_group
|
334
|
+
|
335
|
+
# Use create_command_group which now handles nested groups
|
336
|
+
cli = create_command_group(name=name, registry=self._command_registry, **kwargs)
|
337
|
+
|
338
|
+
# Add version option if provided
|
339
|
+
if version:
|
340
|
+
cli = click.version_option(version=version)(cli)
|
341
|
+
|
342
|
+
self._cli_group = cli
|
343
|
+
return cli
|
344
|
+
|
345
|
+
def add_cli_group(self, group: click.Group) -> None:
|
346
|
+
"""
|
347
|
+
Add an existing Click group to the hub.
|
348
|
+
|
349
|
+
This registers all commands from the group.
|
350
|
+
|
351
|
+
Args:
|
352
|
+
group: Click Group to add
|
353
|
+
"""
|
354
|
+
for name, cmd in group.commands.items():
|
355
|
+
self.add_command(cmd, name)
|
356
|
+
|
357
|
+
# Lifecycle Management
|
358
|
+
|
359
|
+
def initialize(self) -> None:
|
360
|
+
"""Initialize all components that support initialization."""
|
361
|
+
for entry in self._component_registry:
|
362
|
+
if entry.dimension == "command":
|
363
|
+
continue
|
364
|
+
|
365
|
+
component_class = entry.value
|
366
|
+
if hasattr(component_class, "initialize"):
|
367
|
+
try:
|
368
|
+
component_class.initialize()
|
369
|
+
log.debug(f"Initialized component: {entry.name}")
|
370
|
+
except Exception as e:
|
371
|
+
log.error(f"Failed to initialize {entry.name}: {e}")
|
372
|
+
|
373
|
+
def cleanup(self) -> None:
|
374
|
+
"""Cleanup all components that support cleanup."""
|
375
|
+
for entry in self._component_registry:
|
376
|
+
if entry.dimension == "command":
|
377
|
+
continue
|
378
|
+
|
379
|
+
component_class = entry.value
|
380
|
+
if hasattr(component_class, "cleanup"):
|
381
|
+
try:
|
382
|
+
component_class.cleanup()
|
383
|
+
log.debug(f"Cleaned up component: {entry.name}")
|
384
|
+
except Exception as e:
|
385
|
+
log.error(f"Failed to cleanup {entry.name}: {e}")
|
386
|
+
|
387
|
+
def clear(self, dimension: str | None = None) -> None:
|
388
|
+
"""
|
389
|
+
Clear registrations.
|
390
|
+
|
391
|
+
Args:
|
392
|
+
dimension: Optional dimension to clear (None = all)
|
393
|
+
"""
|
394
|
+
if dimension == "command" or dimension is None:
|
395
|
+
self._command_registry.clear(dimension="command" if dimension else None)
|
396
|
+
self._cli_group = None
|
397
|
+
|
398
|
+
if dimension != "command" or dimension is None:
|
399
|
+
self._component_registry.clear(dimension=dimension)
|
400
|
+
|
401
|
+
def __enter__(self) -> Hub:
|
402
|
+
"""Context manager entry."""
|
403
|
+
self.initialize()
|
404
|
+
return self
|
405
|
+
|
406
|
+
def __exit__(self, *args: Any) -> None:
|
407
|
+
"""Context manager exit."""
|
408
|
+
self.cleanup()
|
409
|
+
|
410
|
+
|
411
|
+
# Global hub instance and lock for thread-safe initialization
|
412
|
+
_global_hub: Hub | None = None
|
413
|
+
_hub_lock = threading.Lock()
|
414
|
+
|
415
|
+
|
416
|
+
def get_hub() -> Hub:
|
417
|
+
"""
|
418
|
+
Get the global hub instance.
|
419
|
+
|
420
|
+
Thread-safe: Uses double-checked locking pattern for efficient lazy initialization.
|
421
|
+
|
422
|
+
Returns:
|
423
|
+
Global Hub instance (created if needed)
|
424
|
+
"""
|
425
|
+
global _global_hub
|
426
|
+
|
427
|
+
# Fast path: hub already initialized
|
428
|
+
if _global_hub is not None:
|
429
|
+
return _global_hub
|
430
|
+
|
431
|
+
# Slow path: need to initialize hub
|
432
|
+
with _hub_lock:
|
433
|
+
# Double-check after acquiring lock
|
434
|
+
if _global_hub is None:
|
435
|
+
_global_hub = Hub()
|
436
|
+
|
437
|
+
return _global_hub
|
438
|
+
|
439
|
+
|
440
|
+
def clear_hub() -> None:
|
441
|
+
"""Clear the global hub instance."""
|
442
|
+
global _global_hub
|
443
|
+
with _hub_lock:
|
444
|
+
if _global_hub:
|
445
|
+
_global_hub.clear()
|
446
|
+
_global_hub = None
|