syft-rds 0.1.1.dev4__tar.gz → 0.1.1.dev5__tar.gz

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 (90) hide show
  1. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/PKG-INFO +1 -1
  2. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/pyproject.toml +1 -1
  3. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/__init__.py +1 -1
  4. syft_rds-0.1.1.dev5/src/syft_rds/client/exceptions.py +18 -0
  5. syft_rds-0.1.1.dev4/src/syft_rds/client/local_stores/dataset.py → syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/_dataset.py +1 -1
  6. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/__init__.py +3 -0
  7. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/constants.py +4 -0
  8. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/managers/__init__.py +11 -0
  9. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/managers/files.py +211 -0
  10. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/managers/path.py +78 -0
  11. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/managers/schema.py +89 -0
  12. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/managers/url.py +48 -0
  13. syft_rds-0.1.1.dev5/src/syft_rds/client/local_stores/dataset/store.py +223 -0
  14. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/models/base.py +2 -2
  15. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/models/models.py +4 -0
  16. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/store/store.py +6 -3
  17. syft_rds-0.1.1.dev4/src/syft_rds/client/exceptions.py +0 -6
  18. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/.gitignore +0 -0
  19. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/LICENSE +0 -0
  20. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/README.md +0 -0
  21. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/adjectives.txt +0 -0
  22. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/css/fonts.css +0 -0
  23. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/css/model_repr.css +0 -0
  24. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/css/style.css +0 -0
  25. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/css/tabulator_pysyft.min.css +0 -0
  26. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/data_access_template.py.jinja +0 -0
  27. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/jinja/model_repr.jinja2 +0 -0
  28. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/jinja/model_repr_file_section.jinja2 +0 -0
  29. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/jinja/syft_exception.jinja2 +0 -0
  30. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/jinja/table.jinja2 +0 -0
  31. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/js/table.js +0 -0
  32. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/js/tabulator.min.js +0 -0
  33. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/nouns.txt +0 -0
  34. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/arrow.svg +0 -0
  35. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/clipboard.svg +0 -0
  36. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/copy.svg +0 -0
  37. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/folder.svg +0 -0
  38. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/info.svg +0 -0
  39. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/request.svg +0 -0
  40. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/search.svg +0 -0
  41. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/assets/svg/table.svg +0 -0
  42. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/cli.py +0 -0
  43. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/__init__.py +0 -0
  44. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/client_registry.py +0 -0
  45. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/connection.py +0 -0
  46. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_store.py +0 -0
  47. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_stores/__init__.py +0 -0
  48. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_stores/base.py +0 -0
  49. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_stores/jobs.py +0 -0
  50. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_stores/runtime.py +0 -0
  51. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/local_stores/user_code.py +0 -0
  52. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_client.py +0 -0
  53. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/__init__.py +0 -0
  54. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/base.py +0 -0
  55. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/dataset.py +0 -0
  56. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/jobs.py +0 -0
  57. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/runtime.py +0 -0
  58. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/user_code.py +0 -0
  59. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rds_clients/utils.py +0 -0
  60. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/rpc.py +0 -0
  61. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/setup.py +0 -0
  62. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/client/utils.py +0 -0
  63. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/__init__.py +0 -0
  64. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/components.py +0 -0
  65. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/display.py +0 -0
  66. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/icons.py +0 -0
  67. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/table_utils.py +0 -0
  68. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/tabulator.py +0 -0
  69. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/jupyter_utils/types.py +0 -0
  70. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/models/__init__.py +0 -0
  71. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/models/formatter.py +0 -0
  72. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/models/html_format.py +0 -0
  73. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/orchestra.py +0 -0
  74. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/__init__.py +0 -0
  75. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/app.py +0 -0
  76. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/router.py +0 -0
  77. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/routers/job_router.py +0 -0
  78. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/routers/runtime_router.py +0 -0
  79. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/routers/user_code_router.py +0 -0
  80. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/server/user_file_service.py +0 -0
  81. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/store/__init__.py +0 -0
  82. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/syft_runtime/README.md +0 -0
  83. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/syft_runtime/__init__.py +0 -0
  84. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/syft_runtime/main.py +0 -0
  85. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/name_generator.py +0 -0
  86. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/paths.py +0 -0
  87. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/render_code_template.py +0 -0
  88. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/resources.py +0 -0
  89. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/sanitize.py +0 -0
  90. {syft_rds-0.1.1.dev4 → syft_rds-0.1.1.dev5}/src/syft_rds/utils/zip_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syft-rds
