provide-foundation 0.0.0.dev2__py3-none-any.whl → 0.0.0.dev3__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 (155) hide show
  1. provide/foundation/__init__.py +20 -20
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +90 -91
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +4 -4
  13. provide/foundation/cli/commands/logs/__init__.py +2 -2
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +3 -3
  16. provide/foundation/cli/commands/logs/send.py +2 -2
  17. provide/foundation/cli/commands/logs/tail.py +2 -2
  18. provide/foundation/cli/decorators.py +0 -1
  19. provide/foundation/cli/testing.py +0 -5
  20. provide/foundation/cli/utils.py +1 -2
  21. provide/foundation/config/__init__.py +19 -19
  22. provide/foundation/config/base.py +2 -2
  23. provide/foundation/config/converters.py +81 -83
  24. provide/foundation/config/defaults.py +1 -1
  25. provide/foundation/config/env.py +2 -1
  26. provide/foundation/config/loader.py +1 -1
  27. provide/foundation/config/sync.py +8 -6
  28. provide/foundation/config/types.py +5 -5
  29. provide/foundation/config/validators.py +4 -4
  30. provide/foundation/console/output.py +7 -7
  31. provide/foundation/context/core.py +19 -17
  32. provide/foundation/crypto/certificates/__init__.py +9 -5
  33. provide/foundation/crypto/certificates/base.py +2 -2
  34. provide/foundation/crypto/certificates/certificate.py +48 -19
  35. provide/foundation/crypto/certificates/factory.py +26 -18
  36. provide/foundation/crypto/certificates/generator.py +24 -23
  37. provide/foundation/crypto/certificates/loader.py +24 -16
  38. provide/foundation/crypto/certificates/operations.py +17 -10
  39. provide/foundation/crypto/certificates/trust.py +21 -21
  40. provide/foundation/env/__init__.py +28 -0
  41. provide/foundation/env/core.py +218 -0
  42. provide/foundation/errors/__init__.py +3 -2
  43. provide/foundation/errors/decorators.py +0 -3
  44. provide/foundation/errors/types.py +0 -1
  45. provide/foundation/eventsets/display.py +13 -14
  46. provide/foundation/eventsets/registry.py +61 -31
  47. provide/foundation/eventsets/resolver.py +50 -46
  48. provide/foundation/eventsets/sets/das.py +8 -8
  49. provide/foundation/eventsets/sets/database.py +14 -14
  50. provide/foundation/eventsets/sets/http.py +21 -21
  51. provide/foundation/eventsets/sets/llm.py +16 -16
  52. provide/foundation/eventsets/sets/task_queue.py +13 -13
  53. provide/foundation/eventsets/types.py +7 -7
  54. provide/foundation/file/directory.py +1 -1
  55. provide/foundation/file/lock.py +2 -3
  56. provide/foundation/hub/components.py +19 -21
  57. provide/foundation/hub/config.py +25 -19
  58. provide/foundation/hub/discovery.py +5 -4
  59. provide/foundation/hub/handlers.py +13 -5
  60. provide/foundation/hub/lifecycle.py +10 -9
  61. provide/foundation/hub/manager.py +3 -0
  62. provide/foundation/hub/processors.py +8 -3
  63. provide/foundation/integrations/__init__.py +1 -1
  64. provide/foundation/integrations/openobserve/client.py +2 -2
  65. provide/foundation/integrations/openobserve/commands.py +9 -9
  66. provide/foundation/integrations/openobserve/config.py +2 -2
  67. provide/foundation/integrations/openobserve/otlp.py +2 -2
  68. provide/foundation/integrations/openobserve/search.py +1 -2
  69. provide/foundation/integrations/openobserve/streaming.py +1 -1
  70. provide/foundation/logger/__init__.py +0 -1
  71. provide/foundation/logger/config/base.py +1 -1
  72. provide/foundation/logger/config/logging.py +19 -19
  73. provide/foundation/logger/config/telemetry.py +11 -13
  74. provide/foundation/logger/factories.py +2 -2
  75. provide/foundation/logger/processors/main.py +12 -10
  76. provide/foundation/logger/ratelimit/limiters.py +4 -4
  77. provide/foundation/logger/ratelimit/processor.py +1 -1
  78. provide/foundation/logger/setup/coordinator.py +38 -24
  79. provide/foundation/logger/setup/processors.py +3 -3
  80. provide/foundation/logger/setup/testing.py +14 -0
  81. provide/foundation/logger/trace.py +5 -5
  82. provide/foundation/metrics/__init__.py +1 -1
  83. provide/foundation/metrics/otel.py +3 -1
  84. provide/foundation/observability/__init__.py +1 -1
  85. provide/foundation/process/__init__.py +1 -1
  86. provide/foundation/process/exit.py +6 -5
  87. provide/foundation/process/lifecycle.py +41 -18
  88. provide/foundation/resilience/__init__.py +6 -5
  89. provide/foundation/resilience/circuit.py +32 -30
  90. provide/foundation/resilience/decorators.py +58 -42
  91. provide/foundation/resilience/fallback.py +55 -40
  92. provide/foundation/resilience/retry.py +67 -65
  93. provide/foundation/serialization/__init__.py +16 -0
  94. provide/foundation/serialization/core.py +70 -0
  95. provide/foundation/streams/config.py +8 -9
  96. provide/foundation/streams/console.py +3 -3
  97. provide/foundation/streams/core.py +2 -2
  98. provide/foundation/streams/file.py +1 -1
  99. provide/foundation/testing/__init__.py +22 -7
  100. provide/foundation/testing/archive/__init__.py +7 -7
  101. provide/foundation/testing/archive/fixtures.py +58 -54
  102. provide/foundation/testing/cli.py +3 -6
  103. provide/foundation/testing/common/__init__.py +13 -13
  104. provide/foundation/testing/common/fixtures.py +27 -30
  105. provide/foundation/testing/file/__init__.py +15 -15
  106. provide/foundation/testing/file/content_fixtures.py +65 -92
  107. provide/foundation/testing/file/directory_fixtures.py +19 -19
  108. provide/foundation/testing/file/fixtures.py +14 -17
  109. provide/foundation/testing/file/special_fixtures.py +34 -42
  110. provide/foundation/testing/logger.py +28 -23
  111. provide/foundation/testing/mocking/__init__.py +21 -21
  112. provide/foundation/testing/mocking/fixtures.py +80 -67
  113. provide/foundation/testing/process/__init__.py +23 -23
  114. provide/foundation/testing/process/async_fixtures.py +89 -80
  115. provide/foundation/testing/process/fixtures.py +11 -13
  116. provide/foundation/testing/process/subprocess_fixtures.py +41 -40
  117. provide/foundation/testing/threading/__init__.py +17 -17
  118. provide/foundation/testing/threading/basic_fixtures.py +21 -17
  119. provide/foundation/testing/threading/data_fixtures.py +18 -16
  120. provide/foundation/testing/threading/execution_fixtures.py +67 -52
  121. provide/foundation/testing/threading/fixtures.py +10 -14
  122. provide/foundation/testing/threading/sync_fixtures.py +21 -18
  123. provide/foundation/testing/time/__init__.py +11 -11
  124. provide/foundation/testing/time/fixtures.py +91 -79
  125. provide/foundation/testing/transport/__init__.py +9 -9
  126. provide/foundation/testing/transport/fixtures.py +54 -54
  127. provide/foundation/time/__init__.py +18 -0
  128. provide/foundation/time/core.py +63 -0
  129. provide/foundation/tools/__init__.py +2 -2
  130. provide/foundation/tools/base.py +68 -67
  131. provide/foundation/tools/cache.py +62 -69
  132. provide/foundation/tools/downloader.py +51 -56
  133. provide/foundation/tools/installer.py +51 -57
  134. provide/foundation/tools/registry.py +38 -45
  135. provide/foundation/tools/resolver.py +70 -68
  136. provide/foundation/tools/verifier.py +39 -50
  137. provide/foundation/tracer/spans.py +1 -13
  138. provide/foundation/transport/__init__.py +26 -33
  139. provide/foundation/transport/base.py +32 -30
  140. provide/foundation/transport/client.py +44 -49
  141. provide/foundation/transport/config.py +11 -13
  142. provide/foundation/transport/errors.py +13 -27
  143. provide/foundation/transport/http.py +69 -55
  144. provide/foundation/transport/middleware.py +86 -81
  145. provide/foundation/transport/registry.py +29 -27
  146. provide/foundation/transport/types.py +6 -6
  147. provide/foundation/utils/deps.py +3 -2
  148. provide/foundation/utils/parsing.py +7 -7
  149. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  150. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  151. provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
  152. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  153. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  154. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  155. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -5,14 +5,14 @@ Provides capabilities for downloading tools with progress tracking,
