Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Remove custom Result type and add returns library #471

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,003 changes: 780 additions & 223 deletions server/poetry.lock

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ python-dotenv = "^0.18"
python-multipart = "^0.0.5"
h5py = ">=2.10,<3"
scipy = "^1.6.3"
returns = "^0.16.0"
wemake-python-styleguide = "^0.15.3"

[tool.poetry.dev-dependencies]
pytest = "*"
Expand All @@ -80,12 +82,15 @@ max-line-length = '120'
line-length = 120
target-version = ['py38']

[tool.mypy]
python_version = 3.8
plugins = [
'returns.contrib.mypy.returns_plugin'
]

[build-system]
requires = [
"poetry>=0.12",
"setuptools>=45.2.0"
]
build-backend = "poetry.masonry.api"

[mypy]
python_version = 3.8
10 changes: 6 additions & 4 deletions server/scopeserver/api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from typing import Generator, Optional

from fastapi import Cookie, Depends, HTTPException, status
from returns.io import IOResult
from returns.unsafe import unsafe_perform_io
from sqlalchemy.orm import Session

from scopeserver import crud, models, schemas
Expand All @@ -22,7 +24,7 @@ def get_current_user(database: Session = Depends(get_db), user_id: Optional[int]
"Provide access to the User currently accessing the API."
unknown_user = HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Unknown user ID",
detail=f"Unknown user ID: {user_id}",
)

no_user_id = HTTPException(
Expand All @@ -32,9 +34,9 @@ def get_current_user(database: Session = Depends(get_db), user_id: Optional[int]

if user_id is not None:
user = crud.get_user(database, user=schemas.User(id=user_id))
if not user:
raise unknown_user
if isinstance(user, IOResult.success_type):
return unsafe_perform_io(user.unwrap())

return user
raise unknown_user

raise no_user_id
35 changes: 22 additions & 13 deletions server/scopeserver/api/v1/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pathlib import Path

from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
from returns.io import IOResult
from returns.unsafe import unsafe_perform_io
from sqlalchemy.orm import Session

from scopeserver import crud, models, schemas
Expand All @@ -21,7 +23,7 @@ async def my_projects(
current_user: models.User = Depends(deps.get_current_user),
):
"""Retrieve all projects for the current user."""
return crud.get_projects(db=db, user_id=current_user.id)
return unsafe_perform_io(crud.get_projects(db=db, user_id=current_user.id))


@router.get("/datasets", summary="Get all datasets in a project.", response_model=List[schemas.Dataset])
Expand All @@ -32,9 +34,9 @@ async def datasets(
current_user: models.User = Depends(deps.get_current_user),
):
"""Retrieve all datasets in a given project."""
found_project = crud.get_project(db, user_id=current_user.id, project_uuid=project)
if found_project:
return found_project.datasets
found_project: IOResult[models.Project, str] = crud.get_project(db, user_id=current_user.id, project_uuid=project)
if isinstance(found_project, IOResult.success_type):
return unsafe_perform_io(found_project.unwrap()).datasets

raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No project with id: {project} exists.")

Expand All @@ -49,7 +51,7 @@ async def users(
current_user: models.User = Depends(deps.get_current_user),
):
"""Retrieve all users on a given project."""
all_users = crud.get_users_in_project(db, project_uuid=project)
all_users = unsafe_perform_io(crud.get_users_in_project(db, project_uuid=project))
if current_user.id in (u.user for u in all_users):
return all_users

Expand All @@ -64,7 +66,7 @@ async def new_project(
current_user: models.User = Depends(deps.get_current_user),
):
"""Create a new project."""
project = crud.create_project(db, current_user.id, name)
project = unsafe_perform_io(crud.create_project(db, current_user.id, name))
(settings.DATA_PATH / Path(project.uuid)).mkdir()
return project

Expand All @@ -81,18 +83,22 @@ async def add_user(
user = crud.get_user(db, schemas.User(id=user_id))
found_project = crud.get_project(db, project_uuid=project, user_id=current_user.id)

if user is not None and found_project is not None:
project_existing_users = [existing_user.id for existing_user in found_project.users]
if user.id not in project_existing_users:
crud.add_user_to_project(db, user_id=user.id, project_id=found_project.id)
if isinstance(user, IOResult.success_type) and isinstance(found_project, IOResult.success_type):
_user = unsafe_perform_io(user.unwrap())
_project = unsafe_perform_io(found_project.unwrap())
project_existing_users = [existing_user.id for existing_user in _project.users]
if _user.id not in project_existing_users:
unsafe_perform_io(crud.add_user_to_project(db, user_id=_user.id, project_id=_project.id))
return Response(status_code=status.HTTP_200_OK)

raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User already in project",
)

raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="User or project does not exist")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="User or project does not exist or does not have permission"
)


