simstack 0.2.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.
- simstack/__init__.py +0 -0
- simstack/_version.py +34 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +447 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +167 -0
- simstack/core/data.py +139 -0
- simstack/core/definitions.py +21 -0
- simstack/core/engine.py +116 -0
- simstack/core/find_simstack_modules.py +43 -0
- simstack/core/hash.py +177 -0
- simstack/core/model_table.py +199 -0
- simstack/core/node.py +911 -0
- simstack/core/node_runner.py +254 -0
- simstack/core/node_table.py +380 -0
- simstack/core/recompute_artifacts.py +120 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resources.py +101 -0
- simstack/core/run_node.py +69 -0
- simstack/core/runner.py +435 -0
- simstack/core/simstack_result.py +41 -0
- simstack/core/submit_node.py +143 -0
- simstack/core/task_id.py +22 -0
- simstack/data/__init__.py +0 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/get_file.py +10 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/models/__init__.py +37 -0
- simstack/models/array_list.py +48 -0
- simstack/models/array_storage.py +54 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_types.py +208 -0
- simstack/models/charts_artifact.py +411 -0
- simstack/models/dataset.py +432 -0
- simstack/models/dataset_metadata.py +303 -0
- simstack/models/file_instance.py +77 -0
- simstack/models/file_list.py +321 -0
- simstack/models/files.py +229 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +40 -0
- simstack/models/node_registry.py +105 -0
- simstack/models/pandas_model.py +210 -0
- simstack/models/parameters.py +388 -0
- simstack/models/pickle_models.py +311 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +32 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +273 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/db.py +457 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/file_hashing.py +68 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +26 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +93 -0
- simstack/util/import_module.py +65 -0
- simstack/util/importer.py +163 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +827 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/path_manager.py +234 -0
- simstack/util/project_root_finder.py +36 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +162 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/ui_tools.py +213 -0
- simstack-0.2.0.dist-info/METADATA +361 -0
- simstack-0.2.0.dist-info/RECORD +91 -0
- simstack-0.2.0.dist-info/WHEEL +5 -0
- simstack-0.2.0.dist-info/entry_points.txt +10 -0
- simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- simstack-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterator, List, Union, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DirectoryPath:
|
|
8
|
+
"""
|
|
9
|
+
Represents a directory path with excluded subdirectory patterns and provides iteration capabilities.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
root_path: Union[str, Path],
|
|
15
|
+
excluded_patterns: Optional[List[str]] = None,
|
|
16
|
+
file_extensions: Optional[List[str]] = None,
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the DirectoryPath.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
root_path: The root directory path to traverse
|
|
23
|
+
excluded_patterns: List of patterns to exclude (supports wildcards like *, ?, [])
|
|
24
|
+
file_extensions: List of file extensions to include (e.g., ['.py', '.txt'])
|
|
25
|
+
"""
|
|
26
|
+
self.root_path = Path(root_path).resolve()
|
|
27
|
+
self.excluded_patterns = excluded_patterns or []
|
|
28
|
+
self.file_extensions = file_extensions or []
|
|
29
|
+
|
|
30
|
+
if not self.root_path.exists():
|
|
31
|
+
raise FileNotFoundError(f"Directory does not exist: {self.root_path}")
|
|
32
|
+
if not self.root_path.is_dir():
|
|
33
|
+
raise NotADirectoryError(f"Path is not a directory: {self.root_path}")
|
|
34
|
+
|
|
35
|
+
def _is_excluded(self, path: Path) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Check if a path matches any of the excluded patterns.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
path: Path to check
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
True if the path should be excluded, False otherwise
|
|
44
|
+
"""
|
|
45
|
+
path_str = str(path)
|
|
46
|
+
relative_path = str(path.relative_to(self.root_path))
|
|
47
|
+
|
|
48
|
+
for pattern in self.excluded_patterns:
|
|
49
|
+
# Check against full path
|
|
50
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
51
|
+
return True
|
|
52
|
+
# Check against relative path
|
|
53
|
+
if fnmatch.fnmatch(relative_path, pattern):
|
|
54
|
+
return True
|
|
55
|
+
# Check against directory name only
|
|
56
|
+
if fnmatch.fnmatch(path.name, pattern):
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def _matches_extensions(self, file_path: Path) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if a file matches the specified extensions.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
file_path: File path to check
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the file matches the extensions or no extensions specified
|
|
70
|
+
"""
|
|
71
|
+
if not self.file_extensions:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
return file_path.suffix.lower() in [ext.lower() for ext in self.file_extensions]
|
|
75
|
+
|
|
76
|
+
def iterate_files(self) -> Iterator[Path]:
|
|
77
|
+
"""
|
|
78
|
+
Iterate over all files in the directory tree, respecting exclusion patterns.
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
Path objects for each file found
|
|
82
|
+
"""
|
|
83
|
+
for root, dirs, files in os.walk(self.root_path):
|
|
84
|
+
root_path = Path(root)
|
|
85
|
+
|
|
86
|
+
# Filter out excluded directories in-place to prevent walking into them
|
|
87
|
+
dirs[:] = [d for d in dirs if not self._is_excluded(root_path / d)]
|
|
88
|
+
|
|
89
|
+
# Yield files that match criteria
|
|
90
|
+
for file in files:
|
|
91
|
+
file_path = root_path / file
|
|
92
|
+
if not self._is_excluded(file_path) and self._matches_extensions(
|
|
93
|
+
file_path
|
|
94
|
+
):
|
|
95
|
+
yield file_path
|
|
96
|
+
|
|
97
|
+
def iterate_directories(self) -> Iterator[Path]:
|
|
98
|
+
"""
|
|
99
|
+
Iterate over all directories in the directory tree, respecting exclusion patterns.
|
|
100
|
+
|
|
101
|
+
Yields:
|
|
102
|
+
Path objects for each directory found
|
|
103
|
+
"""
|
|
104
|
+
for root, dirs, _ in os.walk(self.root_path):
|
|
105
|
+
root_path = Path(root)
|
|
106
|
+
|
|
107
|
+
# Filter and yield directories
|
|
108
|
+
for dir_name in dirs:
|
|
109
|
+
dir_path = root_path / dir_name
|
|
110
|
+
if not self._is_excluded(dir_path):
|
|
111
|
+
yield dir_path
|
|
112
|
+
|
|
113
|
+
# Filter out excluded directories to prevent walking into them
|
|
114
|
+
dirs[:] = [d for d in dirs if not self._is_excluded(root_path / d)]
|
|
115
|
+
|
|
116
|
+
def __iter__(self) -> Iterator[Path]:
|
|
117
|
+
"""
|
|
118
|
+
Default iterator returns files.
|
|
119
|
+
"""
|
|
120
|
+
return self.iterate_files()
|
|
121
|
+
|
|
122
|
+
def get_files_list(self) -> List[Path]:
|
|
123
|
+
"""
|
|
124
|
+
Get a list of all files matching the criteria.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of Path objects
|
|
128
|
+
"""
|
|
129
|
+
return list(self.iterate_files())
|
|
130
|
+
|
|
131
|
+
def get_directories_list(self) -> List[Path]:
|
|
132
|
+
"""
|
|
133
|
+
Get a list of all directories matching the criteria.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List of Path objects
|
|
137
|
+
"""
|
|
138
|
+
return list(self.iterate_directories())
|
|
139
|
+
|
|
140
|
+
def add_excluded_pattern(self, pattern: str) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Add an exclusion pattern.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
pattern: Pattern to exclude
|
|
146
|
+
"""
|
|
147
|
+
if pattern not in self.excluded_patterns:
|
|
148
|
+
self.excluded_patterns.append(pattern)
|
|
149
|
+
|
|
150
|
+
def remove_excluded_pattern(self, pattern: str) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Remove an exclusion pattern.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
pattern: Pattern to remove
|
|
156
|
+
"""
|
|
157
|
+
if pattern in self.excluded_patterns:
|
|
158
|
+
self.excluded_patterns.remove(pattern)
|
|
159
|
+
|
|
160
|
+
def __str__(self) -> str:
|
|
161
|
+
return (
|
|
162
|
+
f"DirectoryPath(root='{self.root_path}', excluded={self.excluded_patterns})"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return (
|
|
167
|
+
f"DirectoryPath(root_path='{self.root_path}', "
|
|
168
|
+
f"excluded_patterns={self.excluded_patterns}, "
|
|
169
|
+
f"file_extensions={self.file_extensions})"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Example usage and utility functions
|
|
174
|
+
def find_python_files_with_exclusions(
|
|
175
|
+
root_path: str, excluded_patterns: List[str] = None
|
|
176
|
+
) -> List[Path]:
|
|
177
|
+
"""
|
|
178
|
+
Find Python files using the DirectoryPath class with exclusions.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
root_path: Root directory to search
|
|
182
|
+
excluded_patterns: Patterns to exclude
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of Python file paths
|
|
186
|
+
"""
|
|
187
|
+
excluded_patterns = excluded_patterns or [
|
|
188
|
+
"__pycache__",
|
|
189
|
+
"*.pyc",
|
|
190
|
+
".git",
|
|
191
|
+
".venv",
|
|
192
|
+
"venv",
|
|
193
|
+
]
|
|
194
|
+
dir_path = DirectoryPath(root_path, excluded_patterns, [".py"])
|
|
195
|
+
return dir_path.get_files_list()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
# Example usage
|
|
200
|
+
try:
|
|
201
|
+
# Create a DirectoryPath instance
|
|
202
|
+
dp = DirectoryPath(
|
|
203
|
+
root_path=".",
|
|
204
|
+
excluded_patterns=["__pycache__", "*.pyc", ".git", "node_modules", "*.log"],
|
|
205
|
+
file_extensions=[".py", ".txt"],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
print(f"Directory Path: {dp}")
|
|
209
|
+
print("\nFound files:")
|
|
210
|
+
for file_path in dp:
|
|
211
|
+
print(f" {file_path}")
|
|
212
|
+
|
|
213
|
+
print(f"\nTotal files found: {len(dp.get_files_list())}")
|
|
214
|
+
|
|
215
|
+
except (FileNotFoundError, NotADirectoryError) as e:
|
|
216
|
+
print(f"Error: {e}")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Union, BinaryIO
|
|
4
|
+
|
|
5
|
+
def hash_file(
|
|
6
|
+
file_path: Union[str, Path], algorithm: str = "sha256", chunk_size: int = 8192
|
|
7
|
+
) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Calculate hash of a file on disk.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
file_path: Path to the file
|
|
13
|
+
algorithm: Hash algorithm (md5, sha1, sha256, etc.)
|
|
14
|
+
chunk_size: Size of chunks to read
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
str: Hexadecimal digest of the hash
|
|
18
|
+
"""
|
|
19
|
+
hash_obj = hashlib.new(algorithm)
|
|
20
|
+
|
|
21
|
+
with open(file_path, "rb") as f:
|
|
22
|
+
for chunk in iter(lambda: f.read(chunk_size), b""):
|
|
23
|
+
hash_obj.update(chunk)
|
|
24
|
+
|
|
25
|
+
return hash_obj.hexdigest()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def hash_file_object(
|
|
29
|
+
file_obj: BinaryIO, algorithm: str = "sha256", chunk_size: int = 8192
|
|
30
|
+
) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Calculate hash of a file-like object.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
file_obj: File-like object (must be in binary mode)
|
|
36
|
+
algorithm: Hash algorithm (md5, sha1, sha256, etc.)
|
|
37
|
+
chunk_size: Size of chunks to read
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: Hexadecimal digest of the hash
|
|
41
|
+
"""
|
|
42
|
+
hash_obj = hashlib.new(algorithm)
|
|
43
|
+
|
|
44
|
+
# Store current position
|
|
45
|
+
try:
|
|
46
|
+
original_position = file_obj.tell()
|
|
47
|
+
except (OSError, IOError, AttributeError):
|
|
48
|
+
original_position = None
|
|
49
|
+
|
|
50
|
+
# Seek to the beginning if possible
|
|
51
|
+
try:
|
|
52
|
+
file_obj.seek(0)
|
|
53
|
+
except (OSError, IOError, AttributeError):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
# Calculate hash
|
|
57
|
+
for chunk in iter(lambda: file_obj.read(chunk_size), b""):
|
|
58
|
+
hash_obj.update(chunk)
|
|
59
|
+
|
|
60
|
+
# Restore original position if possible
|
|
61
|
+
if original_position is not None:
|
|
62
|
+
try:
|
|
63
|
+
file_obj.seek(original_position)
|
|
64
|
+
except (OSError, IOError, AttributeError):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return hash_obj.hexdigest()
|
|
68
|
+
|
simstack/util/files.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional, Dict, Any, List
|
|
4
|
+
|
|
5
|
+
from odmantic import Field
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SshAuthMethod(BaseModel):
|
|
10
|
+
""" """
|
|
11
|
+
|
|
12
|
+
# Authentication type
|
|
13
|
+
auth_type: str = Field(
|
|
14
|
+
description="Authentication type (password, key, agent, none)"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Password authentication
|
|
18
|
+
password: Optional[str] = Field(
|
|
19
|
+
default=None,
|
|
20
|
+
description="Password for authentication (should be stored securely)",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Key-based authentication
|
|
24
|
+
key_filename: Optional[str] = Field(
|
|
25
|
+
default=None, description="Path to the private key file"
|
|
26
|
+
)
|
|
27
|
+
key_passphrase: Optional[str] = Field(
|
|
28
|
+
default=None, description="Passphrase for the private key"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Allow host key to be added automatically to known_hosts
|
|
32
|
+
allow_host_key_add: bool = Field(
|
|
33
|
+
default=True, description="Whether to automatically add host key to known_hosts"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Host key verification options
|
|
37
|
+
host_key_policy: str = Field(
|
|
38
|
+
default="ask", description="Host key policy (ask, auto_add, strict)"
|
|
39
|
+
)
|
|
40
|
+
known_hosts_file: Optional[str] = Field(
|
|
41
|
+
default=None, description="Path to known_hosts file"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
model_config = {"extra": "allow"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FfspecFile(BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
Representation of a file with protocol-specific details, metadata, and utility methods
|
|
50
|
+
for remote or local operations.
|
|
51
|
+
|
|
52
|
+
This class facilitates working with files across different storage protocols such as
|
|
53
|
+
local filesystem, SFTP, SCP, or cloud-based protocols. It provides attributes for
|
|
54
|
+
authenticating, accessing, and manipulating files as well as methods for operations
|
|
55
|
+
like copying files, creating URIs, and testing connections.
|
|
56
|
+
|
|
57
|
+
:ivar path: Full path to the file.
|
|
58
|
+
:type path: str
|
|
59
|
+
:ivar name: Name of the file.
|
|
60
|
+
:type name: str
|
|
61
|
+
:ivar extension: File extension.
|
|
62
|
+
:type extension: str
|
|
63
|
+
:ivar protocol: Storage protocol (e.g., 'file', 's3', 'http', 'sftp', 'scp').
|
|
64
|
+
:type protocol: str
|
|
65
|
+
:ivar host: Host name or address where the file is located.
|
|
66
|
+
:type host: Optional[str]
|
|
67
|
+
:ivar port: Port number if applicable.
|
|
68
|
+
:type port: Optional[int]
|
|
69
|
+
:ivar username: Username for authentication if required.
|
|
70
|
+
:type username: Optional[str]
|
|
71
|
+
:ivar ssh_auth: SSH authentication configuration.
|
|
72
|
+
:type ssh_auth: Optional[SshAuthMethod]
|
|
73
|
+
:ivar size: Size of the file in bytes.
|
|
74
|
+
:type size: int
|
|
75
|
+
:ivar created_at: Creation timestamp.
|
|
76
|
+
:type created_at: datetime
|
|
77
|
+
:ivar modified_at: Last modification timestamp.
|
|
78
|
+
:type modified_at: datetime
|
|
79
|
+
:ivar storage_options: Protocol-specific storage options.
|
|
80
|
+
:type storage_options: Dict[str, Any]
|
|
81
|
+
:ivar checksum: File content checksum.
|
|
82
|
+
:type checksum: Optional[str]
|
|
83
|
+
:ivar content_type: MIME type of the file.
|
|
84
|
+
:type content_type: Optional[str]
|
|
85
|
+
:ivar parent_path: Path to parent directory.
|
|
86
|
+
:type parent_path: Optional[str]
|
|
87
|
+
:ivar is_directory: Whether this is a directory.
|
|
88
|
+
:type is_directory: bool
|
|
89
|
+
:ivar children: Child file/directory paths if this is a directory.
|
|
90
|
+
:type children: List[str]
|
|
91
|
+
:ivar last_accessed: Last access timestamp.
|
|
92
|
+
:type last_accessed: Optional[datetime]
|
|
93
|
+
:ivar access_count: Number of times this file has been accessed.
|
|
94
|
+
:type access_count: int
|
|
95
|
+
:ivar metadata: Additional metadata as key-value pairs.
|
|
96
|
+
:type metadata: Dict[str, Any]
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
# Basic file information
|
|
100
|
+
path: str = Field(description="Full path to the file")
|
|
101
|
+
name: str = Field(description="Name of the file")
|
|
102
|
+
extension: str = Field(description="File extension")
|
|
103
|
+
|
|
104
|
+
# Protocol and location information
|
|
105
|
+
protocol: str = Field(
|
|
106
|
+
description="Storage protocol (e.g., 'file', 's3', 'http', 'sftp', 'scp')"
|
|
107
|
+
)
|
|
108
|
+
host: Optional[str] = Field(
|
|
109
|
+
default=None, description="Host name or address where the file is located"
|
|
110
|
+
)
|
|
111
|
+
port: Optional[int] = Field(default=None, description="Port number if applicable")
|
|
112
|
+
|
|
113
|
+
# Authentication information
|
|
114
|
+
username: Optional[str] = Field(
|
|
115
|
+
default=None, description="Username for authentication if required"
|
|
116
|
+
)
|
|
117
|
+
ssh_auth: Optional[SshAuthMethod] = Field(
|
|
118
|
+
default=None, description="SSH authentication configuration"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# File metadata
|
|
122
|
+
size: int = Field(description="Size of the file in bytes")
|
|
123
|
+
created_at: datetime = Field(description="Creation timestamp")
|
|
124
|
+
modified_at: datetime = Field(description="Last modification timestamp")
|
|
125
|
+
|
|
126
|
+
# Ffspec-specific attributes
|
|
127
|
+
storage_options: Dict[str, Any] = Field(
|
|
128
|
+
default_factory=dict, description="Protocol-specific storage options"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Content addressing
|
|
132
|
+
checksum: Optional[str] = Field(default=None, description="File content checksum")
|
|
133
|
+
content_type: Optional[str] = Field(
|
|
134
|
+
default=None, description="MIME type of the file"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Hierarchical structure support
|
|
138
|
+
parent_path: Optional[str] = Field(
|
|
139
|
+
default=None, description="Path to parent directory"
|
|
140
|
+
)
|
|
141
|
+
is_directory: bool = Field(default=False, description="Whether this is a directory")
|
|
142
|
+
children: List[str] = Field(
|
|
143
|
+
default_factory=list,
|
|
144
|
+
description="Child file/directory paths if this is a directory",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Access metadata
|
|
148
|
+
last_accessed: Optional[datetime] = Field(
|
|
149
|
+
default=None, description="Last access timestamp"
|
|
150
|
+
)
|
|
151
|
+
access_count: int = Field(
|
|
152
|
+
default=0, description="Number of times this file has been accessed"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Custom metadata
|
|
156
|
+
metadata: Dict[str, Any] = Field(
|
|
157
|
+
default_factory=dict, description="Additional metadata as key-value pairs"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def from_ssh_key(
|
|
162
|
+
cls,
|
|
163
|
+
host: str,
|
|
164
|
+
path: str,
|
|
165
|
+
username: str,
|
|
166
|
+
key_filename: str,
|
|
167
|
+
protocol: str = "sftp",
|
|
168
|
+
port: int = 22,
|
|
169
|
+
key_passphrase: Optional[str] = None,
|
|
170
|
+
host_key_policy: str = "auto_add",
|
|
171
|
+
**kwargs,
|
|
172
|
+
) -> "FfspecFile":
|
|
173
|
+
"""
|
|
174
|
+
Create a FfspecFile instance with SSH key authentication.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
host: Remote host name or IP address
|
|
178
|
+
path: Path to the file on the remote host
|
|
179
|
+
username: SSH username for authentication
|
|
180
|
+
key_filename: Path to the SSH private key file
|
|
181
|
+
protocol: Protocol to use, either 'sftp' or 'scp' (default: 'sftp')
|
|
182
|
+
port: SSH port on the remote host (default: 22)
|
|
183
|
+
key_passphrase: Passphrase for the SSH key if it's encrypted (default: None)
|
|
184
|
+
host_key_policy: Host key policy - 'auto_add', 'strict', or 'ask' (default: 'auto_add')
|
|
185
|
+
**kwargs: Additional attributes to set on the model
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
FfspecFile: A new instance representing the remote file
|
|
189
|
+
"""
|
|
190
|
+
from pathlib import Path
|
|
191
|
+
|
|
192
|
+
# Validate protocol
|
|
193
|
+
if protocol not in ["sftp", "scp"]:
|
|
194
|
+
raise ValueError(f"Protocol must be 'sftp' or 'scp', got '{protocol}'")
|
|
195
|
+
|
|
196
|
+
# Validate that the key file exists
|
|
197
|
+
if not os.path.exists(key_filename):
|
|
198
|
+
raise FileNotFoundError(f"SSH key file not found: {key_filename}")
|
|
199
|
+
|
|
200
|
+
# Ensure path is absolute (starts with /)
|
|
201
|
+
if not path.startswith("/"):
|
|
202
|
+
path = "/" + path
|
|
203
|
+
|
|
204
|
+
# Create SSH authentication config with proper host key policy
|
|
205
|
+
ssh_auth = SshAuthMethod(
|
|
206
|
+
auth_type="key",
|
|
207
|
+
key_filename=key_filename,
|
|
208
|
+
key_passphrase=key_passphrase,
|
|
209
|
+
host_key_policy=host_key_policy,
|
|
210
|
+
allow_host_key_add=(host_key_policy == "auto_add"),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Extract file name and extension from path
|
|
214
|
+
p = Path(path)
|
|
215
|
+
name = p.name
|
|
216
|
+
extension = p.suffix.lstrip(".") if p.suffix else ""
|
|
217
|
+
parent_path = str(p.parent) if p.parent != p else None
|
|
218
|
+
|
|
219
|
+
# Create comprehensive storage options for fsspec
|
|
220
|
+
# Note: Removed missing_host_key_policy as it's not a valid paramiko connection parameter
|
|
221
|
+
storage_options = {
|
|
222
|
+
"host": host,
|
|
223
|
+
"port": port,
|
|
224
|
+
"username": username,
|
|
225
|
+
"key_filename": key_filename,
|
|
226
|
+
# Connection timeouts
|
|
227
|
+
"timeout": 30,
|
|
228
|
+
"banner_timeout": 30,
|
|
229
|
+
"auth_timeout": 30,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Add key passphrase if provided
|
|
233
|
+
if key_passphrase:
|
|
234
|
+
storage_options["password"] = key_passphrase
|
|
235
|
+
|
|
236
|
+
# Create the instance
|
|
237
|
+
return cls(
|
|
238
|
+
path=path,
|
|
239
|
+
name=name,
|
|
240
|
+
extension=extension,
|
|
241
|
+
protocol=protocol,
|
|
242
|
+
host=host,
|
|
243
|
+
port=port,
|
|
244
|
+
username=username,
|
|
245
|
+
ssh_auth=ssh_auth,
|
|
246
|
+
storage_options=storage_options,
|
|
247
|
+
parent_path=parent_path,
|
|
248
|
+
# Use values from kwargs or sensible defaults for required fields
|
|
249
|
+
size=kwargs.pop("size", 0),
|
|
250
|
+
created_at=kwargs.pop("created_at", datetime.now()),
|
|
251
|
+
modified_at=kwargs.pop("modified_at", datetime.now()),
|
|
252
|
+
is_directory=kwargs.pop("is_directory", False),
|
|
253
|
+
**kwargs,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def get_filesystem(self):
|
|
257
|
+
"""
|
|
258
|
+
Create and return a fsspec filesystem object for this file.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
object: An fsspec filesystem instance
|
|
262
|
+
"""
|
|
263
|
+
import importlib
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Import the appropriate filesystem module
|
|
267
|
+
fsspec = importlib.import_module("fsspec")
|
|
268
|
+
|
|
269
|
+
# Handle SSH-specific protocols
|
|
270
|
+
if self.protocol in ["sftp", "scp"]:
|
|
271
|
+
# Create a copy of storage options to avoid modifying the original
|
|
272
|
+
storage_options = dict(self.storage_options)
|
|
273
|
+
|
|
274
|
+
# Add SSH auth options if available
|
|
275
|
+
if self.ssh_auth:
|
|
276
|
+
if self.ssh_auth.auth_type == "key":
|
|
277
|
+
storage_options["key_filename"] = self.ssh_auth.key_filename
|
|
278
|
+
if self.ssh_auth.key_passphrase:
|
|
279
|
+
storage_options["password"] = self.ssh_auth.key_passphrase
|
|
280
|
+
elif self.ssh_auth.auth_type == "password":
|
|
281
|
+
storage_options["password"] = self.ssh_auth.password
|
|
282
|
+
|
|
283
|
+
# Create the filesystem without the problematic missing_host_key_policy parameter
|
|
284
|
+
return fsspec.filesystem(self.protocol, **storage_options)
|
|
285
|
+
else:
|
|
286
|
+
# Handle other protocols
|
|
287
|
+
return fsspec.filesystem(self.protocol, **self.storage_options)
|
|
288
|
+
|
|
289
|
+
except (ImportError, KeyError) as e:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Could not create filesystem for protocol '{self.protocol}': {e}"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def can_connect(self) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
Test if we can connect to the remote host using the provided credentials.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
bool: True if connection is successful, False otherwise
|
|
300
|
+
"""
|
|
301
|
+
if self.protocol not in ["sftp", "scp"]:
|
|
302
|
+
# Only applicable for SSH-based protocols
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
fs = self.get_filesystem()
|
|
307
|
+
# Try to list the parent directory
|
|
308
|
+
fs.ls(os.path.dirname(self.path))
|
|
309
|
+
return True
|
|
310
|
+
except Exception:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
def to_uri(self) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Convert this model to a URI/URL representation.
|
|
316
|
+
|
|
317
|
+
Note: SSH private key information is not included in the URI.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
str: URI string representing this file
|
|
321
|
+
"""
|
|
322
|
+
import urllib.parse
|
|
323
|
+
|
|
324
|
+
# Build the netloc part
|
|
325
|
+
netloc = ""
|
|
326
|
+
if self.username:
|
|
327
|
+
netloc = f"{self.username}@"
|
|
328
|
+
if self.host:
|
|
329
|
+
netloc += self.host
|
|
330
|
+
if self.port and self.port != 22: # Only include non-standard SSH port
|
|
331
|
+
netloc += f":{self.port}"
|
|
332
|
+
|
|
333
|
+
# Assemble the URI
|
|
334
|
+
return urllib.parse.urlunparse(
|
|
335
|
+
(
|
|
336
|
+
self.protocol, # scheme
|
|
337
|
+
netloc, # netloc
|
|
338
|
+
self.path, # path
|
|
339
|
+
"", # params
|
|
340
|
+
"", # query (not including auth info for security)
|
|
341
|
+
"", # fragment
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def copy_to_local(self, local_path: Optional[str] = None) -> str:
|
|
346
|
+
"""
|
|
347
|
+
Copy the remote file to a local path.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
local_path: Local path to copy the file to. If None, a temporary file is created.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
str: Path to the local copy of the file
|
|
354
|
+
"""
|
|
355
|
+
import tempfile
|
|
356
|
+
|
|
357
|
+
if self.is_directory:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
"Cannot copy a directory, only individual files are supported"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Create a destination path if none is provided
|
|
363
|
+
if local_path is None:
|
|
364
|
+
temp_dir = tempfile.gettempdir()
|
|
365
|
+
local_path = os.path.join(temp_dir, self.name)
|
|
366
|
+
|
|
367
|
+
# Get the filesystem and copy the file
|
|
368
|
+
fs = self.get_filesystem()
|
|
369
|
+
fs.get(self.path, local_path)
|
|
370
|
+
|
|
371
|
+
return local_path
|
|
372
|
+
|
|
373
|
+
def copy_from_local(self, local_path: str) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Copy a local file to the remote location represented by this model.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
local_path: Path to the local file
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
None
|
|
382
|
+
"""
|
|
383
|
+
if not os.path.exists(local_path):
|
|
384
|
+
raise FileNotFoundError(f"Local file {local_path} does not exist")
|
|
385
|
+
|
|
386
|
+
# Get the filesystem and copy the file
|
|
387
|
+
fs = self.get_filesystem()
|
|
388
|
+
fs.put(local_path, self.path)
|
|
389
|
+
|
|
390
|
+
# Update file size and modification time
|
|
391
|
+
try:
|
|
392
|
+
info = fs.info(self.path)
|
|
393
|
+
self.size = info.get("size", 0)
|
|
394
|
+
self.modified_at = datetime.now()
|
|
395
|
+
except Exception:
|
|
396
|
+
pass
|