5
5
  parallel downloads, and mirror support.
6
6
  """
7
7
 
8
- import hashlib
9
8
  from concurrent.futures import ThreadPoolExecutor
9
+ import hashlib
10
10
  from pathlib import Path
11
11
  from typing import Callable
12
12
 
13
13
  from provide.foundation.errors import FoundationError
14
14
  from provide.foundation.logger import get_logger
15
- from provide.foundation.resilience import retry, fallback
15
+ from provide.foundation.resilience import retry
16
16
  from provide.foundation.transport import UniversalClient
17
17
 
18
18
  log = get_logger(__name__)
@@ -20,48 +20,48 @@ log = get_logger(__name__)
20
20
 
21
21
  class DownloadError(FoundationError):
22
22
  """Raised when download fails."""
23
-
23
+
24
24
  pass
25
25
 
26
26
 
27
27
  class ToolDownloader:
28
28
  """
29
29
  Advanced download capabilities for tools.
30
-
30
+
31
31
  Features:
32
32
  - Progress reporting with callbacks
33
33
  - Parallel downloads for multiple files
34
34
  - Mirror fallback support
35
35
  - Checksum verification
36
-
36
+
37
37
  Attributes:
38
38
  client: Transport client for HTTP requests.
39
39
  progress_callbacks: List of progress callback functions.
