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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. 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