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

Feature/app 47 read corpus type information via admin service #274

Merged
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
09f84f7
Add corpus type read route permissions
katybaulch Jan 2, 2025
6af55be
Add /all and /get routes for corpus types
katybaulch Jan 2, 2025
7b11f2d
Add integration tests for /all corpus types
katybaulch Jan 2, 2025
af9ac38
Register corpus types router
katybaulch Jan 2, 2025
822940f
Add /get tests for corpus types router
katybaulch Jan 2, 2025
535745c
Fix return type
katybaulch Jan 2, 2025
5d83994
Test fixes
katybaulch Jan 2, 2025
7ab9029
Bump to 2.17.25
katybaulch Jan 2, 2025
dedeeec
Rename files to avoid pytest discovery error
katybaulch Jan 2, 2025
deab3a3
Fix pytest test discovery config
katybaulch Jan 2, 2025
3160fcd
Make get optional return
katybaulch Jan 2, 2025
77436a4
Use one or none
katybaulch Jan 2, 2025
1aed92f
Update authorisation.py
katybaulch Jan 2, 2025
ef3375e
Update authorisation.py
katybaulch Jan 2, 2025
3a02e74
Add corpus type unit tests
katybaulch Jan 2, 2025
dbf72f3
Make error singular not plural
katybaulch Jan 2, 2025
3cf4f24
Update __init__.py
katybaulch Jan 2, 2025
40b17fb
Update corpus_type_service.py
katybaulch Jan 2, 2025
747aa17
Update conftest.py
katybaulch Jan 2, 2025
8e812c7
Merge branch 'main' into feature/app-47-read-corpus-type-information-…
katybaulch Jan 2, 2025
5c8a97f
Fix tests
katybaulch Jan 2, 2025
427be1d
Fix test
katybaulch Jan 2, 2025
89f7e91
Fix test
katybaulch Jan 2, 2025
bea964e
Fix tests
katybaulch Jan 2, 2025
4bc2f97
Add service unit tests
katybaulch Jan 2, 2025
8187e6b
Update test_get_corpus_type_service.py
katybaulch Jan 2, 2025
3370031
Fix tests
katybaulch Jan 2, 2025
cfe6d3e
Early return
katybaulch Jan 2, 2025
6118af8
Final test fixes
katybaulch Jan 2, 2025
ac60a71
Update test
katybaulch Jan 2, 2025
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
2 changes: 2 additions & 0 deletions app/api/api_v1/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.api.api_v1.routers.collection import collections_router
from app.api.api_v1.routers.config import config_router
from app.api.api_v1.routers.corpus import corpora_router
from app.api.api_v1.routers.corpus_type import corpus_types_router
from app.api.api_v1.routers.document import document_router
from app.api.api_v1.routers.event import event_router
from app.api.api_v1.routers.family import families_router
Expand All @@ -12,6 +13,7 @@
"analytics_router",
"auth_router",
"corpora_router",
"corpus_types_router",
"collections_router",
"config_router",
"document_router",
Expand Down
58 changes: 58 additions & 0 deletions app/api/api_v1/routers/corpus_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import logging

from fastapi import APIRouter, HTTPException, Request, status

from app.errors import RepositoryError, ValidationError
from app.model.corpus_type import CorpusTypeReadDTO
from app.service import corpus_type as corpus_type_service

corpus_types_router = APIRouter()

_LOGGER = logging.getLogger(__name__)


@corpus_types_router.get(
"/corpus-types",
response_model=list[CorpusTypeReadDTO],
)
async def get_all_corpus_types(request: Request) -> list[CorpusTypeReadDTO]:
"""Retrieve all corpus types.

:param Request request: Request object.
:raises HTTPException: If the corpus type is not found.
:return CorpusTypeReadDTO: The requested corpus type.
"""
try:
return corpus_type_service.all(request.state.user)
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)


@corpus_types_router.get(
"/corpus-types/{corpus_type_name}",
katybaulch marked this conversation as resolved.
Show resolved Hide resolved
response_model=CorpusTypeReadDTO,
)
async def get_corpus_type(corpus_type_name: str) -> CorpusTypeReadDTO:
"""Retrieve a specific corpus type by its name.

:param str corpus_type_name: The ID of the corpus type to retrieve.
:raises HTTPException: If the corpus type is not found.
:return CorpusTypeReadDTO: The requested corpus type.
"""
try:
corpus_type = corpus_type_service.get(corpus_type_name)
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
)