40
40
  """
41
-
41
+
42
42
  def __init__(self, client: UniversalClient):
43
43
  """
44
44
  Initialize the downloader.
45
-
45
+
46
46
  Args:
47
47
  client: Universal client for making HTTP requests.
48
48
  """
49
49
  self.client = client
50
50
  self.progress_callbacks: list[Callable[[int, int], None]] = []
51
-
51
+
52
52
  def add_progress_callback(self, callback: Callable[[int, int], None]) -> None:
53
53
  """
54
54
  Add a progress callback.
55
-
55
+
56
56
  Args:
57
57
  callback: Function that receives (downloaded_bytes, total_bytes).
58
58
  """
59
59
  self.progress_callbacks.append(callback)
60
-
60
+
61
61
  def _report_progress(self, downloaded: int, total: int) -> None:
62
62
  """
63
63
  Report progress to all callbacks.
64
-
64
+
65
65
  Args:
66
66
  downloaded: Bytes downloaded so far.
67
67
  total: Total bytes to download (0 if unknown).
@@ -71,101 +71,95 @@ class ToolDownloader:
71
71
  callback(downloaded, total)
72
72
  except Exception as e:
73
73
  log.warning(f"Progress callback failed: {e}")
74
-
74
+
75
75
  @retry(max_attempts=3, base_delay=1.0)
76
76
  def download_with_progress(
77
- self,
78
- url: str,
79
- dest: Path,
80
- checksum: str | None = None
77
+ self, url: str, dest: Path, checksum: str | None = None
81
78
  ) -> Path:
82
79
  """
83
80
  Download a file with progress reporting.
84
-
81
+
85
82
  Args:
86
83
  url: URL to download from.
87
84
  dest: Destination file path.
88
85
  checksum: Optional checksum for verification.
89
-
86
+
90
87
  Returns:
91
88
  Path to the downloaded file.
92
-
89
+
93
90
  Raises:
94
91
  DownloadError: If download or verification fails.
95
92
  """
96
93
  log.debug(f"Downloading {url} to {dest}")
97
-
94
+
98
95
  # Ensure parent directory exists
99
96
  dest.parent.mkdir(parents=True, exist_ok=True)
100
-
97
+
101
98
  # Stream download with progress
102
99
  with self.client.stream("GET", url) as response:
103
100
  # Get total size if available
104
101
  total_size = int(response.headers.get("content-length", 0))
105
102
  downloaded = 0
106
-
103
+
107
104
  # Write to file and report progress
108
105
  with dest.open("wb") as f:
109
106
  for chunk in response.iter_bytes(8192):
110
107
  f.write(chunk)
111
108
  downloaded += len(chunk)
112
109
  self._report_progress(downloaded, total_size)
113
-
110
+
114
111
  # Verify checksum if provided
115
112
  if checksum:
116
113
  if not self.verify_checksum(dest, checksum):
117
114
  dest.unlink()
