dazzlelink 0.8.0__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.
- dazzlelink/__init__.py +306 -0
- dazzlelink/__main__.py +10 -0
- dazzlelink/_version.py +83 -0
- dazzlelink/cli.py +649 -0
- dazzlelink/config.py +143 -0
- dazzlelink/data.py +390 -0
- dazzlelink/exceptions.py +7 -0
- dazzlelink/operations/__init__.py +71 -0
- dazzlelink/operations/batch.py +1100 -0
- dazzlelink/operations/core.py +519 -0
- dazzlelink/operations/links.py +618 -0
- dazzlelink/operations/recreate.py +307 -0
- dazzlelink/operations/timestamps.py +665 -0
- dazzlelink/path.py +188 -0
- dazzlelink-0.8.0.dist-info/METADATA +316 -0
- dazzlelink-0.8.0.dist-info/RECORD +20 -0
- dazzlelink-0.8.0.dist-info/WHEEL +5 -0
- dazzlelink-0.8.0.dist-info/entry_points.txt +2 -0
- dazzlelink-0.8.0.dist-info/licenses/LICENSE +674 -0
- dazzlelink-0.8.0.dist-info/top_level.txt +1 -0
dazzlelink/__init__.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dazzlelink - Symbolic Link Preservation Tool
|
|
3
|
+
|
|
4
|
+
A tool for exporting, importing, and managing symbolic links across
|
|
5
|
+
different systems, particularly useful for network shares and cross-platform
|
|
6
|
+
environments where native symlinks might not be properly supported.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ._version import __version__, __app_name__
|
|
15
|
+
|
|
16
|
+
# Set up package-level logger
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
logger.setLevel(logging.INFO)
|
|
19
|
+
|
|
20
|
+
# Create console handler if not already present
|
|
21
|
+
if not logger.handlers:
|
|
22
|
+
console_handler = logging.StreamHandler()
|
|
23
|
+
console_handler.setFormatter(
|
|
24
|
+
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
25
|
+
)
|
|
26
|
+
logger.addHandler(console_handler)
|
|
27
|
+
|
|
28
|
+
# Import core functionality
|
|
29
|
+
from .exceptions import DazzleLinkException
|
|
30
|
+
from .data import DazzleLinkData
|
|
31
|
+
from .config import DazzleLinkConfig
|
|
32
|
+
from .path import (
|
|
33
|
+
UNCAdapter,
|
|
34
|
+
get_unc_adapter,
|
|
35
|
+
convert_to_drive,
|
|
36
|
+
convert_to_unc,
|
|
37
|
+
normalize_path,
|
|
38
|
+
refresh_mappings
|
|
39
|
+
)
|
|
40
|
+
from .operations import (
|
|
41
|
+
DazzleLink,
|
|
42
|
+
create_windows_symlink,
|
|
43
|
+
restore_file_attributes,
|
|
44
|
+
scan_directory,
|
|
45
|
+
find_dazzlelinks,
|
|
46
|
+
batch_import,
|
|
47
|
+
convert_directory,
|
|
48
|
+
mirror_directory,
|
|
49
|
+
batch_copy,
|
|
50
|
+
update_config_batch,
|
|
51
|
+
check_links,
|
|
52
|
+
rebase_links,
|
|
53
|
+
rebase_dazzlelinks,
|
|
54
|
+
recreate_link,
|
|
55
|
+
execute_dazzlelink
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Create a global instance for convenience
|
|
59
|
+
_dazzlelink_instance = None
|
|
60
|
+
|
|
61
|
+
def get_dazzlelink_instance():
|
|
62
|
+
"""Get or create the global DazzleLink instance."""
|
|
63
|
+
global _dazzlelink_instance
|
|
64
|
+
if _dazzlelink_instance is None:
|
|
65
|
+
_dazzlelink_instance = DazzleLink()
|
|
66
|
+
return _dazzlelink_instance
|
|
67
|
+
|
|
68
|
+
# Convenience functions that use the global instance
|
|
69
|
+
def export_link(link_path, output_path=None, make_executable=None, mode=None):
|
|
70
|
+
"""
|
|
71
|
+
Export a symlink to a .dazzlelink file
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
link_path: Path to the symlink
|
|
75
|
+
output_path: Output path for the dazzlelink file
|
|
76
|
+
make_executable: Whether to make the dazzlelink executable
|
|
77
|
+
mode: Default execution mode for this dazzlelink
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Path to the created dazzlelink file
|
|
81
|
+
"""
|
|
82
|
+
dl = get_dazzlelink_instance()
|
|
83
|
+
return dl.serialize_link(link_path, output_path, make_executable, mode)
|
|
84
|
+
|
|
85
|
+
def import_link(dazzlelink_path, target_location=None, timestamp_strategy='current',
|
|
86
|
+
update_dazzlelink=False, use_live_target=False):
|
|
87
|
+
"""
|
|
88
|
+
Import (recreate) a symlink from a .dazzlelink file
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
dazzlelink_path: Path to the dazzlelink file
|
|
92
|
+
target_location: Override location for the recreated symlink
|
|
93
|
+
timestamp_strategy: Strategy for setting timestamps
|
|
94
|
+
update_dazzlelink: Whether to update dazzlelink metadata
|
|
95
|
+
use_live_target: Whether to check live target for timestamps
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Path to the created symlink
|
|
99
|
+
"""
|
|
100
|
+
return recreate_link(
|
|
101
|
+
dazzlelink_path,
|
|
102
|
+
target_location,
|
|
103
|
+
timestamp_strategy,
|
|
104
|
+
update_dazzlelink,
|
|
105
|
+
use_live_target
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def create_link(target, link_name, make_executable=None, mode=None):
|
|
109
|
+
"""
|
|
110
|
+
Create a new dazzlelink pointing to a target
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
target: The file or directory to link to
|
|
114
|
+
link_name: The path for the new dazzlelink
|
|
115
|
+
make_executable: Whether to make the dazzlelink executable
|
|
116
|
+
mode: Default execution mode for this dazzlelink
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Path to the created dazzlelink file
|
|
120
|
+
"""
|
|
121
|
+
dl = get_dazzlelink_instance()
|
|
122
|
+
return dl.serialize_link(
|
|
123
|
+
target,
|
|
124
|
+
output_path=link_name,
|
|
125
|
+
make_executable=make_executable,
|
|
126
|
+
mode=mode,
|
|
127
|
+
require_symlink=False
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def convert(directory, recursive=True, keep_originals=True,
|
|
131
|
+
make_executable=None, mode=None, config=None):
|
|
132
|
+
"""
|
|
133
|
+
Convert symlinks in a directory to dazzlelinks
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
directory: Directory to scan
|
|
137
|
+
recursive: Whether to scan recursively
|
|
138
|
+
keep_originals: Whether to keep original symlinks
|
|
139
|
+
make_executable: Whether to make the dazzlelinks executable
|
|
140
|
+
(None uses config default)
|
|
141
|
+
mode: Default execution mode for the dazzlelinks (None uses config default)
|
|
142
|
+
config: Configuration object to use (None creates a fresh one).
|
|
143
|
+
Pass the CLI's config so --executable/--mode are honored.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of created dazzlelink paths
|
|
147
|
+
"""
|
|
148
|
+
return convert_directory(
|
|
149
|
+
directory,
|
|
150
|
+
recursive=recursive,
|
|
151
|
+
keep_originals=keep_originals,
|
|
152
|
+
make_executable=make_executable,
|
|
153
|
+
mode=mode,
|
|
154
|
+
config=config
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def mirror(src_dir, dest_dir, recursive=True,
|
|
158
|
+
make_executable=None, mode=None, config=None):
|
|
159
|
+
"""
|
|
160
|
+
Mirror a directory structure with dazzlelinks
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
src_dir: Source directory
|
|
164
|
+
dest_dir: Destination directory
|
|
165
|
+
recursive: Whether to scan recursively
|
|
166
|
+
make_executable: Whether to make the dazzlelinks executable
|
|
167
|
+
(None uses config default)
|
|
168
|
+
mode: Default execution mode for the dazzlelinks (None uses config default)
|
|
169
|
+
config: Configuration object to use (None creates a fresh one).
|
|
170
|
+
Pass the CLI's config so --executable/--mode are honored.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of created dazzlelink paths
|
|
174
|
+
"""
|
|
175
|
+
return mirror_directory(
|
|
176
|
+
src_dir, dest_dir, recursive=recursive,
|
|
177
|
+
make_executable=make_executable, mode=mode, config=config
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def execute(dazzlelink_path, mode=None, config_override=None):
|
|
181
|
+
"""
|
|
182
|
+
Execute/open a dazzlelink
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
dazzlelink_path: Path to the dazzlelink
|
|
186
|
+
mode: Override execution mode
|
|
187
|
+
config_override: Configuration object whose default_mode is used as a
|
|
188
|
+
last-resort fallback (after the CLI mode and the file's embedded mode)
|
|
189
|
+
"""
|
|
190
|
+
return execute_dazzlelink(dazzlelink_path, mode, config_override=config_override)
|
|
191
|
+
|
|
192
|
+
def scan(directory, recursive=True):
|
|
193
|
+
"""
|
|
194
|
+
Scan a directory for symlinks
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
directory: Directory to scan
|
|
198
|
+
recursive: Whether to scan recursively
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of symlink paths
|
|
202
|
+
"""
|
|
203
|
+
return scan_directory(directory, recursive)
|
|
204
|
+
|
|
205
|
+
def check(directory, recursive=True, fix=False):
|
|
206
|
+
"""
|
|
207
|
+
Check symlinks in a directory and report broken ones
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
directory: Directory to scan
|
|
211
|
+
recursive: Whether to scan recursively
|
|
212
|
+
fix: Try to fix broken links when possible
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary with status of links
|
|
216
|
+
"""
|
|
217
|
+
return check_links(directory, recursive, not fix, fix)
|
|
218
|
+
|
|
219
|
+
def rebase(directory, recursive=True, make_relative=None, target_base=None, only_broken=False):
|
|
220
|
+
"""
|
|
221
|
+
Rebase links in a directory
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
directory: Directory containing links to rebase
|
|
225
|
+
recursive: Whether to scan recursively
|
|
226
|
+
make_relative: Convert to relative paths if True, absolute if False
|
|
227
|
+
target_base: Replace base part of absolute paths
|
|
228
|
+
only_broken: Only rebase broken links
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary with status of links
|
|
232
|
+
"""
|
|
233
|
+
return rebase_links(directory, recursive, make_relative, target_base, only_broken)
|
|
234
|
+
|
|
235
|
+
def configure_logging(level=logging.INFO, log_file=None):
|
|
236
|
+
"""
|
|
237
|
+
Configure logging for dazzlelink
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
level: Logging level (default: INFO)
|
|
241
|
+
log_file: Path to log file (if None, only console logging is used)
|
|
242
|
+
"""
|
|
243
|
+
logger.setLevel(level)
|
|
244
|
+
|
|
245
|
+
# Clear existing handlers
|
|
246
|
+
for handler in logger.handlers[:]:
|
|
247
|
+
logger.removeHandler(handler)
|
|
248
|
+
|
|
249
|
+
# Add console handler
|
|
250
|
+
console_handler = logging.StreamHandler()
|
|
251
|
+
console_handler.setFormatter(
|
|
252
|
+
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
253
|
+
)
|
|
254
|
+
logger.addHandler(console_handler)
|
|
255
|
+
|
|
256
|
+
# Add file handler if specified
|
|
257
|
+
if log_file:
|
|
258
|
+
file_handler = logging.FileHandler(log_file)
|
|
259
|
+
file_handler.setFormatter(
|
|
260
|
+
logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
261
|
+
)
|
|
262
|
+
logger.addHandler(file_handler)
|
|
263
|
+
|
|
264
|
+
def enable_verbose_logging():
|
|
265
|
+
"""
|
|
266
|
+
Enable verbose (debug) logging
|
|
267
|
+
"""
|
|
268
|
+
configure_logging(logging.DEBUG)
|
|
269
|
+
# Also set environment variable for modules that check it directly
|
|
270
|
+
os.environ['DAZZLELINK_VERBOSE'] = '1'
|
|
271
|
+
|
|
272
|
+
# Export public API
|
|
273
|
+
__all__ = [
|
|
274
|
+
# Classes
|
|
275
|
+
'DazzleLinkException',
|
|
276
|
+
'DazzleLinkData',
|
|
277
|
+
'DazzleLinkConfig',
|
|
278
|
+
'DazzleLink',
|
|
279
|
+
'UNCAdapter',
|
|
280
|
+
|
|
281
|
+
# Path functions
|
|
282
|
+
'get_unc_adapter',
|
|
283
|
+
'convert_to_drive',
|
|
284
|
+
'convert_to_unc',
|
|
285
|
+
'normalize_path',
|
|
286
|
+
'refresh_mappings',
|
|
287
|
+
|
|
288
|
+
# High-level functions
|
|
289
|
+
'export_link',
|
|
290
|
+
'import_link',
|
|
291
|
+
'create_link',
|
|
292
|
+
'convert',
|
|
293
|
+
'mirror',
|
|
294
|
+
'execute',
|
|
295
|
+
'scan',
|
|
296
|
+
'check',
|
|
297
|
+
'rebase',
|
|
298
|
+
|
|
299
|
+
# Utility functions
|
|
300
|
+
'get_dazzlelink_instance',
|
|
301
|
+
'configure_logging',
|
|
302
|
+
'enable_verbose_logging',
|
|
303
|
+
|
|
304
|
+
# Version
|
|
305
|
+
'__version__'
|
|
306
|
+
]
|
dazzlelink/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Allow running as: python -m dazzlelink"""
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from .cli import main
|
|
5
|
+
|
|
6
|
+
if __name__ == "__main__":
|
|
7
|
+
# Propagate main()'s return code as the process exit status so errors are
|
|
8
|
+
# detectable by scripts/CI (without this, `python -m dazzlelink` always
|
|
9
|
+
# exited 0, even on failure).
|
|
10
|
+
sys.exit(main())
|
dazzlelink/_version.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version information for dazzlelink.
|
|
3
|
+
|
|
4
|
+
This file is the canonical source for version numbers.
|
|
5
|
+
The __version__ string is automatically updated by git hooks
|
|
6
|
+
with build metadata (branch, build number, date, commit hash).
|
|
7
|
+
|
|
8
|
+
Format: MAJOR.MINOR.PATCH[-PHASE]_BRANCH_BUILD-YYYYMMDD-COMMITHASH
|
|
9
|
+
Example: 0.7.0_main_1-20260331-a1b2c3d4
|
|
10
|
+
|
|
11
|
+
To sync versions: python scripts/repokit-common/sync-versions.py
|
|
12
|
+
To bump version: python scripts/repokit-common/sync-versions.py --bump patch
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Version components - edit these for version bumps
|
|
16
|
+
MAJOR = 0
|
|
17
|
+
MINOR = 8
|
|
18
|
+
PATCH = 0
|
|
19
|
+
PHASE = "" # Per-MINOR feature set: None, "alpha", "beta", "rc1", etc.
|
|
20
|
+
PRE_RELEASE_NUM = 1 # PEP 440 pre-release number (e.g., a1, b2)
|
|
21
|
+
PROJECT_PHASE = "alpha" # Project-wide: "prealpha", "alpha", "beta", "stable"
|
|
22
|
+
|
|
23
|
+
# Auto-updated by git hooks - do not edit manually
|
|
24
|
+
__version__ = "0.8.0_main_45-20260612-55c76b11"
|
|
25
|
+
__app_name__ = "dazzlelink"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_version():
|
|
29
|
+
"""Return the full version string including branch and build info."""
|
|
30
|
+
return __version__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_display_version():
|
|
34
|
+
"""Return a human-friendly version string with project phase."""
|
|
35
|
+
base = get_base_version()
|
|
36
|
+
if PROJECT_PHASE and PROJECT_PHASE != "stable":
|
|
37
|
+
return f"{PROJECT_PHASE.upper()} {base}"
|
|
38
|
+
return base
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_base_version():
|
|
42
|
+
"""Return the semantic version string (MAJOR.MINOR.PATCH[-PHASE])."""
|
|
43
|
+
if "_" in __version__:
|
|
44
|
+
return __version__.split("_")[0]
|
|
45
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
46
|
+
if PHASE:
|
|
47
|
+
base = f"{base}-{PHASE}"
|
|
48
|
+
return base
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_pip_version():
|
|
52
|
+
"""
|
|
53
|
+
Return PEP 440 compliant version for pip/setuptools.
|
|
54
|
+
|
|
55
|
+
Converts our version format to PEP 440:
|
|
56
|
+
- Main branch: 0.7.0_main_6-20260331-hash -> 0.7.0
|
|
57
|
+
- Dev branch: 0.7.0_dev_6-20260331-hash -> 0.7.0.dev6
|
|
58
|
+
"""
|
|
59
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
60
|
+
|
|
61
|
+
phase_map = {"alpha": f"a{PRE_RELEASE_NUM}", "beta": f"b{PRE_RELEASE_NUM}"}
|
|
62
|
+
if PHASE:
|
|
63
|
+
base += phase_map.get(PHASE, PHASE)
|
|
64
|
+
|
|
65
|
+
if "_" not in __version__:
|
|
66
|
+
return base
|
|
67
|
+
|
|
68
|
+
parts = __version__.split("_")
|
|
69
|
+
branch = parts[1] if len(parts) > 1 else "unknown"
|
|
70
|
+
|
|
71
|
+
if branch == "main":
|
|
72
|
+
return base
|
|
73
|
+
else:
|
|
74
|
+
build_info = "_".join(parts[2:]) if len(parts) > 2 else ""
|
|
75
|
+
build_num = build_info.split("-")[0] if "-" in build_info else "0"
|
|
76
|
+
return f"{base}.dev{build_num}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# For convenience in imports
|
|
80
|
+
VERSION = get_version()
|
|
81
|
+
BASE_VERSION = get_base_version()
|
|
82
|
+
PIP_VERSION = get_pip_version()
|
|
83
|
+
DISPLAY_VERSION = get_display_version()
|