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,175 @@
1
+ """File-based locking for concurrent access control."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ import time
6
+
7
+ from provide.foundation.errors.resources import LockError
8
+ from provide.foundation.logger import get_logger
9
+
10
+ log = get_logger(__name__)
11
+
12
+
13
+ class FileLock:
14
+ """File-based lock for concurrent access control.
15
+
16
+ Uses exclusive file creation as the locking mechanism.
17
+ The lock file contains the PID of the process holding the lock.
18
+
19
+ Example:
20
+ with FileLock("/tmp/myapp.lock"):
21
+ # Exclusive access to resource
22
+ do_something()
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ path: Path | str,
28
+ timeout: float = 10.0,
29
+ check_interval: float = 0.1,
30
+ ) -> None:
31
+ """Initialize file lock.
32
+
33
+ Args:
34
+ path: Lock file path
35
+ timeout: Max seconds to wait for lock
36
+ check_interval: Seconds between lock checks
37
+ """
38
+ self.path = Path(path)
39
+ self.timeout = timeout
40
+ self.check_interval = check_interval
41
+ self.locked = False
42
+ self.pid = os.getpid()
43
+
44
+ def acquire(self, blocking: bool = True) -> bool:
45
+ """Acquire the lock.
46
+
47
+ Args:
48
+ blocking: If True, wait for lock. If False, return immediately.
49
+
50
+ Returns:
51
+ True if lock acquired, False if not (non-blocking mode only)
52
+
53
+ Raises:
54
+ LockError: If timeout exceeded (blocking mode)
55
+ """
56
+ start_time = time.time()
57
+
58
+ while True:
59
+ try:
60
+ # Try to create lock file exclusively
61
+ fd = os.open(
62
+ str(self.path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644
63
+ )
64
+ try:
65
+ # Write our PID to the lock file
66
+ os.write(fd, str(self.pid).encode())
67
+ finally:
68
+ os.close(fd)
69
+
70
+ self.locked = True
71
+ log.debug("Acquired lock", path=str(self.path), pid=self.pid)
72
+ return True
73
+
74
+ except FileExistsError:
75
+ # Lock file exists, check if holder is still alive
76
+ if self._check_stale_lock():
77
+ continue # Retry after removing stale lock
78
+
79
+ if not blocking:
80
+ log.debug("Lock unavailable (non-blocking)", path=str(self.path))
81
+ return False
82
+
83
+ # Check timeout
84
+ if time.time() - start_time > self.timeout:
85
+ raise LockError(
86
+ f"Failed to acquire lock within {self.timeout}s",
87
+ code="LOCK_TIMEOUT",
88
+ path=str(self.path),
89
+ )
90
+
91
+ # Wait before retry
92
+ time.sleep(self.check_interval)
93
+
94
+ def release(self) -> None:
95
+ """Release the lock.
96
+
97
+ Only removes the lock file if we own it.
98
+ """
99
+ if not self.locked:
100
+ return
101
+
102
+ try:
103
+ # Verify we own the lock before removing
104
+ if self.path.exists():
105
+ try:
106
+ content = self.path.read_text().strip()
107
+ if content == str(self.pid):
108
+ self.path.unlink()
109
+ log.debug("Released lock", path=str(self.path), pid=self.pid)
110
+ else:
111
+ log.warning(
112
+ "Lock owned by different process",
113
+ path=str(self.path),
114
+ owner_pid=content,
115
+ our_pid=self.pid,
116
+ )
117
+ except Exception as e:
118
+ log.warning(
119
+ "Error checking lock ownership",
120
+ path=str(self.path),
121
+ error=str(e),
122
+ )
123
+ # Still try to remove if we think we own it
124
+ if self.locked:
125
+ self.path.unlink()
126
+ except FileNotFoundError:
127
+ pass # Lock already released
128
+ except Exception as e:
129
+ log.error("Failed to release lock", path=str(self.path), error=str(e))
130
+ finally:
131
+ self.locked = False
132
+
133
+ def _check_stale_lock(self) -> bool:
134
+ """Check if lock file is stale and remove if so.
135
+
136
+ Returns:
137
+ True if stale lock was removed, False otherwise
138
+ """
139
+ try:
140
+ content = self.path.read_text().strip()
141
+ if content.isdigit():
142
+ lock_pid = int(content)
143
+
144
+ # Check if process is still alive
145
+ try:
146
+ os.kill(lock_pid, 0)
147
+ # Process exists
148
+ return False
149
+ except ProcessLookupError:
150
+ # Process doesn't exist, lock is stale
151
+ log.warning(
152
+ "Removing stale lock", path=str(self.path), stale_pid=lock_pid
153
+ )
154
+ self.path.unlink()
155
+ return True
156
+ except Exception as e:
157
+ log.debug("Error checking stale lock", path=str(self.path), error=str(e))
158
+
159
+ return False
160
+
161
+ def __enter__(self):
162
+ """Context manager entry."""
163
+ self.acquire()
164
+ return self
165
+
166
+ def __exit__(self, exc_type, exc_val, exc_tb):
167
+ """Context manager exit."""
168
+ self.release()
169
+ return False # Don't suppress exceptions
170
+
171
+
172
+ __all__ = [
173
+ "FileLock",
174
+ "LockError",
175
+ ]
@@ -0,0 +1,179 @@
1
+ """Safe file operations with error handling and defaults."""
2
+
3
+ from pathlib import Path
4
+ import shutil
5
+
6
+ from provide.foundation.logger import get_logger
7
+
8
+ log = get_logger(__name__)
9
+
10
+
11
+ def safe_read(
12
+ path: Path | str,
13
+ default: bytes | None = None,
14
+ encoding: str | None = None,
15
+ ) -> bytes | str | None:
16
+ """Read file safely, returning default if not found.
17
+
18
+ Args:
19
+ path: File to read
20
+ default: Value to return if file doesn't exist
21
+ encoding: If provided, decode bytes to str
22
+
23
+ Returns:
24
+ File contents or default value
25
+ """
26
+ path = Path(path)
27
+
28
+ try:
29
+ data = path.read_bytes()
30
+ if encoding:
31
+ return data.decode(encoding)
32
+ return data
33
+ except FileNotFoundError:
34
+ log.debug("File not found, returning default", path=str(path))
35
+ if default is not None and encoding:
36
+ return default.decode(encoding) if isinstance(default, bytes) else default
37
+ return default
38
+ except Exception as e:
39
+ log.warning("Failed to read file", path=str(path), error=str(e))
40
+ return default
41
+
42
+
43
+ def safe_read_text(
44
+ path: Path | str,
45
+ default: str = "",
46
+ encoding: str = "utf-8",
47
+ ) -> str:
48
+ """Read text file safely with default.
49
+
50
+ Args:
51
+ path: File to read
52
+ default: Default text if file doesn't exist
53
+ encoding: Text encoding
54
+
55
+ Returns:
56
+ File contents or default text
57
+ """
58
+ result = safe_read(path, default=default.encode(encoding), encoding=encoding)
59
+ return result if isinstance(result, str) else default
60
+
61
+
62
+ def safe_delete(
63
+ path: Path | str,
64
+ missing_ok: bool = True,
65
+ ) -> bool:
66
+ """Delete file safely.
67
+
68
+ Args:
69
+ path: File to delete
70
+ missing_ok: If True, don't raise error if file doesn't exist
71
+
72
+ Returns:
73
+ True if deleted, False if didn't exist
74
+
75
+ Raises:
76
+ OSError: If deletion fails and file exists
77
+ """
78
+ path = Path(path)
79
+
80
+ try:
81
+ path.unlink()
82
+ log.debug("Deleted file", path=str(path))
83
+ return True
84
+ except FileNotFoundError:
85
+ if missing_ok:
86
+ log.debug("File already absent", path=str(path))
87
+ return False
88
+ raise
89
+ except Exception as e:
90
+ log.error("Failed to delete file", path=str(path), error=str(e))
91
+ raise
92
+
93
+
94
+ def safe_move(
95
+ src: Path | str,
96
+ dst: Path | str,
97
+ overwrite: bool = False,
98
+ ) -> None:
99
+ """Move file safely with optional overwrite.
100
+
101
+ Args:
102
+ src: Source file path
103
+ dst: Destination file path
104
+ overwrite: Whether to overwrite existing destination
105
+
106
+ Raises:
107
+ FileNotFoundError: If source doesn't exist
108
+ FileExistsError: If destination exists and overwrite=False
109
+ OSError: If move operation fails
110
+ """
111
+ src = Path(src)
112
+ dst = Path(dst)
113
+
114
+ if not src.exists():
115
+ raise FileNotFoundError(f"Source file does not exist: {src}")
116
+
117
+ if dst.exists() and not overwrite:
118
+ raise FileExistsError(f"Destination already exists: {dst}")
119
+
120
+ # Ensure destination directory exists
121
+ dst.parent.mkdir(parents=True, exist_ok=True)
122
+
123
+ try:
124
+ shutil.move(str(src), str(dst))
125
+ log.debug("Moved file", src=str(src), dst=str(dst))
126
+ except Exception as e:
127
+ log.error("Failed to move file", src=str(src), dst=str(dst), error=str(e))
128
+ raise
129
+
130
+
131
+ def safe_copy(
132
+ src: Path | str,
133
+ dst: Path | str,
134
+ overwrite: bool = False,
135
+ preserve_mode: bool = True,
136
+ ) -> None:
137
+ """Copy file safely with metadata preservation.
138
+
139
+ Args:
140
+ src: Source file path
141
+ dst: Destination file path
142
+ overwrite: Whether to overwrite existing destination
143
+ preserve_mode: Whether to preserve file permissions
144
+
145
+ Raises:
146
+ FileNotFoundError: If source doesn't exist
147
+ FileExistsError: If destination exists and overwrite=False
148
+ OSError: If copy operation fails
149
+ """
150
+ src = Path(src)
151
+ dst = Path(dst)
152
+
153
+ if not src.exists():
154
+ raise FileNotFoundError(f"Source file does not exist: {src}")
155
+
156
+ if dst.exists() and not overwrite:
157
+ raise FileExistsError(f"Destination already exists: {dst}")
158
+
159
+ # Ensure destination directory exists
160
+ dst.parent.mkdir(parents=True, exist_ok=True)
161
+
162
+ try:
163
+ if preserve_mode:
164
+ shutil.copy2(str(src), str(dst))
165
+ else:
166
+ shutil.copy(str(src), str(dst))
167
+ log.debug("Copied file", src=str(src), dst=str(dst))
168
+ except Exception as e:
169
+ log.error("Failed to copy file", src=str(src), dst=str(dst), error=str(e))
170
+ raise
171
+
172
+
173
+ __all__ = [
174
+ "safe_copy",
175
+ "safe_delete",
176
+ "safe_move",
177
+ "safe_read",
178
+ "safe_read_text",
179
+ ]
@@ -0,0 +1,170 @@
1
+ """File utility functions."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ import shutil
6
+
7
+ from provide.foundation.logger import get_logger
8
+
9
+ log = get_logger(__name__)
10
+
11
+
12
+ def get_size(path: Path | str) -> int:
13
+ """Get file size in bytes, 0 if not exists.
14
+
15
+ Args:
16
+ path: File path
17
+
18
+ Returns:
19
+ Size in bytes, or 0 if file doesn't exist
20
+ """
21
+ path = Path(path)
22
+
23
+ try:
24
+ return path.stat().st_size
25
+ except FileNotFoundError:
26
+ return 0
27
+ except Exception as e:
28
+ log.warning("Failed to get file size", path=str(path), error=str(e))
29
+ return 0
30
+
31
+
32
+ def get_mtime(path: Path | str) -> float | None:
33
+ """Get modification time, None if not exists.
34
+
35
+ Args:
36
+ path: File path
37
+
38
+ Returns:
39
+ Modification time as timestamp, or None if doesn't exist
40
+ """
41
+ path = Path(path)
42
+
43
+ try:
44
+ return path.stat().st_mtime
45
+ except FileNotFoundError:
46
+ return None
47
+ except Exception as e:
48
+ log.warning("Failed to get modification time", path=str(path), error=str(e))
49
+ return None
50
+
51
+
52
+ def touch(
53
+ path: Path | str,
54
+ mode: int = 0o644,
55
+ exist_ok: bool = True,
56
+ ) -> None:
57
+ """Create empty file or update timestamp.
58
+
59
+ Args:
60
+ path: File path
61
+ mode: File permissions for new files
62
+ exist_ok: If False, raise error if file exists
63
+
64
+ Raises:
65
+ FileExistsError: If exist_ok=False and file exists
66
+ """
67
+ path = Path(path)
68
+
69
+ if path.exists() and not exist_ok:
70
+ raise FileExistsError(f"File already exists: {path}")
71
+
72
+ # Ensure parent directory exists
73
+ path.parent.mkdir(parents=True, exist_ok=True)
74
+
75
+ # Touch the file
76
+ path.touch(mode=mode, exist_ok=exist_ok)
77
+ log.debug("Touched file", path=str(path))
78
+
79
+
80
+ def find_files(
81
+ pattern: str,
82
+ root: Path | str = ".",
83
+ recursive: bool = True,
84
+ ) -> list[Path]:
85
+ """Find files matching pattern.
86
+
87
+ Args:
88
+ pattern: Glob pattern (e.g., "*.py", "**/*.json")
89
+ root: Root directory to search from
90
+ recursive: If True, search recursively
91
+
92
+ Returns:
93
+ List of matching file paths
94
+ """
95
+ root = Path(root)
96
+
97
+ if not root.exists():
98
+ log.warning("Search root doesn't exist", root=str(root))
99
+ return []
100
+
101
+ # Use glob or rglob based on recursive flag
102
+ if recursive and "**" not in pattern:
103
+ pattern = f"**/{pattern}"
104
+
105
+ try:
106
+ if recursive:
107
+ matches = list(root.glob(pattern))
108
+ else:
109
+ matches = list(root.glob(pattern.lstrip("/")))
110
+
111
+ # Filter to files only
112
+ files = [p for p in matches if p.is_file()]
113
+
114
+ log.debug("Found files", pattern=pattern, root=str(root), count=len(files))
115
+ return files
116
+ except Exception as e:
117
+ log.error("Failed to find files", pattern=pattern, root=str(root), error=str(e))
118
+ return []
119
+
120
+
121
+ def backup_file(
122
+ path: Path | str,
123
+ suffix: str = ".bak",
124
+ timestamp: bool = False,
125
+ ) -> Path | None:
126
+ """Create backup copy of file.
127
+
128
+ Args:
129
+ path: File to backup
130
+ suffix: Backup suffix
131
+ timestamp: If True, add timestamp to backup name
132
+
133
+ Returns:
134
+ Path to backup file, or None if source doesn't exist
135
+ """
136
+ path = Path(path)
137
+
138
+ if not path.exists():
139
+ log.debug("Source file doesn't exist, no backup created", path=str(path))
140
+ return None
141
+
142
+ # Build backup filename
143
+ if timestamp:
144
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
145
+ backup_path = path.with_suffix(f".{ts}{suffix}")
146
+ else:
147
+ backup_path = path.with_suffix(path.suffix + suffix)
148
+
149
+ # Find unique name if backup already exists
150
+ counter = 1
151
+ while backup_path.exists():
152
+ backup_path = path.with_suffix(f"{path.suffix}{suffix}.{counter}")
153
+ counter += 1
154
+
155
+ try:
156
+ shutil.copy2(str(path), str(backup_path))
157
+ log.debug("Created backup", source=str(path), backup=str(backup_path))
158
+ return backup_path
159
+ except Exception as e:
160
+ log.error("Failed to create backup", path=str(path), error=str(e))
161
+ return None
162
+
163
+
164
+ __all__ = [
165
+ "backup_file",
166
+ "find_files",
167
+ "get_mtime",
168
+ "get_size",
169
+ "touch",
170
+ ]
@@ -0,0 +1,88 @@
1
+ """
2
+ Provide Foundation Hub - Component and Command Coordination System
3
+ ===================================================================
4
+
5
+ The hub module provides a unified system for registering, discovering, and
6
+ managing components and CLI commands across the provide-io ecosystem.
7
+
8
+ Key Features:
9
+ - Multi-dimensional component registry
10
+ - CLI command registration and discovery
11
+ - Entry point discovery
12
+ - Integration with Click framework
13
+ - Type-safe decorators using Python 3.11+ features
14
+
15
+ Example Usage:
16
+ >>> from provide.foundation.hub import Hub, register_command
17
+ >>>
18
+ >>> class MyResource:
19
+ >>> def __init__(self, name: str):
20
+ >>> self.name = name
21
+ >>>
22
+ >>> @register_command("init")
23
+ >>> def init_command():
24
+ >>> pass
25
+ >>>
26
+ >>> hub = Hub()
27
+ >>> hub.add_component(MyResource, name="my_resource", version="1.0.0")
28
+ >>> resource_class = hub.get_component("my_resource")
29
+ >>> command = hub.get_command("init")
30
+ """
31
+
32
+ # Core hub components (always available)
33
+ from provide.foundation.hub.components import (
34
+ ComponentCategory,
35
+ get_component_registry,
36
+ )
37
+ from provide.foundation.hub.decorators import register_command
38
+ from provide.foundation.hub.manager import (
39
+ Hub,
40
+ clear_hub,
41
+ get_hub,
42
+ )
43
+
44
+
45
+ # CLI features (require click) - lazy loaded
46
+ def get_click_commands():
47
+ """
48
+ Get CLI command building functions.
49
+
50
+ Returns:
51
+ Module with click command building functionality.
52
+
53
+ Raises:
54
+ ImportError: If click is not available.
55
+ """
56
+ try:
57
+ from provide.foundation.hub.commands import build_click_command
58
+
59
+ return {"build_click_command": build_click_command}
60
+ except ImportError as e:
61
+ if "click" in str(e):
62
+ raise ImportError(
63
+ "CLI command building requires optional dependencies. Install with: "
64
+ "pip install 'provide-foundation[cli]'"
65
+ ) from e
66
+ raise
67
+
68
+
69
+ def __getattr__(name: str):
70
+ """Support lazy loading of CLI-dependent features."""
71
+ if name == "build_click_command":
72
+ return get_click_commands()["build_click_command"]
73
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
74
+
75
+
76
+ __all__ = [
77
+ # Components
78
+ "get_component_registry",
79
+ "ComponentCategory",
80
+ # Hub
81
+ "Hub",
82
+ "clear_hub",
83
+ "get_hub",
84
+ # Commands (core)
85
+ "register_command",
86
+ # CLI features (lazy loaded)
87
+ "get_click_commands",
88
+ ]