118
115
  raise DownloadError(f"Checksum mismatch for {url}")
119
-
116
+
120
117
  log.info(f"Downloaded {url} successfully")
121
118
  return dest
122
-
119
+
123
120
  def verify_checksum(self, file_path: Path, expected: str) -> bool:
124
121
  """
125
122
  Verify file checksum.
126
-
123
+
127
124
  Args:
128
125
  file_path: Path to file to verify.
129
126
  expected: Expected checksum (hex string).
130
-
127
+
131
128
  Returns:
132
129
  True if checksum matches, False otherwise.
133
130
  """
134
131
  # Default to SHA256
135
132
  hasher = hashlib.sha256()
136
-
133
+
137
134
  with file_path.open("rb") as f:
138
135
  for chunk in iter(lambda: f.read(8192), b""):
139
136
  hasher.update(chunk)
140
-
137
+
141
138
  actual = hasher.hexdigest()
142
139
  return actual == expected
143
-
144
- def download_parallel(
145
- self,
146
- urls: list[tuple[str, Path]]
147
- ) -> list[Path]:
140
+
141
+ def download_parallel(self, urls: list[tuple[str, Path]]) -> list[Path]:
148
142
  """
149
143
  Download multiple files in parallel.
150
-
144
+
151
145
  Args:
152
146
  urls: List of (url, destination) tuples.
153
-
147
+
154
148
  Returns:
155
149
  List of downloaded file paths in the same order as input.
156
-
150
+
157
151
  Raises:
158
152
  DownloadError: If any download fails.
159
153
  """
160
154
  errors = []
161
-
155
+
162
156
  with ThreadPoolExecutor(max_workers=4) as executor:
163
157
  # Submit all downloads, maintaining order with index
164
158
  futures = [
165
159
  executor.submit(self.download_with_progress, url, dest)
166
160
  for url, dest in urls
167
161
  ]
168
-
162
+
169
163
  # Collect results in order
170
164
  results = []
171
165
  for i, future in enumerate(futures):
@@ -176,49 +170,50 @@ class ToolDownloader:
176
170
  except Exception as e:
177
171
  errors.append((url, e))
178
172
  log.error(f"Failed to download {url}: {e}")
179
-
173
+
180
174
  if errors:
181
175
  raise DownloadError(f"Some downloads failed: {errors}")
182
-
176
+
183
177
  return results
184
-
185
- def download_with_mirrors(
186
- self,
187
- mirrors: list[str],
188
- dest: Path
189
- ) -> Path:
178
+
179
+ def download_with_mirrors(self, mirrors: list[str], dest: Path) -> Path:
190
180
  """
191
181
  Try multiple mirrors until one succeeds using fallback pattern.
192
-
182
+
193
183
  Args:
194
184
  mirrors: List of mirror URLs to try.
195
185
  dest: Destination file path.
196
-
186
+
197
187
  Returns:
198
188
  Path to downloaded file.
199
-
189
+
200
190
  Raises:
201
191
  DownloadError: If all mirrors fail.
202
192
  """
203
193
  from provide.foundation.resilience.fallback import FallbackChain
204
-
194
+
205
195
  if not mirrors:
206
196
  raise DownloadError("No mirrors provided")
207
-
197
+
208
198
  # Create fallback functions for each mirror
209
199
  fallback_funcs = []
210
200
  for mirror_url in mirrors:
201
+
211
202
  def create_mirror_func(url):
212
203
  def mirror_download():
213
204
  log.debug(f"Trying mirror: {url}")
214
205
  return self.download_with_progress(url, dest)
206
+
215
207
  return mirror_download
208
+
216
209
  fallback_funcs.append(create_mirror_func(mirror_url))
217
-
210
+
218
211
  # Use FallbackChain to try mirrors in order
219
- chain = FallbackChain(fallbacks=fallback_funcs[1:]) # All but first are fallbacks
220
-
212
+ chain = FallbackChain(
213
+ fallbacks=fallback_funcs[1:]
214
+ ) # All but first are fallbacks
215
+
221
216
  try:
222
217
  return chain.execute(fallback_funcs[0]) # First is primary
223
218
  except Exception as e:
224
- raise DownloadError(f"All mirrors failed: {e}")
219
+ raise DownloadError(f"All mirrors failed: {e}")
@@ -5,11 +5,10 @@ Handles extraction and installation of tools from different
5
5
  archive formats (zip, tar, gz, etc.) and binary files.
