manta-node 0.5b0__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 (43) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +90 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +11 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/image_executor.py +222 -0
  22. manta_node/infrastructure/container/manager.py +549 -0
  23. manta_node/infrastructure/filesystem/__init__.py +7 -0
  24. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  25. manta_node/infrastructure/grpc/__init__.py +11 -0
  26. manta_node/infrastructure/grpc/client.py +373 -0
  27. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  28. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  29. manta_node/infrastructure/metrics/__init__.py +10 -0
  30. manta_node/infrastructure/metrics/collector.py +94 -0
  31. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  32. manta_node/infrastructure/mqtt/__init__.py +7 -0
  33. manta_node/infrastructure/mqtt/command_handler.py +450 -0
  34. manta_node/node_orchestrator.py +462 -0
  35. manta_node/task_manager.py +677 -0
  36. manta_node/tasks.py +273 -0
  37. manta_node/utils.py +52 -0
  38. manta_node-0.5b0.dist-info/METADATA +794 -0
  39. manta_node-0.5b0.dist-info/RECORD +43 -0
  40. manta_node-0.5b0.dist-info/WHEEL +5 -0
  41. manta_node-0.5b0.dist-info/entry_points.txt +2 -0
  42. manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
  43. manta_node-0.5b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,469 @@
1
+ import os
2
+ from logging import getLogger
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Generator, List, Optional
5
+
6
+ from manta_common.build.common.informations import DatasetOverviews, Overview
7
+ from manta_common.const import CHUNK_SIZE
8
+ from manta_common.errors import MantaSecurityError, MantaValidationError
9
+ from manta_common.traces import Tracer
10
+
11
+ __all__ = ["DatasetManager"]
12
+
13
+ # File type to dtype mapping
14
+ DTYPES = {
15
+ ".txt": "text",
16
+ ".csv": "csv",
17
+ ".npx": "numpy",
18
+ ".npy": "numpy",
19
+ ".npz": "numpy",
20
+ ".np": "numpy",
21
+ ".jpg": "image",
22
+ ".jpeg": "image",
23
+ ".png": "image",
24
+ ".tif": "image",
25
+ ".tiff": "image",
26
+ ".bin": "binary",
27
+ }
28
+
29
+
30
+ class DatasetManager:
31
+ """
32
+ Dataset Manager that handles parsing, retrieval, and metadata extraction
33
+ for datasets connected to a Node.
34
+
35
+ Parameters
36
+ ----------
37
+
38
+ Node ID
39
+ data_directories : Dict[str, str]
40
+ Dictionary mapping dataset names to folder paths
41
+
42
+ """
43
+
44
+ def __init__(self, data_directories: Optional[Dict[str, str]] = None):
45
+ self.tracer = Tracer(getLogger(__name__))
46
+ self.data_directories = {
47
+ name: Path(path) for name, path in (data_directories or {}).items()
48
+ }
49
+ # Parse the datasets to extract metadata
50
+ self._dataset_overviews = self._parse_datasets()
51
+
52
+ self.tracer.info(
53
+ f"Dataset manager initialized with {len(self.data_directories)} datasets"
54
+ )
55
+
56
+ def update_datasets(self, data_directories: Dict[str, str]) -> None:
57
+ """Update dataset mappings at runtime.
58
+
59
+ Parameters
60
+ ----------
61
+ data_directories : Dict[str, str]
62
+ New dictionary mapping dataset names to folder paths
63
+ """
64
+ self.tracer.info("Updating dataset mappings")
65
+
66
+ # Convert paths to Path objects
67
+ new_directories = {name: Path(path) for name, path in data_directories.items()}
68
+
69
+ # Check what's added and removed
70
+ added = set(new_directories.keys()) - set(self.data_directories.keys())
71
+ removed = set(self.data_directories.keys()) - set(new_directories.keys())
72
+
73
+ # Update the directories
74
+ self.data_directories = new_directories
75
+
76
+ # Re-parse datasets to get updated metadata
77
+ self._dataset_overviews = self._parse_datasets()
78
+
79
+ if added:
80
+ self.tracer.info(f"Added datasets: {added}")
81
+ if removed:
82
+ self.tracer.info(f"Removed datasets: {removed}")
83
+
84
+ self.tracer.info(
85
+ f"Dataset manager updated with {len(self.data_directories)} datasets"
86
+ )
87
+
88
+ def _parse_datasets(self) -> Dict[str, Overview]:
89
+ """
90
+ Parse all datasets and extract metadata
91
+
92
+ Returns
93
+ -------
94
+ Dict[str, Overview]
95
+ Dictionary mapping dataset names to their overviews
96
+ """
97
+ result = {}
98
+ for name, path in self.data_directories.items():
99
+ result[name] = self._parse_dataset(path)
100
+ self.tracer.info(f"Parsed dataset '{name}' with : {result[name]}")
101
+ return result
102
+
103
+ def _parse_dataset(self, folder_path: Path) -> Overview:
104
+ """
105
+ Parse a single dataset folder and extract metadata for all files
106
+
107
+ Parameters
108
+ ----------
109
+ folder_path : Path
110
+ Path to the dataset folder
111
+
112
+ Returns
113
+ -------
114
+ Overview
115
+ Dataset overview
116
+
117
+ Raises
118
+ ------
119
+ MantaValidationError
120
+ If the dataset path does not exist
121
+ """
122
+ if not folder_path.exists():
123
+ raise MantaValidationError(
124
+ f"Dataset path does not exist: {folder_path}. "
125
+ "Please create the dataset directory or file before starting the node."
126
+ )
127
+
128
+ if folder_path.is_file():
129
+ return Overview(
130
+ name=folder_path.stem,
131
+ description=f"File in {folder_path.name}",
132
+ dtype=DTYPES.get(folder_path.suffix.lower(), "binary"),
133
+ )
134
+ else:
135
+ return Overview(
136
+ name=folder_path.name,
137
+ description=f"Folder in {folder_path.name}",
138
+ dtype="folder",
139
+ )
140
+
141
+ def get_dataset_as_bytes(self, dataset_name: str) -> Generator[bytes, Any, None]:
142
+ """
143
+ Get binary data from a file
144
+
145
+ Parameters
146
+ ----------
147
+ dataset_name : str
148
+ Name of the dataset
149
+
150
+ Returns
151
+ -------
152
+ Generator[bytes, Any, None]
153
+ Generator yielding binary data from the file
154
+
155
+ Raises
156
+ ------
157
+ FileNotFoundError
158
+ If the file or dataset doesn't exist
159
+ ValueError
160
+ If the dataset is a folder
161
+ """
162
+ if dataset_name not in self._dataset_overviews:
163
+ raise FileNotFoundError(f"Dataset '{dataset_name}' not found")
164
+
165
+ if self._dataset_overviews[dataset_name].dtype == "folder":
166
+ raise ValueError(f"Dataset '{dataset_name}' is a folder")
167
+ elif self._dataset_overviews[dataset_name].dtype in [
168
+ "binary",
169
+ "csv",
170
+ "numpy",
171
+ "image",
172
+ "text",
173
+ ]:
174
+ return self.get_file_chunks(self.data_directories[dataset_name])
175
+
176
+ raise FileNotFoundError(f"Unknown dataset type for '{dataset_name}'")
177
+
178
+ def read_file_lines(
179
+ self,
180
+ dataset_name: str,
181
+ file_path: str,
182
+ encoding: str = "utf-8",
183
+ errors: str = "strict",
184
+ newline: str = "\n",
185
+ ) -> Generator[bytes, Any, None]:
186
+ """
187
+ Read the lines of a file
188
+
189
+ Parameters
190
+ ----------
191
+ dataset_name : str
192
+ Name of the dataset
193
+ file_path : str
194
+ Path to the file within the dataset
195
+ encoding : str, optional
196
+ Text encoding, by default "utf-8"
197
+ errors : str, optional
198
+ How to handle encoding errors, by default "strict"
199
+ newline : str, optional
200
+ Line ending character, by default "\n"
201
+
202
+ Returns
203
+ -------
204
+ Generator[bytes, Any, None]
205
+ Generator yielding lines as bytes
206
+
207
+ Raises
208
+ ------
209
+ FileNotFoundError
210
+ If the file or dataset doesn't exist
211
+ """
212
+ if not self.check_exists(dataset_name, file_path):
213
+ raise FileNotFoundError(f"File not found: {file_path}")
214
+ full_path = self.get_dataset_file_path(dataset_name, file_path)
215
+ with full_path.open(
216
+ mode="r", encoding=encoding, errors=errors, newline=newline
217
+ ) as f:
218
+ for line in f:
219
+ yield line.encode()
220
+
221
+ def get_file_chunks(self, file_path: Path) -> Generator[bytes, Any, None]:
222
+ """
223
+ Get binary data from a file in chunks
224
+
225
+ Parameters
226
+ ----------
227
+ file_path : Path
228
+ Path to the file within the dataset
229
+
230
+ Yields
231
+ ------
232
+ bytes
233
+ Chunks of binary data
234
+
235
+ Raises
236
+ ------
237
+ FileNotFoundError
238
+ If the file or dataset doesn't exist
239
+ """
240
+ if not file_path.exists():
241
+ raise FileNotFoundError(f"File not found: {file_path}")
242
+ with file_path.open("rb") as f:
243
+ while chunk := f.read(CHUNK_SIZE):
244
+ yield chunk
245
+
246
+ def _validate_file_path(self, file_path: str) -> str:
247
+ """
248
+ Validate and sanitize file paths to prevent directory traversal.
249
+
250
+ Parameters
251
+ ----------
252
+ file_path : str
253
+ The file path to validate
254
+
255
+ Returns
256
+ -------
257
+ str
258
+ The validated and sanitized file path
259
+
260
+ Raises
261
+ ------
262
+ MantaNodeError
263
+ If the path is invalid or contains directory traversal attempts
264
+ """
265
+ if not file_path:
266
+ return file_path
267
+
268
+ # Normalize the path to handle redundant separators and up-level references
269
+ normalized = os.path.normpath(file_path)
270
+
271
+ # Check for directory traversal attempts
272
+ if normalized.startswith("..") or os.path.isabs(normalized):
273
+ self.tracer.warning(
274
+ f"Security violation detected: directory traversal attempt in path: {file_path}"
275
+ )
276
+ raise MantaSecurityError(
277
+ f"Invalid file path - directory traversal attempt: {file_path}"
278
+ )
279
+
280
+ # Additional security: check for null bytes and other suspicious patterns
281
+ if "\x00" in normalized:
282
+ self.tracer.warning(
283
+ f"Security violation detected: null bytes in path: {file_path}"
284
+ )
285
+ raise MantaSecurityError(
286
+ f"Invalid file path - contains null bytes: {file_path}"
287
+ )
288
+
289
+ return normalized
290
+
291
+ def get_dataset_file_path(self, dataset_name: str, file_path: str) -> Path:
292
+ """
293
+ Get the full path to a dataset file with security validation
294
+
295
+ Parameters
296
+ ----------
297
+ dataset_name : str
298
+ Name of the dataset
299
+ file_path : str
300
+ Path to the file within the dataset
301
+
302
+ Returns
303
+ -------
304
+ Path
305
+ Full path to the file
306
+
307
+ Raises
308
+ ------
309
+ FileNotFoundError
310
+ If the file or dataset doesn't exist
311
+ MantaNodeError
312
+ If the path contains directory traversal attempts
313
+ """
314
+ if root := self.data_directories.get(dataset_name):
315
+ # Validate and sanitize the file path
316
+ validated_path = self._validate_file_path(file_path)
317
+
318
+ # Construct the full path
319
+ full_path = root / validated_path
320
+
321
+ # Ensure the resolved path is within the dataset directory
322
+ try:
323
+ # Resolve both paths to absolute paths to check containment
324
+ resolved_full = full_path.resolve()
325
+ resolved_root = root.resolve()
326
+
327
+ # Check if the resolved path is within the dataset directory
328
+ resolved_full.relative_to(resolved_root)
329
+ except ValueError:
330
+ raise MantaSecurityError(
331
+ f"Path traversal attempt detected: {file_path} resolves outside dataset directory"
332
+ )
333
+
334
+ return full_path
335
+ raise FileNotFoundError(f"Dataset '{dataset_name}' not found")
336
+
337
+ def list_files(
338
+ self, dataset_name: str, folder_path: str = ""
339
+ ) -> List[Dict[str, Any]]:
340
+ """
341
+ List files in a dataset folder with security validation
342
+
343
+ Parameters
344
+ ----------
345
+ dataset_name : str
346
+ Name of the dataset
347
+ folder_path : str, optional
348
+ Path to the folder within the dataset, by default ""
349
+
350
+ Returns
351
+ -------
352
+ List[Dict[str, Any]]
353
+ List of file information including name and whether it's a file or folder
354
+
355
+ Raises
356
+ ------
357
+ FileNotFoundError
358
+ If the folder or dataset doesn't exist
359
+ MantaNodeError
360
+ If the path contains directory traversal attempts
361
+ """
362
+ if root := self.data_directories.get(dataset_name):
363
+ # Validate and sanitize the folder path
364
+ validated_folder_path = self._validate_file_path(folder_path)
365
+
366
+ # Construct the directory path
367
+ dir_path = root / validated_folder_path
368
+
369
+ # Ensure the resolved path is within the dataset directory
370
+ try:
371
+ resolved_dir = dir_path.resolve()
372
+ resolved_root = root.resolve()
373
+ resolved_dir.relative_to(resolved_root)
374
+ except ValueError:
375
+ raise MantaSecurityError(
376
+ f"Path traversal attempt detected: {folder_path} resolves outside dataset directory"
377
+ )
378
+
379
+ if not dir_path.exists():
380
+ raise FileNotFoundError(f"Folder not found: {dir_path}")
381
+ return [
382
+ {"name": d.name, "is_file": d.is_file()} for d in dir_path.iterdir()
383
+ ]
384
+ raise FileNotFoundError(f"Dataset '{dataset_name}' not found")
385
+
386
+ def check_exists(self, dataset_name: str, file_path: str) -> bool:
387
+ """
388
+ Check if a file exists in a dataset with security validation
389
+
390
+ Parameters
391
+ ----------
392
+ dataset_name : str
393
+ Name of the dataset
394
+ file_path : str
395
+ Path to the file within the dataset
396
+
397
+ Returns
398
+ -------
399
+ bool
400
+ True if the file exists, False otherwise
401
+ """
402
+ try:
403
+ full_path = self.get_dataset_file_path(dataset_name, file_path)
404
+ return full_path.exists()
405
+ except (FileNotFoundError, MantaSecurityError, MantaValidationError):
406
+ return False
407
+
408
+ def to_proto(self) -> DatasetOverviews:
409
+ """
410
+ Convert all dataset overviews to a proto format
411
+
412
+ Returns
413
+ -------
414
+ DatasetOverviews
415
+ List of all dataset overviews
416
+ """
417
+ self.tracer.info(f"Dataset overviews: {self._dataset_overviews}")
418
+ return DatasetOverviews(
419
+ content=list(self._dataset_overviews.values()),
420
+ )
421
+
422
+ def validate_datasets(self) -> List[str]:
423
+ """
424
+ Validate that all configured dataset paths exist and are accessible.
425
+
426
+ Returns
427
+ -------
428
+ List[str]
429
+ List of validation issues (empty if all datasets are valid)
430
+ """
431
+ issues = []
432
+
433
+ for dataset_name, dataset_path in self.data_directories.items():
434
+ # Expand user paths (e.g., ~/)
435
+ expanded_path = dataset_path.expanduser()
436
+
437
+ # Check if the path exists
438
+ if not expanded_path.exists():
439
+ issues.append(
440
+ f"Dataset '{dataset_name}' path does not exist: {dataset_path}"
441
+ )
442
+ continue
443
+
444
+ # Check if the path is readable (optimized using os.access)
445
+ try:
446
+ # Verify path type
447
+ if not (expanded_path.is_file() or expanded_path.is_dir()):
448
+ issues.append(
449
+ f"Dataset '{dataset_name}' path is neither a file nor a directory: {dataset_path}"
450
+ )
451
+ continue
452
+
453
+ # Use os.access for efficient permission check (no I/O overhead)
454
+ if not os.access(expanded_path, os.R_OK):
455
+ issues.append(
456
+ f"Dataset '{dataset_name}' is not readable (permission denied): {dataset_path}"
457
+ )
458
+ except Exception as e:
459
+ issues.append(
460
+ f"Dataset '{dataset_name}' access error: {dataset_path} - {e}"
461
+ )
462
+
463
+ return issues
464
+
465
+ def refresh(self):
466
+ """
467
+ Refresh the dataset metadata
468
+ """
469
+ self._dataset_overviews = self._parse_datasets()
@@ -0,0 +1,11 @@
1
+ """gRPC infrastructure - Client and server implementations."""
2
+
3
+ from .client import NodeClient
4
+ from .local_servicer import LocalServicer
5
+ from .world_servicer import WorldServicer
6
+
7
+ __all__ = [
8
+ "NodeClient",
9
+ "LocalServicer",
10
+ "WorldServicer",
11
+ ]