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.
Files changed (91) hide show
  1. simstack/__init__.py +0 -0
  2. simstack/_version.py +34 -0
  3. simstack/core/__init__.py +0 -0
  4. simstack/core/artifacts.py +447 -0
  5. simstack/core/asnyc_helper.py +14 -0
  6. simstack/core/config_file.py +60 -0
  7. simstack/core/context.py +167 -0
  8. simstack/core/data.py +139 -0
  9. simstack/core/definitions.py +21 -0
  10. simstack/core/engine.py +116 -0
  11. simstack/core/find_simstack_modules.py +43 -0
  12. simstack/core/hash.py +177 -0
  13. simstack/core/model_table.py +199 -0
  14. simstack/core/node.py +911 -0
  15. simstack/core/node_runner.py +254 -0
  16. simstack/core/node_table.py +380 -0
  17. simstack/core/recompute_artifacts.py +120 -0
  18. simstack/core/reset_db.py +12 -0
  19. simstack/core/resources.py +101 -0
  20. simstack/core/run_node.py +69 -0
  21. simstack/core/runner.py +435 -0
  22. simstack/core/simstack_result.py +41 -0
  23. simstack/core/submit_node.py +143 -0
  24. simstack/core/task_id.py +22 -0
  25. simstack/data/__init__.py +0 -0
  26. simstack/methods/__init__.py +0 -0
  27. simstack/methods/get_file.py +10 -0
  28. simstack/methods/multiple_executor.py +307 -0
  29. simstack/models/__init__.py +37 -0
  30. simstack/models/array_list.py +48 -0
  31. simstack/models/array_storage.py +54 -0
  32. simstack/models/artifact_models.py +127 -0
  33. simstack/models/base_types.py +208 -0
  34. simstack/models/charts_artifact.py +411 -0
  35. simstack/models/dataset.py +432 -0
  36. simstack/models/dataset_metadata.py +303 -0
  37. simstack/models/file_instance.py +77 -0
  38. simstack/models/file_list.py +321 -0
  39. simstack/models/files.py +229 -0
  40. simstack/models/log_entry_model.py +37 -0
  41. simstack/models/models.py +40 -0
  42. simstack/models/node_registry.py +105 -0
  43. simstack/models/pandas_model.py +210 -0
  44. simstack/models/parameters.py +388 -0
  45. simstack/models/pickle_models.py +311 -0
  46. simstack/models/runner_model.py +39 -0
  47. simstack/models/simple_table.py +32 -0
  48. simstack/models/simstack_model.py +97 -0
  49. simstack/models/slurm_info.py +17 -0
  50. simstack/models/table_artifact.py +87 -0
  51. simstack/util/__init__.py +0 -0
  52. simstack/util/async_zip_utils.py +18 -0
  53. simstack/util/cleaned_json_schema.py +20 -0
  54. simstack/util/config_reader.py +273 -0
  55. simstack/util/custom_model_dump.py +75 -0
  56. simstack/util/db.py +457 -0
  57. simstack/util/db_logger.py +117 -0
  58. simstack/util/default_from_dict.py +57 -0
  59. simstack/util/directory_iterator.py +216 -0
  60. simstack/util/file_hashing.py +68 -0
  61. simstack/util/files.py +396 -0
  62. simstack/util/find_python_files.py +33 -0
  63. simstack/util/generate_ui_schema.py +80 -0
  64. simstack/util/geometry_utils.py +118 -0
  65. simstack/util/get_module_path.py +26 -0
  66. simstack/util/git_pull.py +65 -0
  67. simstack/util/git_repository_status.py +93 -0
  68. simstack/util/import_module.py +65 -0
  69. simstack/util/importer.py +163 -0
  70. simstack/util/make_graph.py +107 -0
  71. simstack/util/make_table.py +827 -0
  72. simstack/util/minimal_route_finder.py +72 -0
  73. simstack/util/mock_db.py +144 -0
  74. simstack/util/module_path_checker.py +38 -0
  75. simstack/util/path_manager.py +234 -0
  76. simstack/util/project_root_finder.py +36 -0
  77. simstack/util/queue_watcher.py +262 -0
  78. simstack/util/route_finder.py +74 -0
  79. simstack/util/runner_util.py +162 -0
  80. simstack/util/safe_code_executor.py +194 -0
  81. simstack/util/sample_config.py +98 -0
  82. simstack/util/sensitive_content_filter.py +48 -0
  83. simstack/util/setup_logging.py +49 -0
  84. simstack/util/submit_to_watchdog.py +106 -0
  85. simstack/util/ui_tools.py +213 -0
  86. simstack-0.2.0.dist-info/METADATA +361 -0
  87. simstack-0.2.0.dist-info/RECORD +91 -0
  88. simstack-0.2.0.dist-info/WHEEL +5 -0
  89. simstack-0.2.0.dist-info/entry_points.txt +10 -0
  90. simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
  91. 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