6
6
  """
7
7
 
8
+ from pathlib import Path
8
9
  import shutil
9
10
  import tarfile
10
- import tempfile
11
11
  import zipfile
12
- from pathlib import Path
13
12
 
14
13
  from provide.foundation.errors import FoundationError
15
14
  from provide.foundation.logger import get_logger
@@ -20,43 +19,43 @@ log = get_logger(__name__)
20
19
 
21
20
  class InstallError(FoundationError):
22
21
  """Raised when installation fails."""
23
-
22
+
24
23
  pass
25
24
 
26
25
 
27
26
  class ToolInstaller:
28
27
  """
29
28
  Handle tool installation from various artifact formats.
30
-
29
+
31
30
  Supports:
32
31
  - ZIP archives
33
32
  - TAR archives (with compression)
34
33
  - Single binary files
35
34
  - Platform-specific installation patterns
36
35
  """
37
-
36
+
38
37
  def install(self, artifact: Path, metadata: ToolMetadata) -> Path:
39
38
  """
40
39
  Install tool from artifact.
41
-
40
+
42
41
  Args:
43
42
  artifact: Path to downloaded artifact.
44
43
  metadata: Tool metadata with installation info.
45
-
44
+
46
45
  Returns:
47
46
  Path to installed tool directory.
48
-
47
+
49
48
  Raises:
50
49
  InstallError: If installation fails.
51
50
  """
52
51
  if not artifact.exists():
53
52
  raise InstallError(f"Artifact not found: {artifact}")
54
-
53
+
55
54
  # Determine install directory
56
55
  install_dir = self.get_install_dir(metadata)
57
-
56
+
58
57
  log.info(f"Installing {metadata.name} {metadata.version} to {install_dir}")
59
-
58
+
60
59
  # Extract based on file type
61
60
  suffix = artifact.suffix.lower()
62
61
  if suffix == ".zip":
@@ -67,65 +66,65 @@ class ToolInstaller:
67
66
  self.install_binary(artifact, install_dir, metadata)
68
67
  else:
69
68
  raise InstallError(f"Unknown artifact type: {suffix}")
70
-
69
+
71
70
  # Set permissions
72
71
  self.set_permissions(install_dir, metadata)
73
-
72
+
74
73
  # Create symlinks if needed
75
74
  self.create_symlinks(install_dir, metadata)
76
-
75
+
77
76
  log.info(f"Successfully installed {metadata.name} to {install_dir}")
78
77
  return install_dir
79
-
78
+
80
79
  def get_install_dir(self, metadata: ToolMetadata) -> Path:
81
80
  """
82
81
  Get installation directory for tool.
83
-
82
+
84
83
  Args:
85
84
  metadata: Tool metadata.
86
-
85
+
87
86
  Returns:
88
87
  Installation directory path.
89
88
  """
90
89
  if metadata.install_path:
91
90
  return metadata.install_path
92
-
91
+
93
92
  # Default to ~/.wrknv/tools/<name>/<version>
94
93
  base = Path.home() / ".wrknv" / "tools"
95
94
  return base / metadata.name / metadata.version
96
-
95
+
97
96
  def extract_zip(self, archive: Path, dest: Path) -> None:
98
97
  """
99
98
  Extract ZIP archive.
100
-
99
+
101
100
  Args:
102
101
  archive: Path to ZIP file.
103
102
  dest: Destination directory.
104
103
  """
105
104
  log.debug(f"Extracting ZIP {archive} to {dest}")
106
-
105
+
107
106
  dest.mkdir(parents=True, exist_ok=True)
108
-
107
+
109
108
  with zipfile.ZipFile(archive, "r") as zf:
110
109
  # Check for unsafe paths
111
110
  for member in zf.namelist():
112
111
  if member.startswith("/") or ".." in member:
113
112
  raise InstallError(f"Unsafe path in archive: {member}")
114
-
113
+
115
114
  zf.extractall(dest)
116
-
115
+
117
116
  def extract_tar(self, archive: Path, dest: Path) -> None:
118
117
  """
119
118
  Extract tar archive (with optional compression).
120
-
119
+
121
120
  Args:
122
121
  archive: Path to tar file.
123
122
  dest: Destination directory.
