From 5e60218cc534c6548b3d6e04f16ca2b652e63ee0 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:48:53 +0200 Subject: [PATCH 01/12] Create teams resource class --- src/albert/resources/teams.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/albert/resources/teams.py diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py new file mode 100644 index 00000000..66bec4ca --- /dev/null +++ b/src/albert/resources/teams.py @@ -0,0 +1,13 @@ +from typing import Any + +from pydantic import Field + +from albert.resources.base import BaseResource, EntityLinkConvertible, SecurityClass +from albert.resources.users import User + + +class Team(BaseResource, EntityLinkConvertible): + name: str = Field(min_length=1, max_length=255) + team_class: SecurityClass = SecurityClass.RESTRICTED + user: list[User] | None = Field(default=None) + acl: None From 9a5697b5df38ffbba98bf73650ffa5d1ca10a5c3 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:37:38 +0200 Subject: [PATCH 02/12] list method for teams --- src/albert/albert.py | 5 +++ src/albert/collections/teams.py | 59 +++++++++++++++++++++++++++++++++ src/albert/resources/teams.py | 4 +-- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/albert/collections/teams.py diff --git a/src/albert/albert.py b/src/albert/albert.py index 1b7a1f37..ae055bb7 100644 --- a/src/albert/albert.py +++ b/src/albert/albert.py @@ -15,6 +15,7 @@ from albert.collections.roles import RoleCollection from albert.collections.storage_locations import StorageLocationsCollection from albert.collections.tags import TagCollection +from albert.collections.teams import TeamsCollection from albert.collections.un_numbers import UnNumberCollection from albert.collections.units import UnitCollection from albert.collections.users import UserCollection @@ -139,3 +140,7 @@ def storage_locations(self) -> StorageLocationsCollection: @property def pricings(self) -> PricingCollection: return PricingCollection(session=self.session) + + @property + def teams(self) -> TeamsCollection: + return TeamsCollection(session=self.session) diff --git a/src/albert/collections/teams.py b/src/albert/collections/teams.py new file mode 100644 index 00000000..40be6e89 --- /dev/null +++ b/src/albert/collections/teams.py @@ -0,0 +1,59 @@ +from collections.abc import Iterator + +from albert.collections.base import BaseCollection +from albert.resources.teams import Team +from albert.session import AlbertSession + + +class TeamsCollection(BaseCollection): + """ + TeamsCollection is a collection class for managing teams entities. + + Parameters + ---------- + session : AlbertSession + The Albert session instance. + + Attributes + ---------- + base_path : str + The base URL for project API requests. + + Methods + ------- + list(params: dict) -> Iterator + Lists teams with optional filters. + """ + + _api_version = "v3" + + def __init__(self, *, session: AlbertSession): + """ + Initialize a TeamsCollection object. + + Parameters + ---------- + session : AlbertSession + The Albert session instance. + """ + super().__init__(session=session) + self.base_path = f"/api/{TeamsCollection._api_version}/teams" + + def list(self, *, params: dict | None = None) -> Iterator[Team]: + """Lists the available Teams + + Parameters + ---------- + params : dict, optional + _description_, by default {} + + Returns + ------- + List + List of available Roles + """ + if params is None: + params = {} + response = self.session.get(self.base_path, params=params) + team_data = response.json().get("Items", []) + yield from team_data diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py index 66bec4ca..cc775657 100644 --- a/src/albert/resources/teams.py +++ b/src/albert/resources/teams.py @@ -1,5 +1,3 @@ -from typing import Any - from pydantic import Field from albert.resources.base import BaseResource, EntityLinkConvertible, SecurityClass @@ -10,4 +8,4 @@ class Team(BaseResource, EntityLinkConvertible): name: str = Field(min_length=1, max_length=255) team_class: SecurityClass = SecurityClass.RESTRICTED user: list[User] | None = Field(default=None) - acl: None + # acl: None From fda923450e7e9dee68afe78dbedcc17d06c065a2 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 08:00:23 +0200 Subject: [PATCH 03/12] basic tests for teams --- src/albert/collections/teams.py | 71 +++++++++++++++++++++++++++++---- src/albert/resources/teams.py | 4 +- tests/collections/test_teams.py | 24 +++++++++++ tests/conftest.py | 13 ++++++ tests/seeding.py | 5 +++ 5 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 tests/collections/test_teams.py diff --git a/src/albert/collections/teams.py b/src/albert/collections/teams.py index 40be6e89..74971cdc 100644 --- a/src/albert/collections/teams.py +++ b/src/albert/collections/teams.py @@ -1,4 +1,4 @@ -from collections.abc import Iterator +from collections.abc import Generator, Iterator from albert.collections.base import BaseCollection from albert.resources.teams import Team @@ -39,7 +39,55 @@ def __init__(self, *, session: AlbertSession): super().__init__(session=session) self.base_path = f"/api/{TeamsCollection._api_version}/teams" - def list(self, *, params: dict | None = None) -> Iterator[Team]: + def _list_generator( + self, + *, + limit: int = 100, + # order_by: OrderBy = OrderBy.DESCENDING, + # name: str | list[str] = None, + # exact_match: bool = True, + # start_key: str | None = None, + ) -> Generator[Team, None, None]: + """ + Lists team entities with optional filters. + + Parameters + ---------- + limit : int, optional + The maximum number of teams to return, by default 100. + + Returns + ------- + Generator + A generator of Team objects. + """ + params = {"limit": limit} # , "orderBy": order_by.value} + # if name: + # params["name"] = name if isinstance(name, list) else [name] + # params["exactMatch"] = str(exact_match).lower() + # if start_key: # pragma: no cover + # params["startKey"] = start_key + + while True: + response = self.session.get(self.base_path, params=params) + teams_data = response.json().get("Items", []) + if not teams_data or teams_data == []: + break + for t in teams_data: + this_team = Team(**t) + yield this_team + start_key = response.json().get("lastKey") + if not start_key: + break + params["startKey"] = start_key + + def list( + self, + # *, + # order_by: OrderBy = OrderBy.DESCENDING, + # name: str | list[str] = None, + # exact_match: bool = True + ) -> Iterator[Team]: """Lists the available Teams Parameters @@ -52,8 +100,17 @@ def list(self, *, params: dict | None = None) -> Iterator[Team]: List List of available Roles """ - if params is None: - params = {} - response = self.session.get(self.base_path, params=params) - team_data = response.json().get("Items", []) - yield from team_data + return self._list_generator() + + def create(self, *, team: Team) -> Team: + """ """ + response = self.session.post( + self.base_path, json=team.model_dump(by_alias=True, exclude_none=True) + ) + return Team(**response.json()) + + def delete(self, *, team_id: str) -> bool: + """ """ + url = f"{self.base_path}/{team_id}" + self.session.delete(url) + return True diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py index cc775657..08a563df 100644 --- a/src/albert/resources/teams.py +++ b/src/albert/resources/teams.py @@ -5,7 +5,9 @@ class Team(BaseResource, EntityLinkConvertible): + # do all fields from the DWH tables go here? + id: str | None = Field(None, alias="albertId") name: str = Field(min_length=1, max_length=255) - team_class: SecurityClass = SecurityClass.RESTRICTED + team_class: SecurityClass | None = Field(default=None, alias="class") user: list[User] | None = Field(default=None) # acl: None diff --git a/tests/collections/test_teams.py b/tests/collections/test_teams.py new file mode 100644 index 00000000..fc03d051 --- /dev/null +++ b/tests/collections/test_teams.py @@ -0,0 +1,24 @@ +from collections.abc import Generator + +from albert.albert import Albert +from albert.resources.teams import Team + + +def test_create_and_delete(client: Albert, seeded_teams: list[Team]): + # get frist team from seeding list + t = seeded_teams[0] + # create + ret_c = client.teams.create(team=t) + # assert + assert isinstance(ret_c, Team) + # delete + ret_d = client.teams.delete(team_id=ret_c.t) + # assert + assert ret_d == True + + +def test_list(client: Albert): + # get team + t = next(client.teams.list()) + # check type + assert isinstance(t, Team) diff --git a/tests/conftest.py b/tests/conftest.py index e4cf999f..c59e5ac5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,7 @@ generate_project_seeds, generate_storage_location_seeds, generate_tag_seeds, + generate_teams_seeds, generate_unit_seeds, generate_user_seeds, ) @@ -347,3 +348,15 @@ def seeded_pricings(client: Albert, seeded_inventory, seeded_locations): for p in seeded: with suppress(NotFoundError): client.pricings.delete(pricing_id=p.id) + + +@pytest.fixture(scope="session") +def seeded_teams(client: Albert): + seeded = [] # init list + all_teams = generate_teams_seeds() # get list of teams from "seeding.py" + for t in all_teams: + seeded.append(client.teams.create(team=t)) # create all teams -> "setup" + yield seeded + for t in seeded: + with suppress(NotFoundError): + client.teams.delete(team_id=t.id) # delete all teams -> "cleanup" diff --git a/tests/seeding.py b/tests/seeding.py index e5dc790b..1b3f29ab 100644 --- a/tests/seeding.py +++ b/tests/seeding.py @@ -32,6 +32,7 @@ from albert.resources.roles import Role from albert.resources.storage_locations import StorageLocation from albert.resources.tags import Tag +from albert.resources.teams import Team from albert.resources.units import Unit, UnitCategory from albert.resources.users import User, UserClass @@ -663,3 +664,7 @@ def generate_pricing_seeds( price=5375.97, ), ] + + +def generate_teams_seeds() -> list[Team]: + return [Team(name="TEST -- Team A (SDK)"), Team(name="TEST -- Team B (SDK)")] From 41ac0988b8343dafdb567ef95d208fe281da85b6 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:13:50 +0200 Subject: [PATCH 04/12] ruff fix test_teams --- tests/collections/test_teams.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/collections/test_teams.py b/tests/collections/test_teams.py index fc03d051..f5a3d2f9 100644 --- a/tests/collections/test_teams.py +++ b/tests/collections/test_teams.py @@ -1,4 +1,3 @@ -from collections.abc import Generator from albert.albert import Albert from albert.resources.teams import Team From 78e3695d53203f7b09057e8865ee0fe200fcd0aa Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:15:09 +0200 Subject: [PATCH 05/12] Update test_teams.py --- tests/collections/test_teams.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/collections/test_teams.py b/tests/collections/test_teams.py index f5a3d2f9..16db413d 100644 --- a/tests/collections/test_teams.py +++ b/tests/collections/test_teams.py @@ -1,4 +1,3 @@ - from albert.albert import Albert from albert.resources.teams import Team From 1363b389655f83828d7b17ccb1a762adc8bca471 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 09:42:03 +0200 Subject: [PATCH 06/12] name search for list method --- src/albert/collections/teams.py | 10 +++++----- src/albert/resources/teams.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/albert/collections/teams.py b/src/albert/collections/teams.py index 74971cdc..d5a9856e 100644 --- a/src/albert/collections/teams.py +++ b/src/albert/collections/teams.py @@ -44,7 +44,7 @@ def _list_generator( *, limit: int = 100, # order_by: OrderBy = OrderBy.DESCENDING, - # name: str | list[str] = None, + name: list[str] = None, # exact_match: bool = True, # start_key: str | None = None, ) -> Generator[Team, None, None]: @@ -62,8 +62,8 @@ def _list_generator( A generator of Team objects. """ params = {"limit": limit} # , "orderBy": order_by.value} - # if name: - # params["name"] = name if isinstance(name, list) else [name] + if name: + params["name"] = name if isinstance(name, list) else [name] # params["exactMatch"] = str(exact_match).lower() # if start_key: # pragma: no cover # params["startKey"] = start_key @@ -85,7 +85,7 @@ def list( self, # *, # order_by: OrderBy = OrderBy.DESCENDING, - # name: str | list[str] = None, + name: str | list[str] = None, # exact_match: bool = True ) -> Iterator[Team]: """Lists the available Teams @@ -100,7 +100,7 @@ def list( List List of available Roles """ - return self._list_generator() + return self._list_generator(name=name) def create(self, *, team: Team) -> Team: """ """ diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py index 08a563df..d9022b6b 100644 --- a/src/albert/resources/teams.py +++ b/src/albert/resources/teams.py @@ -6,7 +6,7 @@ class Team(BaseResource, EntityLinkConvertible): # do all fields from the DWH tables go here? - id: str | None = Field(None, alias="albertId") + id: str | None = Field(default=None, alias="albertId") name: str = Field(min_length=1, max_length=255) team_class: SecurityClass | None = Field(default=None, alias="class") user: list[User] | None = Field(default=None) From 2843940d1678d2027ef8f9a6b92ce0e60f4e687d Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:34:30 +0200 Subject: [PATCH 07/12] add_users_to_team method added --- src/albert/collections/teams.py | 23 ++++++++++++++++++++++- src/albert/resources/teams.py | 7 +++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/albert/collections/teams.py b/src/albert/collections/teams.py index d5a9856e..ccddbaa8 100644 --- a/src/albert/collections/teams.py +++ b/src/albert/collections/teams.py @@ -1,7 +1,8 @@ from collections.abc import Generator, Iterator from albert.collections.base import BaseCollection -from albert.resources.teams import Team +from albert.resources.teams import Team, TeamRole +from albert.resources.users import User from albert.session import AlbertSession @@ -39,6 +40,26 @@ def __init__(self, *, session: AlbertSession): super().__init__(session=session) self.base_path = f"/api/{TeamsCollection._api_version}/teams" + def add_users_to_team( + self, *, team: Team, users: list[User], team_role: TeamRole = TeamRole.TEAM_VIEWER + ) -> bool: + """ + add users to a team + """ + # build payload + newValue = [] + for _u in users: + newValue.append({"id": _u.id, "fgc": team_role}) + payload = [ + { + "id": team.id, + "data": [{"operation": "add", "attribute": "ACL", "newValue": newValue}], + } + ] + # run request + self.session.patch(self.base_path, json=payload) + return True + def _list_generator( self, *, diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py index d9022b6b..cde5559f 100644 --- a/src/albert/resources/teams.py +++ b/src/albert/resources/teams.py @@ -1,9 +1,16 @@ +from enum import Enum + from pydantic import Field from albert.resources.base import BaseResource, EntityLinkConvertible, SecurityClass from albert.resources.users import User +class TeamRole(str, Enum): + TEAM_OWNER = "TeamOwner" + TEAM_VIEWER = "TeamViewer" + + class Team(BaseResource, EntityLinkConvertible): # do all fields from the DWH tables go here? id: str | None = Field(default=None, alias="albertId") From d569f8e159df531a2da36989bedea982ffa3a342 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:44:12 +0200 Subject: [PATCH 08/12] remove_users_from_team method added --- src/albert/collections/teams.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/albert/collections/teams.py b/src/albert/collections/teams.py index ccddbaa8..f2d6ff85 100644 --- a/src/albert/collections/teams.py +++ b/src/albert/collections/teams.py @@ -60,6 +60,27 @@ def add_users_to_team( self.session.patch(self.base_path, json=payload) return True + def remove_users_from_team(self, *, team: Team, users: list[User]) -> bool: + """ + add users to a team + """ + # build payload + payload = [ + { + "id": team.id, + "data": [ + { + "operation": "delete", + "attribute": "ACL", + "oldValue": [{"id": _u.id} for _u in users], + } + ], + } + ] + # run request + self.session.patch(self.base_path, json=payload) + return True + def _list_generator( self, *, From 5ee03294a755abb5adf3753c09be269ab58b217d Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:03:39 +0100 Subject: [PATCH 09/12] ruff formatting --- src/albert/albert.py | 2 +- tests/conftest.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/albert/albert.py b/src/albert/albert.py index f4619d70..d886bb5c 100644 --- a/src/albert/albert.py +++ b/src/albert/albert.py @@ -185,7 +185,7 @@ def pricings(self) -> PricingCollection: def teams(self) -> TeamsCollection: return TeamsCollection(session=self.session) - @property + @property def files(self) -> FileCollection: return FileCollection(session=self.session) diff --git a/tests/conftest.py b/tests/conftest.py index 0a7b6273..5b0af183 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -581,4 +581,3 @@ def seeded_notes( for note in seeded: with suppress(NotFoundError): client.notes.delete(id=note.id) - From 047fade53da60be01d9f5dac4201e98b000596ef Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:18:37 +0100 Subject: [PATCH 10/12] ruff check fix --- src/albert/albert.py | 2 +- tests/seeding.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/albert/albert.py b/src/albert/albert.py index d886bb5c..f1caa879 100644 --- a/src/albert/albert.py +++ b/src/albert/albert.py @@ -25,8 +25,8 @@ from albert.collections.roles import RoleCollection from albert.collections.storage_locations import StorageLocationsCollection from albert.collections.tags import TagCollection -from albert.collections.teams import TeamsCollection from albert.collections.tasks import TaskCollection +from albert.collections.teams import TeamsCollection from albert.collections.un_numbers import UnNumberCollection from albert.collections.units import UnitCollection from albert.collections.users import UserCollection diff --git a/tests/seeding.py b/tests/seeding.py index bc9e00bb..fdd1d707 100644 --- a/tests/seeding.py +++ b/tests/seeding.py @@ -34,7 +34,6 @@ ) from albert.resources.storage_locations import StorageLocation from albert.resources.tags import Tag -from albert.resources.teams import Team from albert.resources.tasks import ( BaseTask, BatchSizeUnit, @@ -45,6 +44,7 @@ TaskCategory, TaskPriority, ) +from albert.resources.teams import Team from albert.resources.units import Unit, UnitCategory from albert.resources.users import User from albert.resources.workflows import ( From 8c378ba1395e2a13e09166f9b8e5c085994c1e4a Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Fri, 3 Jan 2025 07:40:13 +0100 Subject: [PATCH 11/12] generate task seeds added --- src/albert/__init__.py | 2 +- tests/conftest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/albert/__init__.py b/src/albert/__init__.py index 80db3db1..c924f330 100644 --- a/src/albert/__init__.py +++ b/src/albert/__init__.py @@ -3,4 +3,4 @@ __all__ = ["Albert", "ClientCredentials"] -__version__ = "0.5.9" +__version__ = "0.5.10" diff --git a/tests/conftest.py b/tests/conftest.py index fb3aab20..b21fc846 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,7 @@ generate_project_seeds, generate_storage_location_seeds, generate_tag_seeds, + generate_task_seeds, generate_teams_seeds, generate_unit_seeds, generate_workflow_seeds, From e97b79f7e32496c06f04115d8f9abfa5fb1d31e4 Mon Sep 17 00:00:00 2001 From: m-j-hofmann <139861272+m-j-hofmann@users.noreply.github.com> Date: Fri, 13 Jun 2025 07:34:28 +0200 Subject: [PATCH 12/12] acl added --- src/albert/resources/teams.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/albert/resources/teams.py b/src/albert/resources/teams.py index cde5559f..7cfda981 100644 --- a/src/albert/resources/teams.py +++ b/src/albert/resources/teams.py @@ -2,7 +2,8 @@ from pydantic import Field -from albert.resources.base import BaseResource, EntityLinkConvertible, SecurityClass +from albert.resources.acls import ACL +from albert.resources.base import BaseResource, SecurityClass from albert.resources.users import User @@ -11,10 +12,9 @@ class TeamRole(str, Enum): TEAM_VIEWER = "TeamViewer" -class Team(BaseResource, EntityLinkConvertible): - # do all fields from the DWH tables go here? +class Team(BaseResource): id: str | None = Field(default=None, alias="albertId") name: str = Field(min_length=1, max_length=255) team_class: SecurityClass | None = Field(default=None, alias="class") user: list[User] | None = Field(default=None) - # acl: None + acl: list[ACL] | None = Field(default=None)