Skip to content

Commit

Permalink
implement 'list_projects' with the org access feature (#825)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexey-cord-tech authored Dec 16, 2024
1 parent dc9c7fc commit 3897f76
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 59 deletions.
1 change: 1 addition & 0 deletions encord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,7 @@ def list_project_datasets(self, project_hash: UUID) -> Iterable[ProjectDataset]:
.results
)

@deprecated("0.1.102", alternative="encord.ontology.Ontology class")
def get_project_ontology(self) -> LegacyOntology:
project = self.get_project()
ontology = project["editor_ontology"]
Expand Down
28 changes: 16 additions & 12 deletions encord/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
---
"""

import dataclasses
import datetime
from typing import Iterable, List, Optional, Union
from uuid import UUID
Expand Down Expand Up @@ -94,7 +93,8 @@ def refetch_data(self) -> None:
The Ontology class will only fetch its properties once. Use this function if you suspect the state of those
properties to be dirty.
"""
self._ontology_instance = self._get_ontology()
cls = type(self)
self._ontology_instance = cls._fetch_ontology(self.api_client, self.ontology_hash)

def save(self) -> None:
"""
Expand All @@ -118,15 +118,6 @@ def save(self) -> None:
payload=payload,
)

def _get_ontology(self) -> OrmOntology:
ontology_model = self.api_client.get(
f"/ontologies/{self._ontology_instance.ontology_hash}",
params=None,
result_type=OntologyWithUserRole,
)

return self._legacy_orm_from_api_payload(ontology_model)

def list_groups(self) -> Iterable[OntologyGroup]:
"""
List all groups that have access to a particular ontology.
Expand Down Expand Up @@ -189,4 +180,17 @@ def _from_api_payload(
ontology_with_user_role: OntologyWithUserRole,
api_client: ApiClient,
) -> "Ontology":
return Ontology(Ontology._legacy_orm_from_api_payload(ontology_with_user_role), api_client)
return Ontology(
Ontology._legacy_orm_from_api_payload(ontology_with_user_role),
api_client,
)

@classmethod
def _fetch_ontology(cls, api_client: ApiClient, ontology_hash: str) -> OrmOntology:
ontology_model = api_client.get(
f"/ontologies/{ontology_hash}",
params=None,
result_type=OntologyWithUserRole,
)

return cls._legacy_orm_from_api_payload(ontology_model)
37 changes: 0 additions & 37 deletions encord/orm/base_orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,43 +83,6 @@ def __delattr__(self, name):
else:
super().__delattr__(name)

@staticmethod
def from_db_row(row, db_field):
"""
Static method for conveniently converting db row to client object.
:param row:
:param db_field:
:return:
"""
return {attribute: row[i] for i, attribute in enumerate(db_field)}

def to_dic(self, time_str: bool = True):
"""
Conveniently set client object as dict.
Only considers the dict items, no other object attr will be counted
Args:
time_str: if set to True, will convert datetime field
to str with format %Y-%m-%d %H:%M:%S.
If False, will keep the original datetime type.
Default will be True.
"""
res = {}
for k, v in self.items():
if isinstance(v, datetime.datetime) and time_str:
v = v.strftime("%Y-%m-%d %H:%M:%S")
elif isinstance(v, dict):
v = json.dumps(v)
res[k] = v

return res

def updatable_fields(self):
for k, v in self.items():
if k not in self.NON_UPDATABLE_FIELDS and v is not None:
yield k, v


class BaseListORM(list):
"""A wrapper for a list of objects of a specific ORM."""
Expand Down
34 changes: 32 additions & 2 deletions encord/orm/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from typing import Any, Dict, List, Optional, Tuple, Union
from uuid import UUID

from encord.exceptions import WrongProjectTypeError
from encord.orm import base_orm
from encord.orm.analytics import CamelStrEnum
from encord.orm.base_dto import BaseDTO
from encord.orm.workflow import Workflow
from encord.utilities.project_user import ProjectUserRole


