Skip to content

Commit

Permalink
Add create route to corpus entity (#249)
Browse files Browse the repository at this point in the history
* Update CorpusData to handle entity specific taxonomies

* Add corpora get, all, search and update endpoints

* Add searching within corpus_text

* Fix corpus update

* Remove rogue debug

* Add search tests for /corpora

* Add get tests for /corpora

* Add all tests for /corpora

* Remove debug

* Add corpora info to setup_db

* Change function signatures from doc to corpus

* Linting fixes

* Bump to 2.17.8

* Use validate instead of verify

* Fix repo function name

* Remove unused code

* Bump ruff

* Remove created and last modified for now

* Add corpus auth

* Ingest auth access should be admin not user

* Make test tokens non admin

* Driveby fix tests for ingest

* Make user accounts match test tokens

* Move ingest business logic out of router

* Add todos for timestamps

* Revert "Ingest auth access should be admin not user"

This reverts commit 347a6eb.

* Add tests for checking user auth

* Update article based on next word starting vowel

* Update test_ingest.py

* Fix import path

* Bump db client

* Add create route for corpus entity

* Update tests

* Make corpus routes for supers only

* Remove

* Allow update of corpus type description

* Bump to 2.17.13

* Add corpus type description

* Fix tests

* Update pyproject.toml
  • Loading branch information
katybaulch authored Nov 18, 2024
1 parent 8f95f44 commit 81e159c
Show file tree
Hide file tree
Showing 16 changed files with 781 additions and 81 deletions.
26 changes: 25 additions & 1 deletion app/api/api_v1/routers/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
validate_query_params,
)
from app.errors import AuthorisationError, RepositoryError, ValidationError
from app.model.corpus import CorpusReadDTO, CorpusWriteDTO
from app.model.corpus import CorpusCreateDTO, CorpusReadDTO, CorpusWriteDTO
from app.service import corpus as corpus_service

corpora_router = r = APIRouter()
Expand Down Expand Up @@ -139,3 +139,27 @@ async def update_corpus(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)

return corpus


@r.post("/corpora", response_model=str, status_code=status.HTTP_201_CREATED)
async def create_corpus(request: Request, new_corpus: CorpusCreateDTO) -> str:
"""Create a specific corpus given the import id.
:param Request request: Request object.
:param CorpusCreateDTO new_corpus: New corpus data object.
:raises HTTPException: If there is an error raised during validation
or during adding the corpus to the db.
:return str: returns the import id of the newly created corpus.
"""
try:
corpus_id = corpus_service.create(new_corpus, request.state.user)
except AuthorisationError as e:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=e.message)
except ValidationError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=e.message)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

return corpus_id
6 changes: 3 additions & 3 deletions app/model/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ class AuthEndpoint(str, enum.Enum):
},
# Corpus
AuthEndpoint.CORPUS: {
AuthOperation.CREATE: AuthAccess.ADMIN,
AuthOperation.READ: AuthAccess.ADMIN,
AuthOperation.UPDATE: AuthAccess.ADMIN,
AuthOperation.CREATE: AuthAccess.SUPER,
AuthOperation.READ: AuthAccess.SUPER,
AuthOperation.UPDATE: AuthAccess.SUPER,
},
}
12 changes: 11 additions & 1 deletion app/model/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,15 @@ class CorpusWriteDTO(BaseModel):
corpus_text: Optional[str]
corpus_image_url: Optional[str]

corpus_type_name: str
corpus_type_description: str


class CorpusCreateDTO(BaseModel):
"""Representation of a Corpus."""

title: str
description: str
corpus_text: Optional[str]
corpus_image_url: Optional[str]
organisation_id: int
corpus_type_name: str
50 changes: 45 additions & 5 deletions app/repository/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
from typing import Optional, Union, cast

from db_client.models.organisation import Corpus, CorpusType, Organisation
from db_client.models.organisation.counters import CountedEntity
from sqlalchemy import and_, asc, or_
from sqlalchemy import update as db_update
from sqlalchemy.exc import NoResultFound, OperationalError
from sqlalchemy.orm import Query, Session
from sqlalchemy_utils import escape_like

from app.errors import RepositoryError
from app.model.corpus import CorpusReadDTO, CorpusWriteDTO
from app.model.corpus import CorpusCreateDTO, CorpusReadDTO, CorpusWriteDTO
from app.repository.helpers import generate_import_id

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,6 +66,19 @@ def get_corpus_org_id(db: Session, corpus_id: str) -> Optional[int]:
return db.query(Corpus.organisation_id).filter_by(import_id=corpus_id).scalar()


def is_corpus_type_name_valid(db: Session, corpus_type_name: str) -> bool:
"""Check whether a corpus type name exists in the DB.
:param Session db: The DB session to connect to.
:param str corpus_type_name: The corpus type name we want to search
for.
:return bool: Whether the given corpus type exists in the db.
"""
return bool(
db.query(CorpusType.name).filter_by(name=corpus_type_name).scalar() is not None
)