@router.post("/dataset", summary="", response_model=schemas.Dataset)
Expand All @@ -106,7 +112,8 @@ async def add_dataset(
):
"""Add a dataset to a project."""
found_project = crud.get_project(db, project_uuid=project, user_id=current_user.id)
if found_project:
if isinstance(found_project, IOResult.success_type):
_project = unsafe_perform_io(found_project.unwrap())
size = 0
with (settings.DATA_PATH / Path(project) / Path(uploadfile.filename)).open(mode="wb") as datafile:
data = await uploadfile.read()
Expand All @@ -115,7 +122,9 @@ async def add_dataset(
size = len(data)
datafile.write(data)

return crud.create_dataset(db, name=name, filename=uploadfile.filename, project=found_project, size=size)
return unsafe_perform_io(
crud.create_dataset(db, name=name, filename=uploadfile.filename, project=_project, size=size)
)

raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="You are not in this project")

Expand Down
3 changes: 2 additions & 1 deletion server/scopeserver/api/v1/users.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
" API endpoints related to managing users. "

from fastapi import APIRouter, Depends
from returns.unsafe import unsafe_perform_io
from sqlalchemy.orm import Session

from scopeserver import crud, schemas
Expand All @@ -14,4 +15,4 @@
@router.post("/new", summary="Create a new user.", response_model=schemas.UserResponse)
async def new_user(db: Session = Depends(deps.get_db)):
"""Create a new user."""
return crud.create_user(db=db)
return unsafe_perform_io(crud.create_user(db=db))
52 changes: 29 additions & 23 deletions server/scopeserver/crud.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
" Provides low-level Create, Read, Update, and Delete functions for API resources. "

from typing import List, Optional
from typing import List
from datetime import datetime
from uuid import uuid4

from returns.io import IO, IOFailure, IOResult, IOSuccess
from sqlalchemy.orm import Session

from scopeserver import models, schemas
Expand All @@ -13,33 +14,34 @@
# Projects


def get_projects(db: Session, user_id: int) -> List[models.Project]:
def get_projects(db: Session, user_id: int) -> IO[List[models.Project]]:
"Read all projects for a given user."
user = db.query(models.User).filter(models.User.id == user_id).first()
return user.projects if user else []
return IO(user.projects) if user else IO([])


def get_project(db: Session, project_uuid: str, user_id: int) -> Optional[models.Project]:
def get_project(db: Session, project_uuid: str, user_id: int) -> IOResult[models.Project, str]:
"Get a specified project if it is accessible by a specified user."
project = db.query(models.Project).filter(models.Project.uuid == project_uuid).first()
mapping = (
db.query(models.ProjectMapping)
.filter(models.ProjectMapping.project == project.id, models.ProjectMapping.user == user_id)
.first()
)
if mapping:
return project
if project:
mapping = (
db.query(models.ProjectMapping)
.filter(models.ProjectMapping.project == project.id, models.ProjectMapping.user == user_id)
.first()
)
if mapping:
return IOSuccess(project)

return None
return IOFailure("Not a project you can access")


def get_users_in_project(db: Session, project_uuid: str) -> List[models.User]:
def get_users_in_project(db: Session, project_uuid: str) -> IO[List[models.User]]:
"Get all users in a given project."
project = db.query(models.Project).filter(models.Project.uuid == project_uuid).first()
return db.query(models.ProjectMapping).filter(models.ProjectMapping.project == project.id).all()
return IO(db.query(models.ProjectMapping).filter(models.ProjectMapping.project == project.id).all())


def create_project(db: Session, user_id: int, name: str) -> models.Project:
def create_project(db: Session, user_id: int, name: str) -> IO[models.Project]:
"Create a new project."
new_project = models.Project(name=name, uuid=str(uuid4()), created=datetime.now(), size=0)
db.add(new_project)
Expand All @@ -48,36 +50,40 @@ def create_project(db: Session, user_id: int, name: str) -> models.Project:

db.add(models.ProjectMapping(project=new_project.id, user=user_id))
db.commit()
return new_project
return IO(new_project)


def add_user_to_project(db: Session, user_id: int, project_id: int):
def add_user_to_project(db: Session, user_id: int, project_id: int) -> IO[None]:
"Give a specified user_id access to a given project."
db.add(models.ProjectMapping(project=project_id, user=user_id))
db.commit()
return IO(None)


# Users


def get_user(db: Session, user: schemas.User) -> Optional[models.User]:
def get_user(db: Session, user: schemas.User) -> IOResult[models.User, str]:
"Read a user from the database."
return db.query(models.User).filter(models.User.id == user.id).first()
if (user := db.query(models.User).filter(models.User.id == user.id).first()) is not None:
return IOSuccess(user)

return IOFailure("User not found")


def create_user(db: Session) -> models.User:
def create_user(db: Session) -> IO[models.User]:
"Create a new user."
new_user = models.User(created=datetime.now())
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user
return IO(new_user)


# Datasets


def create_dataset(db: Session, name: str, filename: str, project: models.Project, size: int) -> models.Dataset:
def create_dataset(db: Session, name: str, filename: str, project: models.Project, size: int) -> IO[models.Dataset]:
"Create a new dataset."
new_dataset = models.Dataset(
name=name,
Expand All @@ -90,4 +96,4 @@ def create_dataset(db: Session, name: str, filename: str, project: models.Projec
project.size += size
db.commit()
db.refresh(new_dataset)
return new_dataset
return IO(new_dataset)
Loading