class Project(base_orm.BaseORM):
Expand Down Expand Up @@ -91,7 +93,7 @@ def get_labels_list(self) -> List[Optional[str]]:
label_rows = project.get_label_rows(created_labels_list, get_signed_url=False)
"""
labels = self.to_dic().get("label_rows", [])
labels = self.label_rows() or []
return [label.get("label_hash") for label in labels]

@property
Expand Down Expand Up @@ -136,7 +138,15 @@ def last_edited_at(self) -> datetime.datetime:

@property
def workflow_manager_uuid(self) -> UUID:
return self["workflow_manager_uuid"]
"""
Accessing this property will raise a `WrongProjectTypeError` if the project is not a workflow project.
"""
try:
return self["workflow_manager_uuid"]
except KeyError as e:
raise WrongProjectTypeError(
"This project is not a workflow project, workflow_manager_uuid is not available."
) from e


class ProjectCopy:
Expand Down Expand Up @@ -302,6 +312,10 @@ class ProjectDTO(BaseDTO):
created_at: datetime.datetime
last_edited_at: datetime.datetime
ontology_hash: str
editor_ontology: Dict[str, Any]
user_role: Optional[ProjectUserRole] = None
source_projects: Optional[List[str]] = None
workflow_manager_uuid: Optional[UUID] = None
workflow: Optional[Workflow] = None


Expand Down Expand Up @@ -360,3 +374,19 @@ class CvatImportGetResultResponse(BaseDTO):
status: CvatImportGetResultLongPollingStatus
project_uuid: Optional[UUID] = None
issues: Optional[Dict] = None


class ProjectFilterParams(BaseDTO):
"""
Filter parameters for the /v2/public/projects endpoint
"""

title_eq: Optional[str] = None
title_like: Optional[str] = None
desc_eq: Optional[str] = None
desc_like: Optional[str] = None
created_before: Optional[Union[str, datetime.datetime]] = None
created_after: Optional[Union[str, datetime.datetime]] = None
edited_before: Optional[Union[str, datetime.datetime]] = None
edited_after: Optional[Union[str, datetime.datetime]] = None
include_org_access: bool = False
32 changes: 29 additions & 3 deletions encord/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ def __init__(
self,
client: EncordClientProject,
project_instance: ProjectDTO,
ontology: Ontology,
ontology: Optional[Ontology],
api_client: ApiClient,
):
self._client = client
self._project_instance = project_instance
self._ontology = ontology
self._ontology_internal = ontology
self._api_client = api_client

if project_instance.workflow:
self._workflow = Workflow(api_client, project_instance.project_hash, project_instance.workflow)
Expand Down Expand Up @@ -126,7 +127,7 @@ def ontology_hash(self) -> str:
"""
Get the ontology hash of the project's ontology.
"""
return self._ontology.ontology_hash
return self._project_instance.ontology_hash

@property
def ontology_structure(self) -> OntologyStructure:
Expand All @@ -135,6 +136,23 @@ def ontology_structure(self) -> OntologyStructure:
"""
return self._ontology.structure

@property
def user_role(self) -> Optional[ProjectUserRole]:
"""
Get the current user's role in the project.
This may return `None` if the user is an organisational admin and has accessed the project e.g. using
`include_org_access=True` of :meth:`encord.user_client.UserClient.list_projects`.
"""
return self._project_instance.user_role

@property
def source_projects(self) -> Optional[List[str]]:
"""
Get the source projects for a Training project. Returns None for non-Training projects.
"""
return self._project_instance.source_projects

@property
@deprecated(version="0.1.117", alternative=".list_datasets")
def datasets(self) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -201,6 +219,14 @@ def workflow(self) -> Workflow:
), "project.workflow property only available for workflow projects"
return self._workflow

@property
def _ontology(self) -> Ontology:
if self._ontology_internal is None:
self._ontology_internal = Ontology(
Ontology._fetch_ontology(self._api_client, self.ontology_hash), self._api_client
) # lazy loading
return self._ontology_internal

