Skip to content

Commit

Permalink
Add sharing methods to APIClient (#194)
Browse files Browse the repository at this point in the history
* Add sharing methods to APIClient

* added more methods

* DRY refactor

* changelog
  • Loading branch information
Matt Kafonek authored Oct 23, 2023
1 parent bba1d48 commit 738ccce
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For pre-1.0 releases, see [0.0.35 Changelog](https://github.com/noteable-io/origami/blob/0.0.35/CHANGELOG.md)

## [Unreleased]
### Added
- Programmatically share access to Spaces, Projects, and Notebooks/Files by email and access level. E.g. `await api_client.share_file(file_id, email, 'viewer')`

### Changed
- Removed `RuntimeError` in RTUClient catastrophic failure, top level applications (e.g. PA, Origamist) should define that behavior

Expand Down
104 changes: 104 additions & 0 deletions origami/clients/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import enum
import logging
import os
import uuid
Expand All @@ -18,6 +19,27 @@
logger = logging.getLogger(__name__)


class AccessLevel(enum.Enum):
owner = "role:owner"
contributor = "role:contributor"
commenter = "role:commenter"
viewer = "role:viewer"
executor = "role:executor"

@classmethod
def from_str(cls, s: str):
for level in cls:
if level.name == s:
return level
raise ValueError(f"Invalid access level {s}")


class Resource(enum.Enum):
spaces = "spaces"
projects = "projects"
files = "files"


class APIClient:
def __init__(
self,
Expand Down Expand Up @@ -66,6 +88,46 @@ async def user_info(self) -> User:
self.add_tags_and_contextvars(user_id=str(user.id))
return user

async def share_resource(
self, resource: Resource, resource_id: uuid.UUID, email: str, level: Union[str, AccessLevel]
) -> int:
"""
Add another User as a collaborator to a Resource.
"""
user_lookup_endpoint = f"/{resource.value}/{resource_id}/shareable-users"
user_lookup_params = {"q": email}
user_lookup_resp = await self.client.get(user_lookup_endpoint, params=user_lookup_params)
user_lookup_resp.raise_for_status()
users = user_lookup_resp.json()["data"]

if isinstance(level, str):
level = AccessLevel.from_str(level)
share_endpoint = f"/{resource.value}/{resource_id}/users"
for item in users:
user_id = item["id"]
share_body = {"access_level": level.value, "user_id": user_id}
share_resp = await self.client.put(share_endpoint, json=share_body)
share_resp.raise_for_status()
return len(users)

async def unshare_resource(self, resource: Resource, resource_id: uuid.UUID, email: str) -> int:
"""
Remove access to a Resource for a User
"""
# Need to look this up still to go from email to user-id
user_lookup_endpoint = f"/{resource.value}/{resource_id}/shareable-users"
user_lookup_params = {"q": email}
user_lookup_resp = await self.client.get(user_lookup_endpoint, params=user_lookup_params)
user_lookup_resp.raise_for_status()
users = user_lookup_resp.json()["data"]

for item in users:
user_id = item["id"]
unshare_endpoint = f"/{resource.value}/{resource_id}/users/{user_id}"
unshare_resp = await self.client.delete(unshare_endpoint)
unshare_resp.raise_for_status()
return len(users)

# Spaces are collections of Projects. Some "scoped" resources such as Secrets and Datasources
# can also be attached to a Space and made available to all users of that Space.
async def create_space(self, name: str, description: Optional[str] = None) -> Space:
Expand Down Expand Up @@ -100,6 +162,20 @@ async def list_space_projects(self, space_id: uuid.UUID) -> List[Project]:
projects = [Project.parse_obj(project) for project in resp.json()]
return projects

async def share_space(
self, space_id: uuid.UUID, email: str, level: Union[str, AccessLevel]
) -> int:
"""
Add another user as a collaborator to a Space.
"""
return await self.share_resource(Resource.spaces, space_id, email, level)

async def unshare_space(self, space_id: uuid.UUID, email: str) -> int:
"""
Remove access to a Space for a User
"""
return await self.unshare_resource(Resource.spaces, space_id, email)

# Projects are collections of Files, including Notebooks. When a Kernel is launched for a
# Notebook, all Files in the Project are volume mounted into the Kernel container at startup.
async def create_project(
Expand Down Expand Up @@ -138,6 +214,20 @@ async def delete_project(self, project_id: uuid.UUID) -> Project:
project = Project.parse_obj(resp.json())
return project

async def share_project(
self, project_id: uuid.UUID, email: str, level: Union[str, AccessLevel]
) -> int:
"""
Add another User as a collaborator to a Project.
"""
return await self.share_resource(Resource.projects, project_id, email, level)

async def unshare_project(self, project_id: uuid.UUID, email: str) -> int:
"""
Remove access to a Project for a User
"""
return await self.unshare_resource(Resource.projects, project_id, email)

async def list_project_files(self, project_id: uuid.UUID) -> List[File]:
"""List all Files in a Project. Files do not have presigned download urls included here."""
self.add_tags_and_contextvars(project_id=str(project_id))
Expand Down Expand Up @@ -265,6 +355,20 @@ async def delete_file(self, file_id: uuid.UUID) -> File:
file = File.parse_obj(resp.json())
return file

async def share_file(
self, file_id: uuid.UUID, email: str, level: Union[str, AccessLevel]
) -> int:
"""
Add another User as a collaborator to a Notebook or File.
"""
return await self.share_resource(Resource.files, file_id, email, level)

async def unshare_file(self, file_id: uuid.UUID, email: str) -> int:
"""
Remove access to a Notebook or File for a User
"""
return await self.unshare_resource(Resource.files, file_id, email)

async def get_datasources_for_notebook(self, file_id: uuid.UUID) -> List[DataSource]:
"""Return a list of Datasources that can be used in SQL cells within a Notebook"""
self.add_tags_and_contextvars(file_id=str(file_id))
Expand Down

0 comments on commit 738ccce

Please sign in to comment.