3
- Version: 0.1.1.dev4
3
+ Version: 0.1.1.dev5
4
4
  Summary: Doing remote data science with SyftBox
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.9
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "syft-rds"
3
- version = "0.1.1-dev.4"
3
+ version = "0.1.1-dev.5"
4
4
  description = "Doing remote data science with SyftBox"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -1,4 +1,4 @@
1
- __version__ = "0.1.1-dev.4"
1
+ __version__ = "0.1.1-dev.5"
2
2
 
3
3
  from syft_rds.utils.paths import RDS_NOTEBOOKS_PATH, RDS_REPO_PATH # noqa
4
4
  from syft_rds.jupyter_utils.display import display # noqa
@@ -0,0 +1,18 @@
1
+ class RDSClientError(Exception):
2
+ pass
3
+
4
+
5
+ class RDSValidationError(RDSClientError):
6
+ pass
7
+
8
+
9
+ class DatasetExistsError(RDSClientError):
10
+ pass
11
+
12
+
13
+ class DatasetNotFoundError(RDSClientError):
14
+ pass
15
+
16
+
17
+ class JobNotFoundError(RDSClientError):
18
+ pass
@@ -129,7 +129,7 @@ class DatasetUrlManager:
129
129
 
130
130
  @staticmethod
131
131
  def get_private_dataset_syftbox_url(
132
- datasite_email: str, dataset_name: str, path: Union[Path, str]
132
+ datasite_email: str, dataset_name: str, path: Union[Path, str] = None
133
133
  ) -> SyftBoxURL:
134
134
  """Generate a SyftBox URL for the private dataset."""
