diff --git a/docs/labelbox/user-group.rst b/docs/labelbox/user-group.rst new file mode 100644 index 000000000..66d56891f --- /dev/null +++ b/docs/labelbox/user-group.rst @@ -0,0 +1,6 @@ +User Group +=============================================================================================== + +.. automodule:: labelbox.schema.user_group + :members: + :show-inheritance: \ No newline at end of file diff --git a/libs/labelbox/src/labelbox/client.py b/libs/labelbox/src/labelbox/client.py index 019285350..bb2aaf1cd 100644 --- a/libs/labelbox/src/labelbox/client.py +++ b/libs/labelbox/src/labelbox/client.py @@ -543,6 +543,19 @@ def get_projects(self, where=None) -> PaginatedCollection: """ return self._get_all(Entity.Project, where) + def get_users(self, where=None) -> PaginatedCollection: + """ Fetches all the users. + + >>> users = client.get_users(where=User.email == "") + + Args: + where (Comparison, LogicalOperation or None): The `where` clause + for filtering. + Returns: + An iterable of Users (typically a PaginatedCollection). + """ + return self._get_all(Entity.User, where, filter_deleted=False) + def get_datasets(self, where=None) -> PaginatedCollection: """ Fetches one or more datasets. diff --git a/libs/labelbox/src/labelbox/schema/user_group.py b/libs/labelbox/src/labelbox/schema/user_group.py new file mode 100644 index 000000000..c8779251b --- /dev/null +++ b/libs/labelbox/src/labelbox/schema/user_group.py @@ -0,0 +1,418 @@ +from enum import Enum +from typing import Set, List, Union, Iterator, Optional + +from labelbox import Client +from labelbox.exceptions import ResourceCreationError +from labelbox.pydantic_compat import BaseModel +from labelbox.schema.user import User +from labelbox.schema.project import Project +from labelbox.exceptions import UnprocessableEntityError, InvalidQueryError + + +class UserGroupColor(Enum): + """ + Enum representing the colors available for a group. + + Attributes: + BLUE (str): Hex color code for blue (#9EC5FF). + PURPLE (str): Hex color code for purple (#CEB8FF). + ORANGE (str): Hex color code for orange (#FFB35F). + CYAN (str): Hex color code for cyan (#4ED2F9). + PINK (str): Hex color code for pink (#FFAEA9). + LIGHT_PINK (str): Hex color code for light pink (#FFA9D5). + GREEN (str): Hex color code for green (#3FDC9A). + YELLOW (str): Hex color code for yellow (#E7BF00). + GRAY (str): Hex color code for gray (#B8C4D3). + """ + BLUE = "9EC5FF" + PURPLE = "CEB8FF" + ORANGE = "FFB35F" + CYAN = "4ED2F9" + PINK = "FFAEA9" + LIGHT_PINK = "FFA9D5" + GREEN = "3FDC9A" + YELLOW = "E7BF00" + GRAY = "B8C4D3" + + +class UserGroupUser(BaseModel): + """ + Represents a user in a group. + + Attributes: + id (str): The ID of the user. + email (str): The email of the user. + """ + id: str + email: str + + def __hash__(self): + return hash((self.id)) + + def __eq__(self, other): + if not isinstance(other, UserGroupUser): + return False + return self.id == other.id + + +class UserGroupProject(BaseModel): + """ + Represents a project in a group. + + Attributes: + id (str): The ID of the project. + name (str): The name of the project. + """ + id: str + name: str + + def __hash__(self): + return hash((self.id)) + + def __eq__(self, other): + """ + Check if this GroupProject object is equal to another GroupProject object. + + Args: + other (GroupProject): The other GroupProject object to compare with. + + Returns: + bool: True if the two GroupProject objects are equal, False otherwise. + """ + if not isinstance(other, UserGroupProject): + return False + return self.id == other.id + + +class UserGroup(BaseModel): + """ + Represents a user group in Labelbox. + + Attributes: + id (Optional[str]): The ID of the user group. + name (Optional[str]): The name of the user group. + color (UserGroupColor): The color of the user group. + users (Set[UserGroupUser]): The set of users in the user group. + projects (Set[UserGroupProject]): The set of projects associated with the user group. + client (Client): The Labelbox client object. + + Methods: + __init__(self, client: Client, id: str = "", name: str = "", color: UserGroupColor = UserGroupColor.BLUE, + users: Set[UserGroupUser] = set(), projects: Set[UserGroupProject] = set(), reload=True) + _reload(self) + update(self) -> "UserGroup" + create(self) -> "UserGroup" + delete(self) -> bool + get_user_groups(client: Client) -> Iterator["UserGroup"] + """ + id: Optional[str] + name: Optional[str] + color: UserGroupColor + users: Set[UserGroupUser] + projects: Set[UserGroupProject] + client: Client + + class Config: + # fix for pydnatic 2 + arbitrary_types_allowed = True + + def __init__( + self, + client: Client, + id: str = "", + name: str = "", + color: UserGroupColor = UserGroupColor.BLUE, + users: Set[UserGroupUser] = set(), + projects: Set[UserGroupProject] = set(), + reload=True, + ): + """ + Initializes a UserGroup object. + + Args: + client (Client): The Labelbox client object. + id (str, optional): The ID of the user group. Defaults to an empty string. + name (str, optional): The name of the user group. Defaults to an empty string. + color (UserGroupColor, optional): The color of the user group. Defaults to UserGroupColor.BLUE. + users (Set[UserGroupUser], optional): The set of users in the user group. Defaults to an empty set. + projects (Set[UserGroupProject], optional): The set of projects associated with the user group. Defaults to an empty set. + reload (bool, optional): Whether to reload the partial representation of the group. Defaults to True. + + Raises: + RuntimeError: If the experimental feature is not enabled in the client. + + """ + super().__init__(client=client, id=id, name=name, color=color, users=users, projects=projects) + if not self.client.enable_experimental: + raise RuntimeError( + "Please enable experimental in client to use UserGroups") + + # partial representation of the group, reload + if self.id and reload: + self._reload() + + def _reload(self): + """ + Reloads the user group information from the server. + + This method sends a GraphQL query to the server to fetch the latest information + about the user group, including its name, color, projects, and members. The fetched + information is then used to update the corresponding attributes of the `Group` object. + + Raises: + InvalidQueryError: If the query fails to fetch the group information. + + Returns: + None + """ + query = """ + query GetUserGroupPyApi($id: ID!) { + userGroup(where: {id: $id}) { + id + name + color + projects { + nodes { + id + name + } + totalCount + } + members { + nodes { + id + email + } + totalCount + } + } + } + """ + params = { + "id": self.id, + } + result = self.client.execute(query, params) + if not result: + raise InvalidQueryError("Failed to fetch group") + self.name = result["userGroup"]["name"] + self.color = UserGroupColor(result["userGroup"]["color"]) + self.projects = { + UserGroupProject(id=project["id"], name=project["name"]) + for project in result["userGroup"]["projects"]["nodes"] + } + self.users = { + UserGroupUser(id=member["id"], email=member["email"]) + for member in result["userGroup"]["members"]["nodes"] + } + + def update(self) -> "UserGroup": + """ + Updates the group in Labelbox. + + Returns: + UserGroup: The updated UserGroup object. (self) + + Raises: + UnprocessableEntityError: If the update fails. + """ + query = """ + mutation UpdateUserGroupPyApi($id: ID!, $name: String!, $color: String!, $projectIds: [String!]!, $userIds: [String!]!) { + updateUserGroup( + where: {id: $id} + data: {name: $name, color: $color, projectIds: $projectIds, userIds: $userIds} + ) { + group { + id + name + color + projects { + nodes { + id + name + } + } + members { + nodes { + id + email + } + } + } + } + } + """ + params = { + "id": + self.id, + "name": + self.name, + "color": + self.color.value, + "projectIds": [ + project.id for project in self.projects + ], + "userIds": [ + user.id for user in self.users + ] + } + result = self.client.execute(query, params) + if not result: + raise UnprocessableEntityError("Failed to update group") + return self + + def create(self) -> "UserGroup": + """ + Creates a new user group. + + Raises: + ResourceCreationError: If the group already exists. + ValueError: If the group name is not provided. + + Returns: + UserGroup: The created user group. + """ + if self.id: + raise ResourceCreationError("Group already exists") + if not self.name: + raise ValueError("Group name is required") + query = """ + mutation CreateUserGroupPyApi($name: String!, $color: String!, $projectIds: [String!]!, $userIds: [String!]!) { + createUserGroup( + data: { + name: $name, + color: $color, + projectIds: $projectIds, + userIds: $userIds + } + ) { + group { + id + name + color + projects { + nodes { + id + name + } + } + members { + nodes { + id + email + } + } + } + } + } + """ + params = { + "name": + self.name, + "color": + self.color.value, + "projectIds": [ + project.id for project in self.projects + ], + "userIds": [ + user.id for user in self.users + ] + } + result = self.client.execute(query, params) + if not result: + raise ResourceCreationError("Failed to create group") + result = result["createUserGroup"]["group"] + self.id = result["id"] + return self + + def delete(self) -> bool: + """ + Deletes the user group from Labelbox. + + This method sends a mutation request to the Labelbox API to delete the user group + with the specified ID. If the deletion is successful, it returns True. Otherwise, + it raises an UnprocessableEntityError and returns False. + + Returns: + bool: True if the user group was successfully deleted, False otherwise. + + Raises: + UnprocessableEntityError: If the deletion of the user group fails. + """ + query = """ + mutation DeleteUserGroupPyApi($id: ID!) { + deleteUserGroup(where: {id: $id}) { + success + } + } + """ + params = {"id": self.id} + result = self.client.execute(query, params) + if not result: + raise UnprocessableEntityError("Failed to delete user group") + return result["deleteUserGroup"]["success"] + + @staticmethod + def get_user_groups(client: Client) -> Iterator["UserGroup"]: + """ + Gets all user groups in Labelbox. + + Args: + client (Client): The Labelbox client. + + Returns: + Iterator[UserGroup]: An iterator over the user groups. + """ + query = """ + query GetUserGroupsPyApi { + userGroups { + nodes { + id + name + color + projects { + nodes { + id + name + } + totalCount + } + members { + nodes { + id + email + } + totalCount + } + } + nextCursor + } + } + """ + nextCursor = None + while True: + userGroups = client.execute( + query, {"nextCursor": nextCursor})["userGroups"] + if not userGroups: + return + yield + groups = userGroups["nodes"] + for group in groups: + yield UserGroup(client, + reload=False, + id=group["id"], + name=group["name"], + color=UserGroupColor(group["color"]), + users={ + UserGroupUser(id=member["id"], + email=member["email"]) + for member in group["members"]["nodes"] + }, + projects={ + UserGroupProject(id=project["id"], + name=project["name"]) + for project in group["projects"]["nodes"] + }) + nextCursor = userGroups["nextCursor"] + # this doesn't seem to be implemented right now to return a value other than null from the api + if not nextCursor: + break diff --git a/libs/labelbox/tests/conftest.py b/libs/labelbox/tests/conftest.py index 80229e319..420bc5a83 100644 --- a/libs/labelbox/tests/conftest.py +++ b/libs/labelbox/tests/conftest.py @@ -158,6 +158,8 @@ def execute(self, query=None, params=None, check_naming=True, **kwargs): assert re.match(r"\s*(?:query|mutation) \w+PyApi", query) is not None self.queries.append((query, params)) + if not kwargs.get('timeout'): + kwargs['timeout'] = 30.0 return super().execute(query, params, **kwargs) diff --git a/libs/labelbox/tests/integration/schema/test_user_group.py b/libs/labelbox/tests/integration/schema/test_user_group.py new file mode 100644 index 000000000..678fdc146 --- /dev/null +++ b/libs/labelbox/tests/integration/schema/test_user_group.py @@ -0,0 +1,90 @@ +import pytest +import faker +from labelbox import Client +from labelbox.schema.user_group import UserGroup, UserGroupColor, UserGroupUser, UserGroupProject + +data = faker.Faker() + +@pytest.fixture +def user_group(client): + group_name = data.name() + # Create a new user group + user_group = UserGroup(client) + user_group.name = group_name + user_group.color = UserGroupColor.BLUE + user_group.create() + + yield user_group + + user_group.delete() + + +def test_existing_user_groups(user_group, client): + # Verify that the user group was created successfully + user_group_equal = UserGroup(client, id=user_group.id) + assert user_group.id == user_group_equal.id + assert user_group.name == user_group_equal.name + assert user_group.color == user_group_equal.color + + +def test_create_user_group(user_group): + # Verify that the user group was created successfully + assert user_group.id is not None + assert user_group.name is not None + assert user_group.color == UserGroupColor.BLUE + + +def test_update_user_group(user_group): + # Update the user group + group_name = data.name() + user_group.name = group_name + user_group.color = UserGroupColor.PURPLE + user_group.update() + + # Verify that the user group was updated successfully + assert user_group.name == group_name + assert user_group.color == UserGroupColor.PURPLE + + +def test_get_user_groups(user_group, client): + # Get all user groups + user_groups_old = UserGroup.get_user_groups(client) + + # manual delete for iterators + group_name = data.name() + user_group = UserGroup(client) + user_group.name = group_name + user_group.create() + + user_groups_new = UserGroup.get_user_groups(client) + + # Verify that at least one user group is returned + assert len(user_groups_new) > 0 + assert len(user_groups_new) == len(user_groups_old) + 1 + + # Verify that each user group has a valid ID and name + for user_group in user_groups_new: + assert user_group.id is not None + assert user_group.name is not None + + user_group.delete() + + +# project_pack creates two projects +def test_update_user_group(user_group, client, project_pack): + users = list(client.get_users()) + projects = project_pack + + # Add the user to the group + user_group.users.add(users[0]) + user_group.projects.add(projects[0]) + user_group.update() + + # Verify that the user is added to the group + assert users[0] in user_group.users + assert projects[0] in user_group.projects + + +if __name__ == "__main__": + import subprocess + subprocess.call(["pytest", "-v", __file__]) \ No newline at end of file diff --git a/libs/labelbox/tests/integration/test_offline_chat_evaluation_project.py b/libs/labelbox/tests/integration/test_offline_chat_evaluation_project.py index d27f4e95e..f1e3877ff 100644 --- a/libs/labelbox/tests/integration/test_offline_chat_evaluation_project.py +++ b/libs/labelbox/tests/integration/test_offline_chat_evaluation_project.py @@ -1,6 +1,5 @@ import pytest - def test_create_offline_chat_evaluation_project(client, rand_gen, offline_chat_evaluation_project, chat_evaluation_ontology, diff --git a/libs/labelbox/tests/unit/schema/test_user_group.py b/libs/labelbox/tests/unit/schema/test_user_group.py new file mode 100644 index 000000000..6f1400308 --- /dev/null +++ b/libs/labelbox/tests/unit/schema/test_user_group.py @@ -0,0 +1,417 @@ +import pytest +from unittest.mock import MagicMock +from labelbox import Client +from labelbox.exceptions import ResourceCreationError +from labelbox.schema.user import User +from labelbox.schema.user_group import UserGroup, UserGroupColor, UserGroupUser, UserGroupProject + + +class TestUserGroupColor: + + def test_user_group_color_values(self): + assert UserGroupColor.BLUE.value == "9EC5FF" + assert UserGroupColor.PURPLE.value == "CEB8FF" + assert UserGroupColor.ORANGE.value == "FFB35F" + assert UserGroupColor.CYAN.value == "4ED2F9" + assert UserGroupColor.PINK.value == "FFAEA9" + assert UserGroupColor.LIGHT_PINK.value == "FFA9D5" + assert UserGroupColor.GREEN.value == "3FDC9A" + assert UserGroupColor.YELLOW.value == "E7BF00" + assert UserGroupColor.GRAY.value == "B8C4D3" + + +class TestUserGroupUser: + + def test_user_group_user_attributes(self): + user = UserGroupUser(id="user_id", email="test@example.com") + assert user.id == "user_id" + assert user.email == "test@example.com" + + def test_user_group_user_equality(self): + user1 = UserGroupUser(id="user_id", email="test@example.com") + user2 = UserGroupUser(id="user_id", email="test@example.com") + assert user1 == user2 + + def test_user_group_user_hash(self): + user = UserGroupUser(id="user_id", email="test@example.com") + assert hash(user) == hash("user_id") + + +class TestUserGroupProject: + + def test_user_group_project_attributes(self): + project = UserGroupProject(id="project_id", name="Test Project") + assert project.id == "project_id" + assert project.name == "Test Project" + + def test_user_group_project_equality(self): + project1 = UserGroupProject(id="project_id", name="Test Project") + project2 = UserGroupProject(id="project_id", name="Test Project") + assert project1 == project2 + + def test_user_group_project_hash(self): + project = UserGroupProject(id="project_id", name="Test Project") + assert hash(project) == hash("project_id") + + +class TestUserGroup: + + def setup_method(self): + self.client = MagicMock(Client) + self.client.enable_experimental = True + self.group = UserGroup(client=self.client, name="Test Group") + + def test_constructor_experimental_needed(self): + client = MagicMock(Client) + client.enable_experimental = False + with pytest.raises(RuntimeError): + group = UserGroup(client) + + def test_constructor_name(self): + group = self.group + assert group.name == "Test Group" + assert group.color == UserGroupColor.BLUE + + def test_constructor_id_no_reload(self): + projects = [{ + "id": "project_id_1", + "name": "project_1" + }, { + "id": "project_id_2", + "name": "project_2" + }] + group_members = [{ + "id": "user_id_1", + "email": "email_1" + }, { + "id": "user_id_2", + "email": "email_2" + }] + self.client.execute.return_value = { + "userGroup": { + "id": "group_id", + "name": "Test Group", + "color": "4ED2F9", + "projects": { + "nodes": projects + }, + "members": { + "nodes": group_members + } + } + } + + group = UserGroup(self.client, id="group_id", reload=False) + + assert group.id == "group_id" + assert group.name == "" + assert group.color is UserGroupColor.BLUE + assert len(group.projects) == 0 + assert len(group.users) == 0 + + def test_constructor_id(self): + projects = [{ + "id": "project_id_1", + "name": "project_1" + }, { + "id": "project_id_2", + "name": "project_2" + }] + group_members = [{ + "id": "user_id_1", + "email": "email_1" + }, { + "id": "user_id_2", + "email": "email_2" + }] + self.client.execute.return_value = { + "userGroup": { + "id": "group_id", + "name": "Test Group", + "color": "4ED2F9", + "projects": { + "nodes": projects + }, + "members": { + "nodes": group_members + } + } + } + group = UserGroup(self.client, id="group_id") + assert group.id == "group_id" + assert group.name == "Test Group" + assert group.color == UserGroupColor.CYAN + assert len(group.projects) == 2 + assert len(group.users) == 2 + + def test_id(self): + group = self.group + assert group.id == "" + + group.id = "1" + assert group.id == "1" + + group.id = "2" + assert group.id == "2" + + def test_name(self): + group = self.group + assert group.name == "Test Group" + + group.name = "New Group" + assert group.name == "New Group" + + group.name = "Another Group" + assert group.name == "Another Group" + + def test_color(self): + group = self.group + assert group.color is UserGroupColor.BLUE + + group.color = UserGroupColor.PINK + assert group.color == UserGroupColor.PINK + + group.color = UserGroupColor.YELLOW + assert group.color == UserGroupColor.YELLOW + + def test_users(self): + group = self.group + assert len(group.users) == 0 + + group.users = {UserGroupUser(id="user_id", email="user_id@email")} + assert len(group.users) == 1 + + group.users = { + UserGroupUser(id="user_id", email="user_id@email"), + UserGroupUser(id="user_id", email="user_id@email") + } + assert len(group.users) == 1 + + group.users = {} + assert len(group.users) == 0 + + def test_projects(self): + group = self.group + assert len(group.projects) == 0 + + group.projects = { + UserGroupProject(id="project_id", name="Test Project") + } + assert len(group.projects) == 1 + + group.projects = { + UserGroupProject(id="project_id", name="Test Project"), + UserGroupProject(id="project_id", name="Test Project") + } + assert len(group.projects) == 1 + + group.projects = {} + assert len(group.projects) == 0 + + def test_update(self): + group = self.group + group.id = "group_id" + group.name = "Test Group" + group.color = UserGroupColor.BLUE + group.users = {UserGroupUser(id="user_id", email="test@example.com")} + group.projects = { + UserGroupProject(id="project_id", name="Test Project") + } + + updated_group = group.update() + + execute = self.client.execute.call_args[0] + + assert "UpdateUserGroupPyApi" in execute[0] + assert execute[1]["id"] == "group_id" + assert execute[1]["name"] == "Test Group" + assert execute[1]["color"] == UserGroupColor.BLUE.value + assert len(execute[1]["userIds"]) == 1 + assert list(execute[1]["userIds"])[0] == "user_id" + assert len(execute[1]["projectIds"]) == 1 + assert list(execute[1]["projectIds"])[0] == "project_id" + + assert updated_group.id == "group_id" + assert updated_group.name == "Test Group" + assert updated_group.color == UserGroupColor.BLUE + assert len(updated_group.users) == 1 + assert list(updated_group.users)[0].id == "user_id" + assert len(updated_group.projects) == 1 + assert list(updated_group.projects)[0].id == "project_id" + + def test_create_with_exception_id(self): + group = self.group + group.id = "group_id" + + with pytest.raises(ResourceCreationError): + group.create() + + def test_create_with_exception_name(self): + group = self.group + group.name = "" + + with pytest.raises(ValueError): + group.create() + + def test_create(self): + group = self.group + group.name = "New Group" + group.color = UserGroupColor.PINK + group.users = {UserGroupUser(id="user_id", email="test@example.com")} + group.projects = { + UserGroupProject(id="project_id", name="Test Project") + } + + self.client.execute.return_value = { + "createUserGroup": { + "group": { + "id": "group_id" + } + } + } + created_group = group.create() + execute = self.client.execute.call_args[0] + + assert "CreateUserGroupPyApi" in execute[0] + assert execute[1]["name"] == "New Group" + assert execute[1]["color"] == UserGroupColor.PINK.value + assert len(execute[1]["userIds"]) == 1 + assert list(execute[1]["userIds"])[0] == "user_id" + assert len(execute[1]["projectIds"]) == 1 + assert list(execute[1]["projectIds"])[0] == "project_id" + assert created_group.id is not None + assert created_group.id == "group_id" + assert created_group.name == "New Group" + assert created_group.color == UserGroupColor.PINK + assert len(created_group.users) == 1 + assert list(created_group.users)[0].id == "user_id" + assert len(created_group.projects) == 1 + assert list(created_group.projects)[0].id == "project_id" + + def test_delete(self): + group = self.group + group.id = "group_id" + + self.client.execute.return_value = { + "deleteUserGroup": { + "success": True + } + } + deleted = group.delete() + execute = self.client.execute.call_args[0] + + assert "DeleteUserGroupPyApi" in execute[0] + assert execute[1]["id"] == "group_id" + assert deleted is True + + def test_user_groups_empty(self): + self.client.execute.return_value = {"userGroups": None} + + user_groups = list(UserGroup.get_user_groups(self.client)) + + assert len(user_groups) == 0 + + def test_user_groups(self): + self.client.execute.return_value = { + "userGroups": { + "nextCursor": + None, + "nodes": [{ + "id": "group_id_1", + "name": "Group 1", + "color": "9EC5FF", + "projects": { + "nodes": [{ + "id": "project_id_1", + "name": "Project 1" + }, { + "id": "project_id_2", + "name": "Project 2" + }] + }, + "members": { + "nodes": [{ + "id": "user_id_1", + "email": "user1@example.com" + }, { + "id": "user_id_2", + "email": "user2@example.com" + }] + } + }, { + "id": "group_id_2", + "name": "Group 2", + "color": "9EC5FF", + "projects": { + "nodes": [{ + "id": "project_id_3", + "name": "Project 3" + }, { + "id": "project_id_4", + "name": "Project 4" + }] + }, + "members": { + "nodes": [{ + "id": "user_id_3", + "email": "user3@example.com" + }, { + "id": "user_id_4", + "email": "user4@example.com" + }] + } + }, { + "id": "group_id_3", + "name": "Group 3", + "color": "9EC5FF", + "projects": { + "nodes": [{ + "id": "project_id_5", + "name": "Project 5" + }, { + "id": "project_id_6", + "name": "Project 6" + }] + }, + "members": { + "nodes": [{ + "id": "user_id_5", + "email": "user5@example.com" + }, { + "id": "user_id_6", + "email": "user6@example.com" + }] + } + }] + } + } + + user_groups = list(UserGroup.get_user_groups(self.client)) + + assert len(user_groups) == 3 + + # Check the attributes of the first user group + assert user_groups[0].id == "group_id_1" + assert user_groups[0].name == "Group 1" + assert user_groups[0].color == UserGroupColor.BLUE + assert len(user_groups[0].projects) == 2 + assert len(user_groups[0].users) == 2 + + # Check the attributes of the second user group + assert user_groups[1].id == "group_id_2" + assert user_groups[1].name == "Group 2" + assert user_groups[1].color == UserGroupColor.BLUE + assert len(user_groups[1].projects) == 2 + assert len(user_groups[1].users) == 2 + + # Check the attributes of the third user group + assert user_groups[2].id == "group_id_3" + assert user_groups[2].name == "Group 3" + assert user_groups[2].color == UserGroupColor.BLUE + assert len(user_groups[2].projects) == 2 + assert len(user_groups[2].users) == 2 + + +if __name__ == "__main__": + import subprocess + subprocess.call(["pytest", "-v", __file__]) diff --git a/pyproject.toml b/pyproject.toml index 238a6e247..ca5e7d0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dev-dependencies = [ "pytest-cov>=4.1.0", "pytest-xdist>=3.5.0", "toml-cli>=0.6.0", + "faker>=25.5.0", ] [tool.rye.workspace] diff --git a/requirements-dev.lock b/requirements-dev.lock index 288f35a94..60a73324c 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -31,7 +31,7 @@ bleach==6.1.0 # via nbconvert cachetools==5.3.3 # via google-auth -certifi==2024.2.2 +certifi==2024.6.2 # via pyproj # via requests charset-normalizer==3.3.2 @@ -41,7 +41,7 @@ click==8.1.7 # via typer commonmark==0.9.1 # via rich -coverage==7.5.3 +coverage==7.5.4 # via pytest-cov databooks==1.3.10 decopatch==1.4.10 @@ -59,7 +59,8 @@ execnet==2.1.1 # via pytest-xdist executing==2.0.1 # via stack-data -fastjsonschema==2.19.1 +faker==25.9.1 +fastjsonschema==2.20.0 # via nbformat geojson==3.1.0 # via labelbox @@ -67,18 +68,18 @@ gitdb==4.0.11 # via gitpython gitpython==3.1.43 # via databooks -google-api-core==2.19.0 +google-api-core==2.19.1 # via labelbox -google-auth==2.29.0 +google-auth==2.30.0 # via google-api-core -googleapis-common-protos==1.63.0 +googleapis-common-protos==1.63.2 # via google-api-core idna==3.7 # via requests imagesize==1.4.1 # via labelbox # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==7.2.1 # via jupyter-client # via nbconvert # via sphinx @@ -136,7 +137,7 @@ numpy==1.24.4 # via shapely opencv-python-headless==4.10.0.84 # via labelbox -packaging==24.0 +packaging==24.1 # via black # via nbconvert # via pytest @@ -164,11 +165,11 @@ platformdirs==4.2.2 # via yapf pluggy==1.5.0 # via pytest -prompt-toolkit==3.0.45 +prompt-toolkit==3.0.47 # via ipython -proto-plus==1.23.0 +proto-plus==1.24.0 # via google-api-core -protobuf==4.25.3 +protobuf==5.27.1 # via google-api-core # via googleapis-common-protos # via proto-plus @@ -181,10 +182,10 @@ pyasn1==0.6.0 # via rsa pyasn1-modules==0.4.0 # via google-auth -pydantic==2.7.2 +pydantic==2.7.4 # via databooks # via labelbox -pydantic-core==2.18.3 +pydantic-core==2.18.4 # via pydantic pygeotile==1.0.6 # via labelbox @@ -195,7 +196,7 @@ pygments==2.18.0 # via sphinx pyproj==3.5.0 # via labelbox -pytest==8.2.1 +pytest==8.2.2 # via pytest-cov # via pytest-rerunfailures # via pytest-snapshot @@ -206,6 +207,7 @@ pytest-rerunfailures==14.0 pytest-snapshot==0.9.0 pytest-xdist==3.6.1 python-dateutil==2.8.2 + # via faker # via jupyter-client # via labelbox # via pandas @@ -219,7 +221,7 @@ referencing==0.35.1 # via jsonschema-specifications regex==2024.5.15 # via toml-cli -requests==2.32.2 +requests==2.32.3 # via google-api-core # via labelbox # via sphinx @@ -282,7 +284,7 @@ tomli==2.0.1 # via yapf tomlkit==0.12.5 # via toml-cli -tornado==6.4 +tornado==6.4.1 # via jupyter-client tqdm==4.66.4 # via labelbox @@ -301,9 +303,9 @@ typer==0.12.3 # via toml-cli types-pillow==10.2.0.20240520 types-python-dateutil==2.9.0.20240316 -types-requests==2.32.0.20240523 +types-requests==2.32.0.20240622 types-tqdm==4.66.0.20240417 -typing-extensions==4.12.0 +typing-extensions==4.12.2 # via annotated-types # via black # via databooks @@ -317,7 +319,7 @@ typing-extensions==4.12.0 # via typer tzdata==2024.1 # via pandas -urllib3==2.2.1 +urllib3==2.2.2 # via requests # via types-requests wcwidth==0.2.13 @@ -326,6 +328,6 @@ webencodings==0.5.1 # via bleach # via tinycss2 yapf==0.40.2 -zipp==3.19.0 +zipp==3.19.2 # via importlib-metadata # via importlib-resources diff --git a/requirements.lock b/requirements.lock index a1b09c201..b052771f6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -17,7 +17,7 @@ babel==2.15.0 # via sphinx cachetools==5.3.3 # via google-auth -certifi==2024.2.2 +certifi==2024.6.2 # via pyproj # via requests charset-normalizer==3.3.2 @@ -27,18 +27,18 @@ docutils==0.20.1 # via sphinx-rtd-theme geojson==3.1.0 # via labelbox -google-api-core==2.19.0 +google-api-core==2.19.1 # via labelbox -google-auth==2.29.0 +google-auth==2.30.0 # via google-api-core -googleapis-common-protos==1.63.0 +googleapis-common-protos==1.63.2 # via google-api-core idna==3.7 # via requests imagesize==1.4.1 # via labelbox # via sphinx -importlib-metadata==7.1.0 +importlib-metadata==7.2.1 # via sphinx # via typeguard jinja2==3.1.4 @@ -51,13 +51,13 @@ numpy==1.24.4 # via shapely opencv-python-headless==4.10.0.84 # via labelbox -packaging==24.0 +packaging==24.1 # via sphinx pillow==10.3.0 # via labelbox -proto-plus==1.23.0 +proto-plus==1.24.0 # via google-api-core -protobuf==4.25.3 +protobuf==5.27.1 # via google-api-core # via googleapis-common-protos # via proto-plus @@ -66,9 +66,9 @@ pyasn1==0.6.0 # via rsa pyasn1-modules==0.4.0 # via google-auth -pydantic==2.7.2 +pydantic==2.7.4 # via labelbox -pydantic-core==2.18.3 +pydantic-core==2.18.4 # via pydantic pygeotile==1.0.6 # via labelbox @@ -80,7 +80,7 @@ python-dateutil==2.8.2 # via labelbox pytz==2024.1 # via babel -requests==2.32.2 +requests==2.32.3 # via google-api-core # via labelbox # via sphinx @@ -117,13 +117,13 @@ tqdm==4.66.4 # via labelbox typeguard==4.3.0 # via labelbox -typing-extensions==4.12.0 +typing-extensions==4.12.2 # via annotated-types # via labelbox # via pydantic # via pydantic-core # via typeguard -urllib3==2.2.1 +urllib3==2.2.2 # via requests -zipp==3.19.0 +zipp==3.19.2 # via importlib-metadata