def verify_corpus_exists(db: Session, corpus_id: str) -> bool:
"""Validate whether a corpus with the given ID exists in the DB.
Expand All @@ -72,6 +87,7 @@ def verify_corpus_exists(db: Session, corpus_id: str) -> bool:
:return bool: Return whether or not the corpus exists in the DB.
"""
corpora = [corpus[0] for corpus in db.query(Corpus.import_id).distinct().all()]
print(corpora)
return bool(corpus_id in corpora)


Expand Down Expand Up @@ -189,7 +205,6 @@ def update(db: Session, import_id: str, corpus: CorpusWriteDTO) -> bool:
return False

# Check what has changed.
ct_name_has_changed = original_corpus_type.name != new_values["corpus_type_name"]
ct_description_has_changed = (
original_corpus_type.name != new_values["corpus_type_description"]
)
Expand All @@ -202,7 +217,6 @@ def update(db: Session, import_id: str, corpus: CorpusWriteDTO) -> bool:

if not any(
[
ct_name_has_changed,
ct_description_has_changed,
title_has_changed,
description_has_changed,
Expand All @@ -216,12 +230,11 @@ def update(db: Session, import_id: str, corpus: CorpusWriteDTO) -> bool:
commands = []

# Update logic to only perform update if not idempotent.
if ct_name_has_changed or ct_description_has_changed:
if ct_description_has_changed:
commands.append(
db_update(CorpusType)
.where(CorpusType.name == original_corpus_type.name)
.values(
name=new_values["corpus_type_name"],
description=new_values["corpus_type_description"],
)
)
Expand Down Expand Up @@ -258,3 +271,30 @@ def update(db: Session, import_id: str, corpus: CorpusWriteDTO) -> bool:
raise RepositoryError(msg)

return True


def create(db: Session, corpus: CorpusCreateDTO) -> str:
"""Create a new corpus.
:param db Session: the database connection
:param CorpusCreateDTO corpus: the values for the new corpus
:return str: The ID of the created corpus.
"""
try:
import_id = generate_import_id(db, CountedEntity.Corpus, corpus.organisation_id)
new_corpus = Corpus(
import_id=import_id,
title=corpus.title,
description=corpus.description or "TBD",
corpus_text=corpus.corpus_text,
corpus_image_url=corpus.corpus_image_url,
organisation_id=corpus.organisation_id,
corpus_type_name=corpus.corpus_type_name,
)
db.add(new_corpus)
db.flush()
except Exception as e:
_LOGGER.exception("Error trying to create Corpus")
raise RepositoryError(e)

return cast(str, new_corpus.import_id)
44 changes: 40 additions & 4 deletions app/service/corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import app.clients.db.session as db_session
import app.repository.corpus as corpus_repo
import app.repository.organisation as org_repo
from app.errors import RepositoryError, ValidationError
from app.model.corpus import CorpusReadDTO, CorpusWriteDTO
from app.model.corpus import CorpusCreateDTO, CorpusReadDTO, CorpusWriteDTO
from app.model.user import UserContext
from app.service import app_user, id

Expand Down Expand Up @@ -149,9 +150,6 @@ def update(
if original_corpus is None:
return None

entity_org_id: int = get_corpus_org_id(import_id, db)
app_user.raise_if_unauthorised_to_make_changes(user, entity_org_id, import_id)

try:
if corpus_repo.update(db, import_id, corpus):
db.commit()
Expand All @@ -161,3 +159,41 @@ def update(
db.rollback()
raise e
return get(import_id)


@db_session.with_database()
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def create(
corpus: CorpusCreateDTO,
user: UserContext,
db: Optional[Session] = None,
) -> str:
"""Create a new Corpus from the values passed.
:param CorpusCreateDTO corpus: The values for the new Family.
:param UserContext user: The current user context.
:raises RepositoryError: raised on a database error
:raises ValidationError: raised should the import_id be invalid.
:return str: The new created Corpus or None if unsuccessful.
"""
if db is None:
db = db_session.get_db()

# Check the corpus type name exists in the database already.
if not corpus_repo.is_corpus_type_name_valid(db, corpus.corpus_type_name):
raise ValidationError("Invalid corpus type name")

# Check that the organisation ID exists in the database.
if org_repo.get_name_from_id(db, corpus.organisation_id) is None:
raise ValidationError("Invalid organisation")

try:
import_id = corpus_repo.create(db, corpus)
if len(import_id) == 0:
db.rollback()
return import_id
except Exception as e:
db.rollback()
raise e
finally:
db.commit()
76 changes: 38 additions & 38 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 81e159c

Please sign in to comment.