syft-perms 0.1.0__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.
@@ -0,0 +1,178 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ credentials/
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ .python-version
87
+
88
+ # pipenv
89
+ Pipfile.lock
90
+
91
+ # poetry
92
+ poetry.lock
93
+
94
+ # pdm
95
+ .pdm.toml
96
+
97
+ # PEP 582
98
+ __pypackages__/
99
+
100
+ # Celery stuff
101
+ celerybeat-schedule
102
+ celerybeat.pid
103
+
104
+ # SageMath parsed files
105
+ *.sage.py
106
+
107
+ # Environments
108
+ .env
109
+ .venv
110
+ env/
111
+ venv/
112
+ ENV/
113
+ env.bak/
114
+ venv.bak/
115
+
116
+ # Spyder project settings
117
+ .spyderproject
118
+ .spyproject
119
+
120
+ # Rope project settings
121
+ .ropeproject
122
+
123
+ # mkdocs documentation
124
+ /site
125
+
126
+ # mypy
127
+ .mypy_cache/
128
+ .dmypy.json
129
+ dmypy.json
130
+
131
+ # Pyre type checker
132
+ .pyre/
133
+
134
+ # pytype static type analyzer
135
+ .pytype/
136
+
137
+ # Cython debug symbols
138
+ cython_debug/
139
+
140
+ # PyCharm
141
+ .idea/
142
+
143
+ # VS Code
144
+ .vscode/
145
+
146
+ # macOS
147
+ .DS_Store
148
+
149
+ # Google Drive credentials
150
+ credentials.json
151
+ client_secret*.json
152
+ token.pickle
153
+ token.json
154
+
155
+ # SyftBox related
156
+ ~/SyftBox/
157
+ SyftBox/
158
+
159
+ # Temporary files
160
+ *.tmp
161
+ *.bak
162
+ *.swp
163
+ *~
164
+
165
+ # Claude AI settings
166
+ .claude/*
167
+ !.claude/settings.json
168
+ CLAUDE.md
169
+
170
+ # CI test outputs
171
+ test_outputs/
172
+ test_suite_output.log
173
+
174
+
175
+ # Notebooks
176
+ notebooks/e2e/sales_mock.csv
177
+ notebooks/e2e/sales_private.csv
178
+ notebooks/e2e/readme.md
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: syft-perms
3
+ Version: 0.1.0
4
+ Summary: User-facing permission API for Syft datasites
5
+ Author-email: OpenMined <info@openmined.org>
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: syft-permissions
@@ -0,0 +1,68 @@
1
+ # syft-perms
2
+
3
+ User-facing permission API for Syft datasites.
4
+
5
+ ## Dev Setup
6
+
7
+ ```bash
8
+ uv pip install -e .
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import syft_perms as sp
15
+
16
+ # 1. Opening files and folders
17
+ file = sp.open("data.csv") # Open a file
18
+ folder = sp.open("my_project/") # Open a folder
19
+ remote = sp.open("syft://alice@datasite.org/data.csv") # Remote files
20
+
21
+ # 2. Granting permissions (each level includes all lower permissions)
22
+ file.grant_read_access("bob@company.com") # Can view
23
+ file.grant_create_access("alice@company.com") # Can view + create new files
24
+ file.grant_write_access("team@company.com") # Can view + create + modify
25
+ file.grant_admin_access("admin@company.com") # Full control
26
+
27
+ # 3. Revoking permissions
28
+ file.revoke_read_access("bob@company.com") # Remove all access
29
+ file.revoke_create_access("alice@company.com") # Remove create (keeps read)
30
+ file.revoke_write_access("team@company.com") # Remove write (keeps read/create)
31
+ file.revoke_admin_access("admin@company.com") # Remove admin privileges
32
+
33
+ # 4. Checking permissions
34
+ if file.has_read_access("bob@company.com"):
35
+ print("Bob can read this file")
36
+
37
+ if file.has_create_access("alice@company.com"):
38
+ print("Alice can create new files")
39
+
40
+ if file.has_write_access("team@company.com"):
41
+ print("Team can modify this file")
42
+
43
+ if file.has_admin_access("admin@company.com"):
44
+ print("Admin has full control")
45
+
46
+ # 5. Understanding permissions with explain
47
+ explanation = file.explain_permissions("bob@company.com")
48
+ print(explanation) # Shows why bob has/doesn't have access
49
+
50
+ # 6. Working with the Files API
51
+ all_items = sp.files_and_folders.all() # Get all files and folders
52
+ files_only = sp.files.all() # Get only files
53
+ folders_only = sp.folders.all() # Get only folders
54
+
55
+ paginated = sp.files.get(limit=10, offset=0) # Get first 10 files
56
+ filtered = sp.files.search(admin="me@datasite.org") # My admin files
57
+ sliced = sp.files[0:5] # First 5 files using slice
58
+
59
+ # 7. Moving files while preserving permissions
60
+ new_file = file.move_file_and_its_permissions("archive/data.csv")
61
+ ```
62
+
63
+ ### Permission Hierarchy
64
+
65
+ - **Read**: View file contents
66
+ - **Create**: Read + create new files in folders
67
+ - **Write**: Read + Create + modify existing files
68
+ - **Admin**: Read + Create + Write + manage permissions
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "syft-perms"
3
+ version = "0.1.0"
4
+ description = "User-facing permission API for Syft datasites"
5
+ authors = [{ name = "OpenMined", email = "info@openmined.org" }]
6
+ license = { text = "Apache-2.0" }
7
+ requires-python = ">=3.10"
8
+
9
+ dependencies = [
10
+ "syft-permissions",
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["src/syft_perms"]
@@ -0,0 +1,18 @@
1
+ from syft_perms.api import files, files_and_folders, folders, open
2
+ from syft_perms.browser import FilesBrowser
3
+ from syft_perms.explain import PermissionExplanation
4
+ from syft_perms.file import SyftFile
5
+ from syft_perms.folder import SyftFolder
6
+ from syft_perms.syftperm_context import SyftPermContext
7
+
8
+ __all__ = [
9
+ "SyftPermContext",
10
+ "SyftFile",
11
+ "SyftFolder",
12
+ "PermissionExplanation",
13
+ "FilesBrowser",
14
+ "open",
15
+ "files",
16
+ "folders",
17
+ "files_and_folders",
18
+ ]
@@ -0,0 +1,35 @@
1
+ """Top-level convenience functions that auto-detect the datasite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from syft_perms.browser import FilesBrowser
6
+ from syft_perms.datasite_utils import _candidate_syftbox_folders, _find_datasite
7
+ from syft_perms.file import SyftFile
8
+ from syft_perms.folder import SyftFolder
9
+ from syft_perms.syftperm_context import SyftPermContext
10
+
11
+
12
+ def _default_context() -> SyftPermContext:
13
+ candidates = _candidate_syftbox_folders()
14
+ datasite = _find_datasite(candidates)
15
+ return SyftPermContext(datasite=datasite)
16
+
17
+
18
+ def open(path: str) -> SyftFile | SyftFolder:
19
+ """Open a file or folder for permission management using auto-detected datasite."""
20
+ return _default_context().open(path)
21
+
22
+
23
+ def files() -> FilesBrowser:
24
+ """Browse all files using auto-detected datasite."""
25
+ return _default_context().files
26
+
27
+
28
+ def folders() -> FilesBrowser:
29
+ """Browse all folders using auto-detected datasite."""
30
+ return _default_context().folders
31
+
32
+
33
+ def files_and_folders() -> FilesBrowser:
34
+ """Browse all files and folders using auto-detected datasite."""
35
+ return _default_context().files_and_folders
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Literal
4
+
5
+ from syft_permissions import PERMISSION_FILE_NAME
6
+
7
+ if TYPE_CHECKING:
8
+ from syft_perms.syftperm_context import SyftPermContext
9
+ from syft_perms.file import SyftFile
10
+ from syft_perms.folder import SyftFolder
11
+
12
+ Kind = Literal["files", "folders", "all"]
13
+
14
+
15
+ class FilesBrowser:
16
+ def __init__(self, perm_context: SyftPermContext, kind: Kind):
17
+ self._perm_context = perm_context
18
+ self._kind = kind
19
+
20
+ def all(self) -> list[SyftFile | SyftFolder]:
21
+ return self._files_and_folders()
22
+
23
+ def get(self, limit: int, offset: int = 0) -> list[SyftFile | SyftFolder]:
24
+ items = self._files_and_folders()
25
+ return items[offset : offset + limit]
26
+
27
+ def search(self, **kwargs: str) -> list[SyftFile | SyftFolder]:
28
+ """Search for files/folders by permission.
29
+
30
+ Keyword args: read, write, or admin with a user email as value.
31
+ Example: search(admin="user@example.com")
32
+ """
33
+ query_clauses = kwargs
34
+ items = self._files_and_folders()
35
+ return [
36
+ item
37
+ for item in items
38
+ if all(
39
+ _has_access(item, level, user) for level, user in query_clauses.items()
40
+ )
41
+ ]
42
+
43
+ def __getitem__(self, key: slice) -> list[SyftFile | SyftFolder]:
44
+ items = self._files_and_folders()
45
+ return items[key]
46
+
47
+ def _files_and_folders(self) -> list[SyftFile | SyftFolder]:
48
+ from syft_perms.file import SyftFile
49
+ from syft_perms.folder import SyftFolder
50
+
51
+ root = self._perm_context.datasite
52
+ items: list[SyftFile | SyftFolder] = []
53
+
54
+ for p in sorted(root.rglob("*")):
55
+ if p.name == PERMISSION_FILE_NAME:
56
+ continue
57
+ if p.is_dir() and self._kind in ("folders", "all"):
58
+ items.append(SyftFolder(p, self._perm_context))
59
+ elif p.is_file() and self._kind in ("files", "all"):
60
+ items.append(SyftFile(p, self._perm_context))
61
+
62
+ return items
63
+
64
+
65
+ def _has_access(item: SyftFile | SyftFolder, level: str, user: str) -> bool:
66
+ if level == "read":
67
+ return item.has_read_access(user)
68
+ elif level == "write":
69
+ return item.has_write_access(user)
70
+ elif level == "admin":
71
+ return item.has_admin_access(user)
72
+ else:
73
+ raise ValueError(f"Unknown access level: {level}")
@@ -0,0 +1,58 @@
1
+ """Auto-detect the SyftBox datasite folder."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ _SYFTBOX_FOLDER_ENV = "SYFTBOX_FOLDER"
10
+
11
+
12
+ def _is_colab() -> bool:
13
+ try:
14
+ import google.colab # noqa: F401
15
+
16
+ return True
17
+ except Exception:
18
+ return False
19
+
20
+
21
+ def _candidate_syftbox_folders() -> list[Path]:
22
+ """Return candidate SyftBox root folders in priority order."""
23
+ env = os.environ.get(_SYFTBOX_FOLDER_ENV)
24
+ if env:
25
+ return [Path(env)]
26
+
27
+ candidates: list[Path] = []
28
+ if _is_colab():
29
+ candidates.append(Path("/content"))
30
+ candidates.append(Path.home() / "SyftBox")
31
+ return candidates
32
+
33
+
34
+ def _find_datasite(candidates: list[Path]) -> Path:
35
+ """Find a single datasite directory inside the first existing candidate."""
36
+ for folder in candidates:
37
+ if not folder.is_dir():
38
+ continue
39
+ datasites = [d for d in folder.iterdir() if d.is_dir() and "@" in d.name]
40
+ if len(datasites) == 1:
41
+ return datasites[0]
42
+ if len(datasites) > 1:
43
+ names = ", ".join(d.name for d in datasites)
44
+ raise ValueError(
45
+ f"Multiple datasites found in {folder}: {names}. "
46
+ "Create a SyftPermContext explicitly:\n"
47
+ " ctx = SyftPermContext(datasite='/path/to/datasite')\n"
48
+ " ctx.open('file.txt')"
49
+ )
50
+ raise ValueError(
51
+ "Could not auto-detect a SyftBox datasite folder. "
52
+ "Either:\n"
53
+ " 1. Set the environment variable: "
54
+ f"export {_SYFTBOX_FOLDER_ENV}=/path/to/syftbox\n"
55
+ " 2. Create a SyftPermContext explicitly:\n"
56
+ " ctx = SyftPermContext(datasite='/path/to/datasite')\n"
57
+ " ctx.open('file.txt')"
58
+ )
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from syft_permissions import PERMISSION_FILE_NAME, ACLRequest, AccessLevel, User
8
+ from syft_permissions.engine.compiled_rule import ACLRule
9
+
10
+ if TYPE_CHECKING:
11
+ from syft_perms.syftperm_context import SyftPermContext
12
+
13
+
14
+ @dataclass
15
+ class PermissionExplanation:
16
+ path: str
17
+ user: str
18
+ is_owner: bool
19
+ governing_yaml: str | None
20
+ matched_rule: str | None
21
+ read: bool
22
+ write: bool
23
+ admin: bool
24
+ reasons: dict[str, str] = field(default_factory=dict)
25
+
26
+ def __repr__(self) -> str:
27
+ return self.__str__()
28
+
29
+ def __str__(self) -> str:
30
+ lines = [
31
+ f"Permissions for '{self.path}' (user: {self.user})",
32
+ f" Owner: {self.is_owner}",
33
+ f" Read: {self.read} — {self.reasons.get('read', '')}",
34
+ f" Write: {self.write} — {self.reasons.get('write', '')}",
35
+ f" Admin: {self.admin} — {self.reasons.get('admin', '')}",
36
+ ]
37
+ if self.governing_yaml:
38
+ lines.append(f" Governing file: {self.governing_yaml}")
39
+ if self.matched_rule:
40
+ lines.append(f" Matched rule: {self.matched_rule}")
41
+ return "\n".join(lines)
42
+
43
+
44
+ def explain(perm: SyftPermContext, rel_path: str, user: str) -> PermissionExplanation:
45
+ """Build a PermissionExplanation for a given path and user."""
46
+ if user == perm.service.owner:
47
+ return _explain_owner(rel_path, user)
48
+
49
+ node = perm.service.tree.get_nearest_node(rel_path)
50
+ if node is None or node.ruleset is None:
51
+ return _explain_denied(rel_path, user, "No permission file found")
52
+
53
+ # NOTE: we are using AccessLevel.READ here because it cannot be empty but its not used
54
+ compiled_rule = perm.service.tree.get_compiled_rule(
55
+ ACLRequest(path=rel_path, user=User(id=user), level=AccessLevel.READ)
56
+ )
57
+ governing_yaml = str(Path(node.path) / PERMISSION_FILE_NAME)
58
+ if compiled_rule is not None:
59
+ return _explain_matched(
60
+ rel_path, user, governing_yaml, compiled_rule.pattern, compiled_rule
61
+ )
62
+
63
+ return _explain_denied(
64
+ rel_path, user, "No matching rule in permission file", governing_yaml
65
+ )
66
+
67
+
68
+ def _explain_owner(rel_path: str, user: str) -> PermissionExplanation:
69
+ reason = "Owner of datasite"
70
+ return PermissionExplanation(
71
+ path=rel_path,
72
+ user=user,
73
+ is_owner=True,
74
+ governing_yaml=None,
75
+ matched_rule=None,
76
+ read=True,
77
+ write=True,
78
+ admin=True,
79
+ reasons={"read": reason, "write": reason, "admin": reason},
80
+ )
81
+
82
+
83
+ def _explain_denied(
84
+ rel_path: str,
85
+ user: str,
86
+ reason: str,
87
+ governing_yaml: str | None = None,
88
+ ) -> PermissionExplanation:
89
+ return PermissionExplanation(
90
+ path=rel_path,
91
+ user=user,
92
+ is_owner=False,
93
+ governing_yaml=governing_yaml,
94
+ matched_rule=None,
95
+ read=False,
96
+ write=False,
97
+ admin=False,
98
+ reasons={"read": reason, "write": reason, "admin": reason},
99
+ )
100
+
101
+
102
+ def _explain_matched(
103
+ rel_path: str,
104
+ user: str,
105
+ governing_yaml: str,
106
+ pattern: str,
107
+ compiled: ACLRule,
108
+ ) -> PermissionExplanation:
109
+ read = compiled.has_read(user)
110
+ write = compiled.has_write(user)
111
+ admin = compiled.has_admin(user)
112
+ reasons = _build_reasons(pattern, read=read, write=write, admin=admin)
113
+ return PermissionExplanation(
114
+ path=rel_path,
115
+ user=user,
116
+ is_owner=False,
117
+ governing_yaml=governing_yaml,
118
+ matched_rule=pattern,
119
+ read=read,
120
+ write=write,
121
+ admin=admin,
122
+ reasons=reasons,
123
+ )
124
+
125
+
126
+ def _build_reasons(pattern: str, **levels: bool) -> dict[str, str]:
127
+ reasons = {}
128
+ for level_name, has_it in levels.items():
129
+ if has_it:
130
+ reasons[level_name] = f"Granted via '{pattern}' in {level_name} list"
131
+ else:
132
+ reasons[level_name] = f"Not in {level_name} list for '{pattern}'"
133
+ return reasons
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from syft_permissions import (
8
+ ACLRequest,
9
+ AccessLevel,
10
+ User,
11
+ )
12
+
13
+ from syft_perms.explain import PermissionExplanation, explain
14
+
15
+ if TYPE_CHECKING:
16
+ from syft_perms.syftperm_context import SyftPermContext
17
+
18
+
19
+ class SyftFile:
20
+ def __init__(self, abs_path: Path, perm_context: SyftPermContext):
21
+ self.abs_path = abs_path
22
+ self._perm_context = perm_context
23
+
24
+ @property
25
+ def _rel_path(self) -> str:
26
+ return str(self.abs_path.relative_to(self._perm_context.datasite))
27
+
28
+ def grant_read_access(self, user: str) -> None:
29
+ self._perm_context.modifier.add_permission_for_user(
30
+ self._rel_path, "read", user
31
+ )
32
+
33
+ def grant_write_access(self, user: str) -> None:
34
+ self._perm_context.modifier.add_permission_for_user(
35
+ self._rel_path, "write", user
36
+ )
37
+
38
+ def grant_admin_access(self, user: str) -> None:
39
+ self._perm_context.modifier.add_permission_for_user(
40
+ self._rel_path, "admin", user
41
+ )
42
+
43
+ def revoke_read_access(self, user: str) -> None:
44
+ self._perm_context.modifier.remove_permission_for_user(
45
+ self._rel_path, "read", user
46
+ )
47
+
48
+ def revoke_write_access(self, user: str) -> None:
49
+ self._perm_context.modifier.remove_permission_for_user(
50
+ self._rel_path, "write", user
51
+ )
52
+
53
+ def revoke_admin_access(self, user: str) -> None:
54
+ self._perm_context.modifier.remove_permission_for_user(
55
+ self._rel_path, "admin", user
56
+ )
57
+
58
+ def has_read_access(self, user: str) -> bool:
59
+ return self._check(AccessLevel.READ, user)
60
+
61
+ def has_write_access(self, user: str) -> bool:
62
+ return self._check(AccessLevel.WRITE, user)
63
+
64
+ def has_admin_access(self, user: str) -> bool:
65
+ return self._check(AccessLevel.ADMIN, user)
66
+
67
+ def explain_permissions(self, user: str) -> PermissionExplanation:
68
+ return explain(self._perm_context, self._rel_path, user)
69
+
70
+ def move_file_and_its_permissions(self, new_path: str) -> SyftFile:
71
+ new_abs = self._perm_context.datasite / new_path
72
+ new_abs.parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ if self.abs_path.exists():
75
+ shutil.move(str(self.abs_path), str(new_abs))
76
+
77
+ self._perm_context.modifier.copy_permissions(self._rel_path, new_abs)
78
+ self._perm_context.modifier.remove_all_rules(self._rel_path)
79
+ self._perm_context._reload()
80
+
81
+ return SyftFile(new_abs, self._perm_context)
82
+
83
+ def _check(self, level: AccessLevel, user: str) -> bool:
84
+ request = ACLRequest(
85
+ path=self._rel_path,
86
+ level=level,
87
+ user=User(id=user),
88
+ )
89
+ return self._perm_context.service.can_access(request)
90
+
91
+ def __repr__(self) -> str:
92
+ return f"SyftFile({self._rel_path})"