124
123
  """
125
124
  log.debug(f"Extracting tar {archive} to {dest}")
126
-
125
+
127
126
  dest.mkdir(parents=True, exist_ok=True)
128
-
127
+
129
128
  # Determine mode based on extension
130
129
  mode = "r"
131
130
  if archive.suffix in [".gz", ".tgz"]:
@@ -134,22 +133,22 @@ class ToolInstaller:
134
133
  mode = "r:bz2"
135
134
  elif archive.suffix == ".xz":
136
135
  mode = "r:xz"
137
-
136
+
138
137
  with tarfile.open(archive, mode) as tf:
139
138
  # Check for unsafe paths
140
139
  for member in tf.getmembers():
141
140
  if member.name.startswith("/") or ".." in member.name:
142
141
  raise InstallError(f"Unsafe path in archive: {member.name}")
143
-
142
+
144
143
  tf.extractall(dest)
145
-
144
+
146
145
  def is_binary(self, file_path: Path) -> bool:
147
146
  """
148
147
  Check if file is a binary executable.
149
-
148
+
150
149
  Args:
151
150
  file_path: Path to check.
152
-
151
+
153
152
  Returns:
154
153
  True if file appears to be binary.
155
154
  """
@@ -170,85 +169,80 @@ class ToolInstaller:
170
169
  return True
171
170
  except Exception:
172
171
  pass
173
-
172
+
174
173
  return False
175
-
176
- def install_binary(
177
- self,
178
- binary: Path,
179
- dest: Path,
180
- metadata: ToolMetadata
181
- ) -> None:
174
+
175
+ def install_binary(self, binary: Path, dest: Path, metadata: ToolMetadata) -> None:
182
176
  """
183
177
  Install single binary file.
184
-
178
+
185
179
  Args:
186
180
  binary: Path to binary file.
187
181
  dest: Destination directory.
188
182
  metadata: Tool metadata.
189
183
  """
190
184
  log.debug(f"Installing binary {binary} to {dest}")
191
-
185
+
192
186
  dest.mkdir(parents=True, exist_ok=True)
193
187
  bin_dir = dest / "bin"
194
188
  bin_dir.mkdir(exist_ok=True)
195
-
189
+
196
190
  # Determine target name
197
191
  target_name = metadata.executable_name or binary.name
198
192
  target = bin_dir / target_name
199
-
193
+
200
194
  # Copy binary
201
195
  shutil.copy2(binary, target)
202
-
196
+
203
197
  # Make executable
204
198
  target.chmod(0o755)
205
-
199
+
206
200
  def set_permissions(self, install_dir: Path, metadata: ToolMetadata) -> None:
207
201
  """
208
202
  Set appropriate permissions on installed files.
209
-
203
+
210
204
  Args:
211
205
  install_dir: Installation directory.
212
206
  metadata: Tool metadata.
213
207
  """
214
208
  import platform
215
-
209
+
216
210
  if platform.system() == "Windows":
217
211
  return # Windows handles permissions differently
218
-
212
+
219
213
  # Find executables and make them executable
220
214
  bin_dir = install_dir / "bin"
221
215
  if bin_dir.exists():
222
216
  for file in bin_dir.iterdir():
223
217
  if file.is_file():
224
218
  file.chmod(0o755)
225
-
219
+
226
220
  # Check for executable name in root
227
221
  if metadata.executable_name:
228
222
  exe_path = install_dir / metadata.executable_name
229
223
  if exe_path.exists():
230
224
  exe_path.chmod(0o755)
231
-
225
+
232
226
  def create_symlinks(self, install_dir: Path, metadata: ToolMetadata) -> None:
233
227
  """
234
228
  Create symlinks for easier access.
235
-
229
+
236
230
  Args:
237
231
  install_dir: Installation directory.
238
232
  metadata: Tool metadata.
239
233
  """
240
234
  import platform
241
-
235
+
242
236
  if platform.system() == "Windows":
243
237
  return # Windows doesn't support symlinks easily
244
-
238
+
245
239
  # Create version-less symlink
246
240
  if metadata.name and metadata.version:
247
241
  parent = install_dir.parent
248
242
  latest_link = parent / "latest"
249
-
243
+
250
244
  if latest_link.exists() or latest_link.is_symlink():
251
245
  latest_link.unlink()
252
-
246
+
253
247
  latest_link.symlink_to(install_dir)
254
- log.debug(f"Created symlink {latest_link} -> {install_dir}")
248
+ log.debug(f"Created symlink {latest_link} -> {install_dir}")