if corpus_type is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Corpus type not found: {corpus_type_name}",
)
return corpus_type
9 changes: 9 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
collections_router,
config_router,
corpora_router,
corpus_types_router,
document_router,
event_router,
families_router,
Expand Down Expand Up @@ -111,6 +112,14 @@ async def lifespan(app_: FastAPI):
tags=["corpora"],
dependencies=[Depends(check_user_auth)],
)

app.include_router(
corpus_types_router,
prefix="/api/v1",
tags=["corpus-types"],
dependencies=[Depends(check_user_auth)],
)

# Add CORS middleware to allow cross origin requests from any port
app.add_middleware(
CORSMiddleware,
Expand Down
5 changes: 5 additions & 0 deletions app/model/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class AuthEndpoint(str, enum.Enum):
EVENT = "EVENTS"
BULK_IMPORT = "BULK-IMPORT"
CORPUS = "CORPORA"
CORPUS_TYPE = "CORPUS-TYPES"


AuthMap = Mapping[AuthEndpoint, Mapping[AuthOperation, AuthAccess]]
Expand Down Expand Up @@ -72,4 +73,8 @@ class AuthEndpoint(str, enum.Enum):
AuthOperation.READ: AuthAccess.SUPER,
AuthOperation.UPDATE: AuthAccess.SUPER,
},
# Corpus Type
AuthEndpoint.CORPUS_TYPE: {
AuthOperation.READ: AuthAccess.SUPER,
},
}
11 changes: 11 additions & 0 deletions app/model/corpus_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel

from app.model.general import Json


class CorpusTypeReadDTO(BaseModel):
"""Representation of a Corpus Type."""

name: str
description: str
metadata: Json
2 changes: 2 additions & 0 deletions app/repository/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import app.repository.collection as collection_repo
import app.repository.config as config_repo
import app.repository.corpus as corpus_repo
import app.repository.corpus_type as corpus_type_repo
import app.repository.document as document_repo
import app.repository.event as event_repo
import app.repository.family as family_repo # type: ignore
Expand All @@ -18,6 +19,7 @@
"collection_repo",
"config_repo",
"corpus_repo",
"corpus_type_repo",
"document_repo",
"event_repo",
"family_repo",
Expand Down
66 changes: 66 additions & 0 deletions app/repository/corpus_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging
from typing import Optional, cast

from db_client.models.organisation import CorpusType, Organisation
from sqlalchemy import asc
from sqlalchemy.exc import MultipleResultsFound
from sqlalchemy.orm import Session

from app.errors import RepositoryError
from app.model.corpus_type import CorpusTypeReadDTO

_LOGGER = logging.getLogger(__name__)


def _corpus_type_to_dto(corpus_type: CorpusType) -> CorpusTypeReadDTO:
"""Convert a CorpusType model to a CorpusTypeReadDTO.

:param CorpusType corpus_type: The corpus type model.
:return CorpusTypeReadDTO: The corresponding DTO.
"""
return CorpusTypeReadDTO(
name=str(corpus_type.name),
description=str(corpus_type.description),
metadata=cast(dict, corpus_type.valid_metadata),
)


def all(db: Session, org_id: Optional[int]) -> list[CorpusTypeReadDTO]:
"""Get a list of all corpus types in the database.

:param db Session: The database connection.
:param org_id int: the ID of the organisation the user belongs to
:return CorpusTypeReadDTO: The requested corpus type.
:raises RepositoryError: If the corpus type is not found.
"""
query = db.query(CorpusType)
if org_id is not None:
query = query.filter(Organisation.id == org_id)

result = query.order_by(asc(CorpusType.name)).all()

if not result:
return []

return [_corpus_type_to_dto(corpus_type) for corpus_type in result]


def get(db: Session, corpus_type_name: str) -> Optional[CorpusTypeReadDTO]:
"""Get a corpus type from the database given a name.

:param db Session: The database connection.
:param str corpus_type_name: The ID of the corpus type to retrieve.
:return CorpusTypeReadDTO: The requested corpus type.
:raises RepositoryError: If the corpus type is not found.
"""
try:
corpus_type = (
db.query(CorpusType)
.filter(CorpusType.name == corpus_type_name)
.one_or_none()
)
return _corpus_type_to_dto(corpus_type) if corpus_type is not None else None

except MultipleResultsFound as e:
_LOGGER.error(e)
raise RepositoryError(e)
2 changes: 1 addition & 1 deletion app/service/authorisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def is_authorised(user: UserContext, entity: AuthEndpoint, op: AuthOperation) ->
return

raise AuthorisationError(
f"User {user.email} is not authorised to {op} {_get_article(entity.value)} {entity}"
f"User {user.email} is not authorised to {op} {_get_article(entity.value)} {entity.name}"
)


