Skip to content

Commit dc4eb16

Browse files
authored
Better Team and Folder support (#1373)
* CLI fixes * docs + overwrite * changelog * wip * wip * pr feedback * no-downsample * wip * tests * lint * changelog * typing * pr feedback * pr feedback
1 parent 7915fd2 commit dc4eb16

File tree

15 files changed

+799
-59
lines changed

15 files changed

+799
-59
lines changed

docs/mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ nav:
275275
- Layer: api/webknossos/dataset/layer.md
276276
- MagView: api/webknossos/dataset/mag_view.md
277277
- View: api/webknossos/dataset/view.md
278+
- RemoteFolder: api/webknossos/dataset/remote_folder.md
278279
- Annotation: api/webknossos/annotation/annotation.md
279280
- Skeleton:
280281
- Skeleton: api/webknossos/skeleton/skeleton.md
@@ -284,6 +285,7 @@ nav:
284285
- Authentication & Server Context: api/webknossos/client/context.md
285286
- Administration:
286287
- User: api/webknossos/administration/user.md
288+
- Team: api/webknossos/administration/team.md
287289
- Project: api/webknossos/administration/project.md
288290
- Task: api/webknossos/administration/task.md
289291
- CLI Reference:

webknossos/Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
2020
- Added `downsample`, `max-mag`, `interpolation-mode`, `sampling-mode` args to `convert`, `convert-raw` and `convert-zarr` CLI commands, where it was missing, for consistency. [#1372](https://github.com/scalableminds/webknossos-libs/pull/1372)
2121
- Added value rescaling to `convert-raw` CLI command through `source-dtype` and `rescale-min-max` args. [#1372](https://github.com/scalableminds/webknossos-libs/pull/1372)
2222
- Added `interpolation_mode` and `compress` kwargs to `Dataset.downsample` method. [#1372](https://github.com/scalableminds/webknossos-libs/pull/1372)
23+
- Added `Team.get_by_id`, `Team.add_user` and `Team.delete` methods. [#1373](https://github.com/scalableminds/webknossos-libs/pull/1373)
24+
- Added `RemoteFolder.get_root`, `RemoteFolder.get_subfolders`, `RemoteFolder.get_datasets`, `RemoteFolder.add_subfolder`, `RemoteFolder.move_to`, `RemoteFolder.delete` methods and `RemoteFolder.allowed_teams`, `RemoteFolder.name` properties. [#1373](https://github.com/scalableminds/webknossos-libs/pull/1373)
2325

2426
### Changed
27+
- `Team.add` now returns the created team object. [#1373](https://github.com/scalableminds/webknossos-libs/pull/1373)
28+
- Moved `Team` to `webknossos.administration.team` module. [#1373](https://github.com/scalableminds/webknossos-libs/pull/1373)
2529

2630
### Fixed
2731

webknossos/tests/cassettes/test_dataset_download_upload_remote/test_folders_and_teams.yml

Lines changed: 495 additions & 0 deletions
Large diffs are not rendered by default.

webknossos/tests/dataset/test_dataset_download_upload_remote.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,29 @@ def test_remote_dataset(tmp_path: Path) -> None:
135135
assert remote_ds.folder.name == "A subfolder!"
136136

137137

138+
def test_folders_and_teams() -> None:
139+
folder_name = "test_folder"
140+
team_name = "test_team"
141+
142+
remote_folder = wk.RemoteFolder.get_root().add_subfolder(folder_name)
143+
assert remote_folder.name == folder_name
144+
145+
remote_team = wk.Team.add(team_name)
146+
remote_folder.allowed_teams = (remote_team,)
147+
assert remote_folder.allowed_teams == (remote_team,)
148+
149+
remote_folder.name = f"{folder_name}_renamed"
150+
assert remote_folder.name == f"{folder_name}_renamed"
151+
152+
subfolder = remote_folder.add_subfolder(f"{folder_name}_subfolder")
153+
assert remote_folder.get_subfolders() == (subfolder,)
154+
subfolder.delete()
155+
assert remote_folder.get_subfolders() == ()
156+
157+
remote_folder.delete()
158+
remote_team.delete()
159+
160+
138161
def test_upload_download_roundtrip(tmp_path: Path) -> None:
139162
ds_original = get_sample_dataset(tmp_path)
140163
uploaded_dataset = ds_original.upload(

webknossos/webknossos/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1919
Additionally, we provide the various geometrical primitives, e.g. `Vec3Int`, `BoundingBox` and `Mag`.
2020
21-
The `User`, `Project` and `Task` classes provide WEBKNOSSOS server interactions for administration purposes.
21+
The `User`, `Team`, `Project` and `Task` classes provide WEBKNOSSOS server interactions for administration purposes.
2222
Server interactions may require [authentication](webknossos/client/context.html) e.g. via `webknossos_context`.
2323
"""
2424

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# ruff: noqa: F401 imported but unused
22
from .project import Project
33
from .task import Task, TaskType
4-
from .user import Team, User
4+
from .team import Team
5+
from .user import User

webknossos/webknossos/administration/project.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from ..client.api_client.errors import UnexpectedStatusError
77
from ..client.api_client.models import ApiProject, ApiProjectCreate
88
from ..client.context import _get_api_client
9-
from .user import Team, User
9+
from .team import Team
10+
from .user import User
1011

1112
if TYPE_CHECKING:
1213
from .task import Task

webknossos/webknossos/administration/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from ..geometry import BoundingBox, Vec3Int
2222
from ..utils import warn_deprecated
2323
from .project import Project
24-
from .user import Team
24+
from .team import Team
2525

2626
logger = logging.getLogger(__name__)
2727

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from typing import TYPE_CHECKING
2+
3+
import attr
4+
5+
from ..client.api_client.models import ApiTeamAdd
6+
from ..client.context import _get_api_client
7+
8+
if TYPE_CHECKING:
9+
from .user import User
10+
11+
12+
@attr.frozen
13+
class Team:
14+
id: str
15+
name: str
16+
organization_id: str
17+
18+
@classmethod
19+
def get_by_name(cls, name: str) -> "Team":
20+
"""Returns the Team specified by the passed name if your token authorizes you to see it."""
21+
client = _get_api_client(enforce_auth=True)
22+
api_teams = client.team_list()
23+
for api_team in api_teams:
24+
if api_team.name == name:
25+
return cls(api_team.id, api_team.name, api_team.organization)
26+
raise KeyError(f"Could not find team {name}.")
27+
28+
@classmethod
29+
def get_by_id(cls, team_id: str) -> "Team":
30+
"""Returns the Team specified by the passed ID."""
31+
client = _get_api_client(enforce_auth=True)
32+
api_teams = client.team_list()
33+
for api_team in api_teams:
34+
if api_team.id == team_id:
35+
return cls(api_team.id, api_team.name, api_team.organization)
36+
raise KeyError(f"Could not find team {team_id}.")
37+
38+
@classmethod
39+
def get_list(cls) -> list["Team"]:
40+
"""Returns all teams of the current user."""
41+
client = _get_api_client(enforce_auth=True)
42+
api_teams = client.team_list()
43+
return [
44+
cls(api_team.id, api_team.name, api_team.organization)
45+
for api_team in api_teams
46+
]
47+
48+
@classmethod
49+
def add(cls, team_name: str) -> "Team":
50+
"""Adds a new team with the specified name."""
51+
client = _get_api_client(enforce_auth=True)
52+
client.team_add(team=ApiTeamAdd(team_name))
53+
return cls.get_by_name(team_name)
54+
55+
def add_user(self, user: "User", *, is_team_manager: bool = False) -> None:
56+
"""Adds a user to the team."""
57+
user.assign_team_roles(self, is_team_manager=is_team_manager)
58+
59+
def delete(self) -> None:
60+
"""Deletes the team."""
61+
client = _get_api_client(enforce_auth=True)
62+
client.team_delete(team_id=self.id)

webknossos/webknossos/administration/user.py

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from ..client.api_client.models import (
44
ApiLoggedTimeGroupedByMonth,
5-
ApiTeamAdd,
65
ApiTeamMembership,
76
ApiUser,
87
)
98
from ..client.context import _get_api_client
9+
from .team import Team
1010

1111

1212
@attr.frozen
@@ -21,7 +21,7 @@ class User:
2121
last_name: str
2222
created: int
2323
last_activity: int
24-
teams: tuple["Team", ...]
24+
teams: tuple[Team, ...]
2525
experiences: dict[str, int]
2626
is_active: bool
2727
is_admin: bool
@@ -84,59 +84,26 @@ def get_all_managed_users(cls) -> list["User"]:
8484
api_users = client.user_list()
8585
return [cls._from_api_user(i) for i in api_users]
8686

87-
def assign_team_roles(self, team_name: str, is_team_manager: bool) -> None:
87+
def assign_team_roles(self, team: "str | Team", is_team_manager: bool) -> None:
8888
"""Assigns the specified roles to the user for the specified team."""
8989
client = _get_api_client(enforce_auth=True)
9090
api_user = client.user_by_id(user_id=self.user_id)
91-
if team_name in [team.name for team in api_user.teams]:
91+
team_obj = Team.get_by_name(team) if isinstance(team, str) else team
92+
if team_obj.id in [t.id for t in api_user.teams]:
93+
# updates tean membership
9294
api_user.teams = [
93-
team
94-
if team.name != team_name
95-
else ApiTeamMembership(team.id, team.name, is_team_manager)
96-
for team in api_user.teams
95+
t
96+
if t.id != team_obj.id
97+
else ApiTeamMembership(t.id, t.name, is_team_manager)
98+
for t in api_user.teams
9799
]
98100
else:
99101
api_user.teams.append(
100-
ApiTeamMembership(
101-
Team.get_by_name(team_name).id, team_name, is_team_manager
102-
)
102+
ApiTeamMembership(team_obj.id, team_obj.name, is_team_manager)
103103
)
104104
client.user_update(user=api_user)
105105

106106

107-
@attr.frozen
108-
class Team:
109-
id: str
110-
name: str
111-
organization_id: str
112-
113-
@classmethod
114-
def get_by_name(cls, name: str) -> "Team":
115-
"""Returns the Team specified by the passed name if your token authorizes you to see it."""
116-
client = _get_api_client(enforce_auth=True)
117-
api_teams = client.team_list()
118-
for api_team in api_teams:
119-
if api_team.name == name:
120-
return cls(api_team.id, api_team.name, api_team.organization)
121-
raise KeyError(f"Could not find team {name}.")
122-
123-
@classmethod
124-
def get_list(cls) -> list["Team"]:
125-
"""Returns all teams of the current user."""
126-
client = _get_api_client(enforce_auth=True)
127-
api_teams = client.team_list()
128-
return [
129-
cls(api_team.id, api_team.name, api_team.organization)
130-
for api_team in api_teams
131-
]
132-
133-
@classmethod
134-
def add(cls, team_name: str) -> None:
135-
"""Adds a new team with the specified name."""
136-
client = _get_api_client(enforce_auth=True)
137-
client.team_add(team=ApiTeamAdd(team_name))
138-
139-
140107
@attr.frozen
141108
class LoggedTime:
142109
duration_in_seconds: int

0 commit comments

Comments
 (0)