def list_label_rows_v2(
self,
data_hashes: Optional[Union[List[str], List[UUID]]] = None,
Expand Down
57 changes: 52 additions & 5 deletions encord/user_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from __future__ import annotations

import base64
import dataclasses
import logging
import time
import uuid
Expand Down Expand Up @@ -91,6 +90,8 @@
CvatImportStartPayload,
CvatReviewMode,
ManualReviewWorkflowSettings,
ProjectDTO,
ProjectFilterParams,
ProjectWorkflowSettings,
ProjectWorkflowType,
ReviewMode,
Expand Down Expand Up @@ -169,7 +170,7 @@ def get_dataset(
orm_dataset = client.get_dataset()
return Dataset(client, orm_dataset)

def get_project(self, project_hash: str | UUID) -> Project:
def get_project(self, project_hash: Union[str, UUID]) -> Project:
"""
Get the Project class to access project fields and manipulate a project.
Expand Down Expand Up @@ -347,7 +348,7 @@ def create_with_ssh_private_key(
ssh_private_key: Optional[str] = None,
password: Optional[str] = None,
requests_settings: RequestsSettings = DEFAULT_REQUESTS_SETTINGS,
ssh_private_key_path: Optional[str | Path] = None,
ssh_private_key_path: Optional[Union[str, Path]] = None,
**kwargs,
) -> EncordUserClient:
"""
Expand Down Expand Up @@ -413,13 +414,59 @@ def get_projects(
edited_after: optional last modification date filter, 'greater'
Returns:
list of (role, projects) pairs for Project matching filter conditions.
list of Projects matching filter conditions, with the roles that the current user has on them. Each item
is a dictionary with `"project"` and `"user_role"` keys.
"""
properties_filter = self.__validate_filter(locals())
# a hack to be able to share validation code without too much c&p
data = self._querier.get_multiple(ProjectWithUserRole, payload={"filter": properties_filter})
return [{"project": OrmProject(p.project), "user_role": ProjectUserRole(p.user_role)} for p in data]

def list_projects(
self,
title_eq: Optional[str] = None,
title_like: Optional[str] = None,
desc_eq: Optional[str] = None,
desc_like: Optional[str] = None,
created_before: Optional[Union[str, datetime]] = None,
created_after: Optional[Union[str, datetime]] = None,
edited_before: Optional[Union[str, datetime]] = None,
edited_after: Optional[Union[str, datetime]] = None,
include_org_access: bool = False,
) -> Iterable[Project]:
"""
List either all (if called with no arguments) or matching projects the user has access to.
Args:
title_eq: optional exact title filter
title_like: optional fuzzy title filter; SQL syntax
desc_eq: optional exact description filter
desc_like: optional fuzzy description filter; SQL syntax
created_before: optional creation date filter, 'less'
created_after: optional creation date filter, 'greater'
edited_before: optional last modification date filter, 'less'
edited_after: optional last modification date filter, 'greater'
include_org_access: if set to true and the calling user is the organization admin, the
method will return all ontologies in the organization.
Returns:
list of Projects matching filter conditions, as :class:`~encord.project.Project` instances.
"""
properties_filter = ProjectFilterParams.from_dict(self.__validate_filter(locals()))
properties_filter.include_org_access = include_org_access
page = self._api_client.get("projects", params=properties_filter, result_type=Page[ProjectDTO])

for row in page.results:
querier = Querier(self._config.config, resource_type=TYPE_PROJECT, resource_id=str(row.project_hash))
client = EncordClientProject(querier=querier, config=self._config.config, api_client=self._api_client)

yield Project(
client=client,
project_instance=row,
ontology=None, # lazy-load
api_client=self._api_client,
)

def create_project(
self,
project_title: str,
Expand Down Expand Up @@ -1389,7 +1436,7 @@ def get_collection(self, collection_uuid: Union[str, UUID]) -> Collection:
def list_collections(
self,
top_level_folder_uuid: Union[str, UUID, None] = None,
collection_uuids: List[str | UUID] | None = None,
collection_uuids: Optional[List[Union[str, UUID]]] = None,
page_size: Optional[int] = None,
) -> Iterator[Collection]:
"""
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def project(
created_at=datetime.now(),
last_edited_at=datetime.now(),
ontology_hash="dummy-ontology-hash",
editor_ontology=ONTOLOGY_BLURB,
workflow=None,
)

Expand Down
1 change: 1 addition & 0 deletions tests/test_user_client_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
created_at=datetime.now(),
last_edited_at=datetime.now(),
ontology_hash=str(ONTOLOGY_UUID),
editor_ontology=ontology_orm.editor,
workflow=None,
)

Expand Down

0 comments on commit 3897f76

Please sign in to comment.