135
135
  return SyftBoxURL(
@@ -0,0 +1,3 @@
1
+ from .store import DatasetLocalStore
2
+
3
+ __all__ = ["DatasetLocalStore"]
@@ -0,0 +1,4 @@
1
+ # Constants for path segments to avoid magic strings
2
+ DIRECTORY_PUBLIC = "public"
3
+ DIRECTORY_PRIVATE = "private"
4
+ DIRECTORY_DATASETS = "datasets"
@@ -0,0 +1,11 @@
1
+ from .files import DatasetFilesManager
2
+ from .path import DatasetPathManager
3
+ from .schema import DatasetSchemaManager
4
+ from .url import DatasetUrlManager
5
+
6
+ __all__ = [
7
+ "DatasetFilesManager",
8
+ "DatasetPathManager",
9
+ "DatasetSchemaManager",
10
+ "DatasetUrlManager",
11
+ ]
@@ -0,0 +1,211 @@
1
+ import shutil
2
+ from pathlib import Path
3
+ from typing import Union
4
+ from loguru import logger
5
+
6
+ from syft_rds.models.models import DatasetCreate
7
+
8
+ from .path import DatasetPathManager
9
+
10
+
11
+ class DatasetFilesManager:
12
+ """Manages file operations for datasets."""
13
+
14
+ def __init__(self, path_manager: DatasetPathManager) -> None:
15
+ """
16
+ Initialize the files manager.
17
+
18
+ Args:
19
+ path_manager: The path manager to use
20
+ """
21
+ self._path_manager = path_manager
22
+
23
+ def validate_file_extensions(
24
+ self, path: Union[str, Path], mock_path: Union[str, Path]
25
+ ) -> None:
26
+ """
27
+ Validate that both directories contain the same file extensions.
28
+
29
+ Args:
30
+ path: Path to the original data directory
31
+ mock_path: Path to the mock data directory
32
+
33
+ Raises:
34
+ ValueError: If the directories contain different file extensions
35
+ """
36
+ path = Path(path)
37
+ mock_path = Path(mock_path)
38
+
39
+ if not (path.is_dir() and mock_path.is_dir()):
40
+ raise ValueError("Both paths must be directories")
41
+
42
+ # Get all file extensions from the first directory into a set
43
+ path_extensions = self._collect_file_extensions(path)
44
+
45
+ # Get all file extensions from the second directory into a set
46
+ mock_extensions = self._collect_file_extensions(mock_path)
47
+
48
+ # Compare the sets of extensions
49
+ if path_extensions != mock_extensions:
50
+ self._report_extension_differences(
51
+ path, mock_path, path_extensions, mock_extensions
52
+ )
53
+
54
+ def _collect_file_extensions(self, directory: Path) -> set:
55
+ """Collect all file extensions from a directory."""
56
+ extensions = set()
57
+ for file_path in directory.glob("**/*"):
58
+ if file_path.is_file() and file_path.suffix:
59
+ extensions.add(file_path.suffix.lower())
60
+ return extensions
61
+
62
+ def _report_extension_differences(
63
+ self, path: Path, mock_path: Path, path_extensions: set, mock_extensions: set
64
+ ) -> None:
65
+ """Report differences in file extensions between directories."""
66
+ extra_in_path = path_extensions - mock_extensions
67
+ extra_in_mock = mock_extensions - path_extensions
68
+
69
+ error_msg = "Directories contain different file extensions:\n"
70
+ if extra_in_path:
71
+ error_msg += f"Extensions in {path} but not in {mock_path}: {', '.join(extra_in_path)}\n"
72
+ if extra_in_mock:
73
+ error_msg += f"Extensions in {mock_path} but not in {path}: {', '.join(extra_in_mock)}"
74
+
75
+ raise ValueError(error_msg)
76
+
77
+ def copy_directory(self, src: Union[str, Path], dest_dir: Path) -> Path:
78
+ """
79
+ Copy a directory to a destination path.
80
+
81
+ Args:
82
+ src: Source directory
83
+ dest_dir: Destination directory
84
+
85
+ Returns:
86
+ Path to the destination directory
87
+
88
+ Raises:
89
+ ValueError: If the source is not a directory
90
+ """
91
+ src_path = Path(src)
92
+ dest_dir.mkdir(parents=True, exist_ok=True)
93
+
94
+ if not src_path.is_dir():
95
+ raise ValueError(f"Source path is not a directory: {src_path}")
96
+
97
+ # Iterate through all items in the source directory
98
+ for item in src_path.iterdir():
99
+ item_dest = dest_dir / item.name
100
+
101
+ if item.is_dir():
102
+ # Recursively copy subdirectories
103
+ shutil.copytree(item, item_dest, dirs_exist_ok=True)
104
+ else:
105
+ # Copy files
106
+ shutil.copy2(item, dest_dir)
107
+ return dest_dir
108
+
109
+ def copy_mock_to_public_syftbox_dir(
110
+ self, dataset_name: str, mock_path: Union[str, Path]
111
+ ) -> Path:
112
+ """Copy mock data to the public SyftBox directory."""
113
+ public_dataset_dir: Path = self._path_manager.get_local_public_dataset_dir(
114
+ dataset_name
115
+ )
116
+ return self.copy_directory(mock_path, public_dataset_dir)
117
+
118
+ def copy_private_to_private_syftbox_dir(
119
+ self,
120
+ dataset_name: str,
121
+ path: Union[str, Path],
122
+ ) -> Path:
123
+ """Copy private data to the private SyftBox directory."""
124
+ private_dataset_dir: Path = self._path_manager.get_local_private_dataset_dir(
125
+ dataset_name
126
+ )
127
+ return self.copy_directory(path, private_dataset_dir)
128
+
129
+ def copy_description_file_to_public_syftbox_dir(
130
+ self, dataset_name: str, description_path: Union[str, Path, None]
131
+ ) -> Path | None:
132
+ """Copy description file to the public SyftBox directory."""
133
+ if not description_path:
134
+ return
135
+
136
+ public_dataset_dir: Path = self._path_manager.get_local_public_dataset_dir(
137
+ dataset_name
138
+ )
139
+ if not Path(description_path).exists():
140
+ raise ValueError(f"Description file does not exist: {description_path}")
141
+ dest_path = public_dataset_dir / Path(description_path).name
142
+ shutil.copy2(description_path, dest_path)
143
+ return dest_path
144
+
145
+ def copy_dataset_files(self, dataset_create: DatasetCreate) -> None:
146
+ """Copy all necessary files for a new dataset."""
147
+ self.copy_mock_to_public_syftbox_dir(
148
+ dataset_create.name, dataset_create.mock_path
149
+ )
150
+ self.copy_description_file_to_public_syftbox_dir(
151
+ dataset_create.name, dataset_create.description_path
152
+ )
153
+ self.copy_private_to_private_syftbox_dir(
154
+ dataset_create.name, dataset_create.path
155
+ )
156
+
157
+ def cleanup_dataset_files(self, name: str) -> None:
158
+ """
159
+ Remove all dataset files for a given dataset name.
160
+
161
+ Args:
162
+ name: Name of the dataset to clean up
163
+
164
+ Raises:
165
+ RuntimeError: If cleanup fails
166
+ """
167
+ try:
168
+ public_dir = self._path_manager.get_local_public_dataset_dir(name)
169
+ private_dir = self._path_manager.get_local_private_dataset_dir(name)
170
+
171
+ self._safe_remove_directory(public_dir)
172
+ self._safe_remove_directory(private_dir)
173
+ except Exception as e:
174
+ logger.error(f"Failed to cleanup dataset files: {str(e)}")
175
+ raise RuntimeError(f"Failed to clean up dataset '{name}'") from e
176
+
177
+ def move_dataset_files(
178
+ self, src_dataset_name: str, dst_dataset_name: str, overwrite: bool = False
179
+ ) -> None:
180
+ src_public_dir = self._path_manager.get_local_public_dataset_dir(
181
+ src_dataset_name
182
+ )
183
+ src_private_dir: Path = self._path_manager.get_local_private_dataset_dir(
184
+ src_dataset_name
185
+ )
186
+
187
+ if not src_public_dir.exists() or not src_private_dir.exists():
188
+ raise FileNotFoundError("Source directories don't exist.")
189
+
190
+ dst_public_dir: Path = self._path_manager.get_local_public_dataset_dir(
191
+ dst_dataset_name
192
+ )
193
+ dst_private_dir: Path = self._path_manager.get_local_private_dataset_dir(
194
+ dst_dataset_name
195
+ )
196
+
197
+ if not overwrite and (dst_public_dir.exists() or dst_private_dir.exists()):
198
+ raise FileExistsError("Destination directories already exist.")
199
+
200
+ if dst_public_dir.exists():
201
+ shutil.rmtree(dst_public_dir)
202
+ if dst_private_dir.exists():
203
+ shutil.rmtree(dst_private_dir)
204
+
205
+ shutil.move(src_public_dir, dst_public_dir)
206
+ shutil.move(src_private_dir, dst_private_dir)
207
+
208
+ def _safe_remove_directory(self, directory: Path) -> None:
209
+ """Safely remove a directory if it exists."""
210
+ if directory.exists():
211
+ shutil.rmtree(directory)
@@ -0,0 +1,78 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+ from syft_core import Client as SyftBoxClient
4
+
5
+ from ..constants import DIRECTORY_DATASETS, DIRECTORY_PRIVATE, DIRECTORY_PUBLIC
6
+
7
+
8
+ class DatasetPathManager:
9
+ """Manages filesystem paths for dataset operations."""
10
+
11
+ def __init__(self, syftbox_client: SyftBoxClient):
12
+ """
13
+ Initialize the path manager.
14
+
15
+ Args:
16
+ syftbox_client: The SyftBox client
17
+ host: The host identifier
18
+ """
19
+ self.syftbox_client = syftbox_client
20
+
21
+ def get_local_public_dataset_dir(self, dataset_name: str) -> Path:
22
+ """Get the local public directory path for a dataset."""
23
+ return (
24
+ self.syftbox_client.my_datasite
25
+ / DIRECTORY_PUBLIC
26
+ / DIRECTORY_DATASETS
27
+ / dataset_name
28
+ )
29
+
30
+ def get_local_private_dataset_dir(self, dataset_name: str) -> Path:
31
+ """Get the local private directory path for a dataset."""
32
+ return (
33
+ self.syftbox_client.my_datasite
34
+ / DIRECTORY_PRIVATE
35
+ / DIRECTORY_DATASETS
36
+ / dataset_name
37
+ )
38
+
39
+ def get_local_public_datasets_dir(self) -> Path:
40
+ """Get the local base public directory for all datasets."""
41
+ return self.syftbox_client.my_datasite / DIRECTORY_PUBLIC / DIRECTORY_DATASETS
42
+
43
+ @property
44
+ def syftbox_client_email(self) -> str:
45
+ """Get the email of the SyftBox client."""
46
+ return self.syftbox_client.email
47
+
48
+ def validate_path_exists(self, path: Union[str, Path]) -> None:
49
+ """
50
+ Validate that a path exists.
51
+
52
+ Args:
53
+ path: The path to validate
54
+
55
+ Raises:
56
+ ValueError: If the path doesn't exist
57
+ """
58
+ if not Path(path).exists():
59
+ raise FileNotFoundError(f"Path does not exist: {path}")
60
+
61
+ def validate_directory_paths(
62
+ self, path: Union[str, Path], mock_path: Union[str, Path]
63
+ ) -> None:
64
+ """
65
+ Validate that both paths are directories.
66
+
67
+ Args:
68
+ path: The first path to validate
69
+ mock_path: The second path to validate
70
+
71
+ Raises:
72
+ ValueError: If either path is not a directory
73
+ """
74
+ path, mock_path = Path(path), Path(mock_path)
75
+ if not (path.is_dir() and mock_path.is_dir()):
76
+ raise NotADirectoryError(
77
+ f"Mock and private data paths must be directories: {path} and {mock_path}"
78
+ )
@@ -0,0 +1,89 @@
1
+ from pathlib import Path
2
+
3
+ from syft_core import SyftBoxURL
4
+ from syft_rds.models.models import Dataset, DatasetCreate
5
+ from syft_rds.store.store import YAMLStore
6
+
7
+ from .path import DatasetPathManager
8
+ from .url import DatasetUrlManager
9
+
10
+
11
+ class DatasetSchemaManager:
12
+ """Manages schema operations for datasets."""
13
+
14
+ def __init__(self, path_manager: DatasetPathManager, store: YAMLStore) -> None:
15
+ """
16
+ Initialize the schema manager.
17
+
18
+ Args:
19
+ path_manager: The path manager to use
20
+ store: The RDS store for persistence
21
+ """
22
+ self._path_manager = path_manager
23
+ self._schema_store = store
24
+
25
+ def create(self, dataset_create: DatasetCreate) -> Dataset:
26
+ """
27
+ Create a dataset schema.
28
+
29
+ Args:
30
+ dataset_create: Dataset creation data
31
+
32
+ Returns:
33
+ The created dataset
34
+ """
35
+ syftbox_client_email = self._path_manager.syftbox_client_email
36
+
37
+ # Generate URLs for the dataset components
38
+ mock_url = DatasetUrlManager.get_mock_dataset_syftbox_url(
39
+ syftbox_client_email, dataset_create.name, Path(dataset_create.mock_path)
40
+ )
41
+ private_url = DatasetUrlManager.get_private_dataset_syftbox_url(
42
+ syftbox_client_email, dataset_create.name, Path(dataset_create.path)
43
+ )
44
+
45
+ readme_url: SyftBoxURL = (
46
+ DatasetUrlManager.get_readme_syftbox_url(
47
+ syftbox_client_email,
48
+ dataset_create.name,
49
+ Path(dataset_create.description_path),
50
+ )
51
+ if dataset_create.description_path
52
+ else None
53
+ )
54
+
55
+ # Create the dataset schema object
56
+ # TODO: Could we not unpack DatasetCreate directly?
57
+ dataset = Dataset(
58
+ name=dataset_create.name,
59
+ private=private_url,
60
+ mock=mock_url,
61
+ tags=dataset_create.tags,
62
+ summary=dataset_create.summary,
63
+ readme=readme_url,
64
+ auto_approval=dataset_create.auto_approval,
65
+ )
66
+ if dataset_create.runtime:
67
+ dataset.runtime = dataset_create.runtime
68
+
69
+ # Persist the schema to store
70
+ self._schema_store.create(dataset)
71
+ return dataset
72
+
73
+ def delete(self, name: str) -> bool:
74
+ """
75
+ Delete a dataset by name.
76
+
77
+ Args:
78
+ name: Name of the dataset to delete
79
+
80
+ Returns:
81
+ True if deleted, False if not found
82
+ """
83
+ queried_result: list[Dataset] = self._schema_store.get_all(
84
+ filters={"name": name}
85
+ )
86
+ if not queried_result:
87
+ return False
88
+ first_res: Dataset = queried_result[0]
89
+ return self._schema_store.delete(first_res.uid)
@@ -0,0 +1,48 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+ from syft_core import SyftBoxURL
4
+
5
+ from ..constants import DIRECTORY_DATASETS, DIRECTORY_PRIVATE, DIRECTORY_PUBLIC
6
+
7
+
8
+ class DatasetUrlManager:
9
+ """Manages SyftBox URLs for datasets."""
10
+
11
+ @staticmethod
12
+ def get_mock_dataset_syftbox_url(
13
+ datasite_email: str, dataset_name: str, mock_path: Union[Path, str] = None
14
+ ) -> SyftBoxURL:
15
+ """Generate a SyftBox URL for the mock dataset."""
16
+ return SyftBoxURL(
17
+ f"syft://{datasite_email}/{DIRECTORY_PUBLIC}/{DIRECTORY_DATASETS}/{dataset_name}"
18
+ )
19
+
20
+ @staticmethod
21
+ def get_private_dataset_syftbox_url(
22
+ datasite_email: str, dataset_name: str, path: Union[Path, str] = None
23
+ ) -> SyftBoxURL:
24
+ """Generate a SyftBox URL for the private dataset."""
25
+ return SyftBoxURL(
26
+ f"syft://{datasite_email}/{DIRECTORY_PRIVATE}/{DIRECTORY_DATASETS}/{dataset_name}"
27
+ )
28
+
29
+ @staticmethod
30
+ def get_readme_syftbox_url(
31
+ datasite_email: str, dataset_name: str, readme_path: Union[Path, str]
32
+ ) -> SyftBoxURL:
33
+ """Generate a SyftBox URL for the readme file."""
34
+ return SyftBoxURL(
35
+ f"syft://{datasite_email}/{DIRECTORY_PUBLIC}/{DIRECTORY_DATASETS}/{dataset_name}/{Path(readme_path).name}"
36
+ )
37
+
38
+ @staticmethod
39
+ def update_readme_syftbox_url(
40
+ url: SyftBoxURL, *, dataset_name: str = None
41
+ ) -> SyftBoxURL:
42
+ path = Path(url.path)
43
+ datasite_email = url.host
44
+
45
+ if dataset_name:
46
+ path = path.parent.parent / dataset_name / path.name
47
+
48
+ return SyftBoxURL(f"syft://{datasite_email}/{path}")
@@ -0,0 +1,223 @@
1
+ from pathlib import Path
2
+ import traceback
3
+ from typing import TYPE_CHECKING, Final, Type, Union
4
+ from syft_rds.client.exceptions import DatasetExistsError, DatasetNotFoundError
5
+ from syft_rds.client.local_stores.base import CRUDLocalStore
6
+ from syft_rds.models.models import (
7
+ Dataset,
8
+ DatasetCreate,
9
+ DatasetUpdate,
10
+ GetAllRequest,
11
+ GetOneRequest,
12
+ )
13
+ from syft_core import Client as SyftBoxClient
14
+
15
+ from .managers import (
16
+ DatasetPathManager,
17
+ DatasetFilesManager,
18
+ DatasetUrlManager,
19
+ DatasetSchemaManager,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from syft_rds.client.rds_client import RDSClientConfig
24
+
25
+
26
+ class DatasetLocalStore(CRUDLocalStore[Dataset, DatasetCreate, DatasetUpdate]):
27
+ """Local store for dataset operations."""
28
+
29
+ ITEM_TYPE: Final[Type[Dataset]] = Dataset
30
+
31
+ def __init__(self, config: "RDSClientConfig", syftbox_client: SyftBoxClient):
32
+ """
33
+ Initialize the dataset local store.
34
+
35
+ Args:
36
+ config: The RDS client configuration
37
+ syftbox_client: The SyftBox client
38
+ """
39
+ super().__init__(config, syftbox_client)
40
+ self._path_manager = DatasetPathManager(self.syftbox_client)
41
+ self._files_manager = DatasetFilesManager(self._path_manager)
42
+ self._schema_manager = DatasetSchemaManager(self._path_manager, self.store)
43
+
44
+ def _validate_dataset_paths(
45
+ self,
46
+ name: str,
47
+ path: Union[str, Path],
48
+ mock_path: Union[str, Path],
49
+ ) -> None:
50
+ """
51
+ Validate all aspects of dataset paths.
52
+
53
+ Args:
54
+ name: Dataset name
55
+ path: Path to private data
56
+ mock_path: Path to mock data
57
+
58
+ Raises:
59
+ DatasetExistsError: if the Dataset name already exists
60
+ FileNotFoundError: if the private/mock paths don't exist
61
+ NotADirectoryError: if the provided private/mock paths are not directories
62
+ """
63
+ self._validate_dataset_name_unique(name)
64
+ self._validate_path_existence(path, mock_path)
65
+ self._validate_file_extensions(path, mock_path)
66
+
67
+ def _validate_dataset_name_unique(self, name: str) -> None:
68
+ """Validate that dataset name is unique."""
69
+ if (
70
+ self._path_manager.get_local_public_dataset_dir(name).exists()
71
+ or self._path_manager.get_local_private_dataset_dir(name).exists()
72
+ ):
73
+ raise DatasetExistsError(f"Dataset with name '{name}' already exists")
74
+
75
+ def _validate_path_existence(
76
+ self, path: Union[str, Path], mock_path: Union[str, Path]
77
+ ) -> None:
78
+ """Validate that paths exist and are directories."""
79
+ self._path_manager.validate_path_exists(path)
80
+ self._path_manager.validate_path_exists(mock_path)
81
+ self._path_manager.validate_directory_paths(path, mock_path)
82
+
83
+ def _validate_file_extensions(
84
+ self, path: Union[str, Path], mock_path: Union[str, Path]
85
+ ) -> None:
86
+ """Validate that file extensions match between directories."""
87
+ self._files_manager.validate_file_extensions(path, mock_path)
88
+
89
+ def create(self, dataset_create: DatasetCreate) -> Dataset:
90
+ """
91
+ Create a new dataset.
92
+
93
+ Args:
94
+ dataset_create: Dataset creation data
95
+
96
+ Returns:
97
+ The created dataset
98
+
99
+ Raises:
100
+ RuntimeError: If creation fails
101
+ """
102
+ self._validate_dataset_paths(
103
+ dataset_create.name,
104
+ dataset_create.path,
105
+ dataset_create.mock_path,
106
+ )
107
+ try:
108
+ self._files_manager.copy_dataset_files(dataset_create)
109
+ dataset = self._schema_manager.create(dataset_create)
110
+ return dataset._register_client_id_recursive(self.config.uid)
111
+ except Exception as e:
112
+ self._files_manager.cleanup_dataset_files(dataset_create.name)
113
+ self._schema_manager.delete(dataset_create.name)
114
+ raise RuntimeError(
115
+ f"Failed to create dataset '{dataset_create.name}': {str(e)}"
116
+ ) from e
117
+
118
+ def get_all(self, request: GetAllRequest) -> list[Dataset]:
119
+ """
120
+ Get all datasets.
121
+
122
+ Args:
123
+ request: The get all request object
124
+
125
+ Returns:
126
+ List of all datasets
127
+ """
128
+ return super().get_all(request)
129
+
130
+ def update(self, update_item: DatasetUpdate) -> Dataset:
131
+ """
132
+ Update an existing dataset.
133
+
134
+ Args:
135
+ update_dataset: The dataset update data
136
+
137
+ Returns:
138
+ The updated dataset
139
+
140
+ Raises:
141
+ RuntimeError: If update fails
142
+ """
143
+ try:
144
+ existing_dataset = self.store.get_by_uid(update_item.uid)
145
+ if existing_dataset is None:
146
+ raise DatasetNotFoundError(
147
+ f"Dataset with uid {update_item.uid} not found"
148
+ )
149
+
150
+ updated_dataset = existing_dataset.apply_update(update_item, in_place=False)
151
+
152
+ if update_item.name:
153
+ self._validate_dataset_name_unique(update_item.name)
154
+ syftbox_client_email = self._path_manager.syftbox_client_email
155
+ old_dataset_name = existing_dataset.name
156
+ new_dataset_name = update_item.name
157
+
158
+ self._files_manager.move_dataset_files(
159
+ old_dataset_name, new_dataset_name
160
+ )
161
+
162
+ new_private_url = DatasetUrlManager.get_private_dataset_syftbox_url(
163
+ syftbox_client_email, updated_dataset.name
164
+ )
165
+ new_mock_url = DatasetUrlManager.get_mock_dataset_syftbox_url(
166
+ syftbox_client_email, updated_dataset.name
167
+ )
168
+ new_readme_url = DatasetUrlManager.update_readme_syftbox_url(
169
+ updated_dataset.readme, dataset_name=updated_dataset.name
170
+ )
171
+
172
+ updated_dataset.private = new_private_url
173
+ updated_dataset.mock = new_mock_url
174
+ updated_dataset.readme = new_readme_url
175
+
176
+ if update_item.path:
177
+ self._files_manager.copy_private_to_private_syftbox_dir(
178
+ updated_dataset.name, update_item.path
179
+ )
180
+
181
+ updated_dataset = self.store.update(updated_dataset.uid, updated_dataset)
182
+ return updated_dataset._register_client_id_recursive(self.config.uid)
183
+ except DatasetExistsError:
184
+ raise
185
+ except Exception as e:
186
+ print(traceback.format_exc())
187
+ raise RuntimeError(
188
+ f"Failed to update dataset with uid {update_item.uid}: {str(e)}"
189
+ )
190
+
191
+ def get(self, request: GetOneRequest) -> Dataset:
192
+ """
193
+ Get a dataset based on name / id
194
+
195
+ Args:
196
+ request: The get one request object
197
+
198
+ Returns:
199
+ The dataset
200
+ """
201
+ return super().get_one(request)
202
+
203
+ def delete_by_name(self, name: str) -> bool:
204
+ """
205
+ Delete a dataset by name.
206
+
207
+ Args:
208
+ name: Name of the dataset to delete
209
+
210
+ Returns:
211
+ True if deleted, False if not found
212
+
213
+ Raises:
214
+ RuntimeError: If deletion fails
215
+ """
216
+ try:
217
+ schema_deleted = self._schema_manager.delete(name)
218
+ if schema_deleted:
219
+ self._files_manager.cleanup_dataset_files(name)
220
+ return True
221
+ return False
222
+ except Exception as e:
223
+ raise RuntimeError(f"Failed to delete dataset '{name}'") from e
@@ -83,14 +83,14 @@ class ItemBase(BaseModel, ABC):
83
83
  f"Cannot apply update with UID {other.uid} to instance with UID {self.uid}"
84
84
  )
85
85
  if isinstance(other, type(self)):
86
- update_dict = other.model_dump()
86
+ update_dict = other.model_dump(exclude_unset=True, exclude_none=True)
87
87
  elif isinstance(other, ItemBaseUpdate):
88
88
  update_target_type = other.get_target_model()
89
89
  if other.get_target_model() is not type(self):
90
90
  raise ValueError(
91
91
  f"Attempted to apply update for {update_target_type} to {type(self)}"
92
92
  )
93
- update_dict = other.model_dump(exclude_unset=True)
93
+ update_dict = other.model_dump(exclude_unset=True, exclude_none=True)
94
94
  update_dict["updated_at"] = _utcnow()
95
95
  else:
96
96
  raise TypeError(
@@ -436,6 +436,10 @@ class DatasetCreate(ItemBaseCreate[Dataset]):
436
436
 
437
437
 
438
438
  class DatasetUpdate(ItemBaseUpdate[Dataset]):
439
+ name: Optional[str] = None
440
+ path: Optional[str] = Field(
441
+ description="Path of the new private dataset directory", default=None
442
+ )
439
443
  summary: Optional[str] = None
440
444
  auto_approval: Optional[list[str]] = Field(
441
445
  default_factory=list,
@@ -195,7 +195,7 @@ class YAMLStore(Generic[T]):
195
195
  return record
196
196
 
197
197
  @ensure_store_exists
198
- def update(self, uid: str | UUID, record: T) -> Optional[T]:
198
+ def update(self, uid: str | UUID, record: T) -> T:
199
199
  """
200
200
  Update a record by UID
201
201
 
@@ -204,14 +204,17 @@ class YAMLStore(Generic[T]):
204
204
  record: New data to update with
205
205
 
206
206
  Returns:
207
- Updated record if found, None otherwise
207
+ Updated record
208
+
209
+ Raises:
210
+ FileNotFoundError: If no record with the given UID exists
208
211
  """
209
212
  if not isinstance(record, self.item_type):
210
213
  raise TypeError(f"`record` must be of type {self.item_type.__name__}")
211
214
 
212
215
  existing_record = self.get_by_uid(uid)
213
216
  if not existing_record:
214
- return None
217
+ raise FileNotFoundError(f"Record with UID {uid} does not exist")
215
218
 
216
219
  # Update the record
217
220
  updated_record = existing_record.model_copy(
@@ -1,6 +0,0 @@
1
- class RDSClientError(Exception):
2
- pass
3
-
4
-
5
- class RDSValidationError(RDSClientError):
6
- pass
File without changes
File without changes
File without changes