Expand Down
42 changes: 42 additions & 0 deletions app/service/corpus_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging
from typing import Optional

from pydantic import ConfigDict, validate_call
from sqlalchemy.orm import Session

import app.clients.db.session as db_session
from app.model.corpus_type import CorpusTypeReadDTO
from app.model.user import UserContext
from app.repository import corpus_type as corpus_type_repo
from app.service import app_user

_LOGGER = logging.getLogger(__name__)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def all(user: UserContext) -> list[CorpusTypeReadDTO]:
"""
Gets the entire list of corpora from the repository.

:param UserContext user: The current user context.
:return list[CorpusReadDTO]: The list of corpora.
"""
with db_session.get_db() as db:
org_id = app_user.restrict_entities_to_user_org(user)
return corpus_type_repo.all(db, org_id)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def get(
corpus_type_name: str, db: Optional[Session] = None
) -> Optional[CorpusTypeReadDTO]:
"""Retrieve a corpus type by ID.

:param str corpus_type_name: The name of the corpus type to retrieve.
:return CorpusTypeReadDTO: The requested corpus type.
:raises RepositoryError: If there is an error during retrieval.
"""
if db is None:
db = db_session.get_db()

return corpus_type_repo.get(db, corpus_type_name)
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "admin_backend"
version = "2.17.24"
version = "2.17.25"
description = ""
authors = ["CPR-dev-team <[email protected]>"]
packages = [{ include = "app" }, { include = "tests" }]
Expand Down Expand Up @@ -51,7 +51,7 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
addopts = "-p no:cacheprovider"
addopts = "--import-mode=importlib"
env_files = """
.env.test
.env
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests/bulk_import/test_bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def test_bulk_import_admin_non_super(
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert (
data["detail"] == "User [email protected] is not authorised to CREATE a BULK-IMPORT"
data["detail"] == "User [email protected] is not authorised to CREATE a BULK_IMPORT"
)


Expand All @@ -416,5 +416,5 @@ def test_bulk_import_non_super_non_admin(
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert (
data["detail"] == "User [email protected] is not authorised to CREATE a BULK-IMPORT"
data["detail"] == "User [email protected] is not authorised to CREATE a BULK_IMPORT"
)
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def test_get_template_admin_non_super(
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert (
data["detail"] == "User [email protected] is not authorised to READ a BULK-IMPORT"
data["detail"] == "User [email protected] is not authorised to READ a BULK_IMPORT"
)


Expand All @@ -250,4 +250,4 @@ def test_get_template_non_admin_non_super(
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to READ a BULK-IMPORT"
assert data["detail"] == "User [email protected] is not authorised to READ a BULK_IMPORT"
2 changes: 1 addition & 1 deletion tests/integration_tests/corpus/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_get_all_corpora_non_super(
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to READ a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to READ a CORPUS"


def test_get_all_corpora_when_not_authenticated(client: TestClient, data_db: Session):
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests/corpus/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def test_create_corpus_non_admin_non_super(client: TestClient, user_header_token
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to CREATE a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to CREATE a CORPUS"


def test_create_corpus_admin_non_super(client: TestClient, admin_user_header_token):
Expand All @@ -174,7 +174,7 @@ def test_create_corpus_admin_non_super(client: TestClient, admin_user_header_tok
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to CREATE a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to CREATE a CORPUS"


def test_create_corpus_rollback(
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/corpus/test_get.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_get_corpus_non_super(client: TestClient, data_db: Session, user_header_
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to READ a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to READ a CORPUS"


def test_get_corpus_when_not_authenticated(client: TestClient, data_db: Session):
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/corpus/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_search_corpus_non_super(
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to READ a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to READ a CORPUS"


def test_search_corpus_when_not_authorised(client: TestClient, data_db: Session):
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests/corpus/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def test_update_corpus_non_super_non_admin(client: TestClient, user_header_token
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to UPDATE a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to UPDATE a CORPUS"


def test_update_corpus_non_super_admin(client: TestClient, admin_user_header_token):
Expand All @@ -177,7 +177,7 @@ def test_update_corpus_non_super_admin(client: TestClient, admin_user_header_tok
)
assert response.status_code == status.HTTP_403_FORBIDDEN
data = response.json()
assert data["detail"] == "User [email protected] is not authorised to UPDATE a CORPORA"
assert data["detail"] == "User [email protected] is not authorised to UPDATE a CORPUS"
katybaulch marked this conversation as resolved.
Show resolved Hide resolved


def test_update_corpus_idempotent(
Expand Down
Loading
Loading