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/pdct 451 add summary endpoint #17

Merged
merged 11 commits into from
Oct 9, 2023
30 changes: 21 additions & 9 deletions app/api/api_v1/routers/analytics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Endpoints for managing the Analytics service."""
import logging
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status
from app.errors import RepositoryError

import app.service.analytics as analytics_service
from app.model.analytics import SummaryDTO

analytics_router = r = APIRouter()

Expand All @@ -9,18 +13,26 @@

@r.get(
"/analytics/summary",
response_model=dict[str, int],
response_model=SummaryDTO,
)
async def get_analytics_summary() -> dict[str, int]:
async def get_analytics_summary() -> SummaryDTO:
"""
Returns an analytics summary.

:return dict[str, int]: returns a dictionary of the summarised analytics
data in key (str): value (int) form.
"""
return {
"n_documents": 1000,
"n_families": 800,
"n_collections": 10,
"n_events": 50,
}
try:
summary_dto = analytics_service.summary()
except RepositoryError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=e.message
)

if any(summary_value is None for _, summary_value in summary_dto):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Analytics summary not found",
)

return summary_dto
1 change: 1 addition & 0 deletions app/api/api_v1/routers/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Endpoints for managing the Collection entity."""
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, status
from app.errors import RepositoryError, ValidationError

Expand Down
11 changes: 11 additions & 0 deletions app/model/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from pydantic import BaseModel
from typing import Optional


class SummaryDTO(BaseModel):
"""Representation of an Analytics Summary."""

n_documents: Optional[int]
n_families: Optional[int]
n_collections: Optional[int]
n_events: Optional[int]
32 changes: 32 additions & 0 deletions app/repository/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ def all(db: Session) -> list[CollectionReadDTO]:
return result


# def count(db: Session) -> Optional[int]:
# """
# Counts the number of collections in the repository.

# :param db Session: the database connection
# :return Optional[int]: The number of collections in the repository or none.
# """
# try:
# n_collections = _get_query(db).count()
# except Exception as e:
# _LOGGER.error(e)
# return

# return n_collections


def get(db: Session, import_id: str) -> Optional[CollectionReadDTO]:
"""
Gets a single collection from the repository.
Expand Down Expand Up @@ -201,3 +217,19 @@ def delete(db: Session, import_id: str) -> bool:
result = db.execute(c)

return result.rowcount > 0 # type: ignore


def count(db: Session) -> Optional[int]:
"""
Counts the number of collections in the repository.

:param db Session: the database connection
:return Optional[int]: The number of collections in the repository or none.
"""
try:
n_collections = _get_query(db).count()
except Exception as e:
_LOGGER.error(e)
return

return n_collections
16 changes: 16 additions & 0 deletions app/repository/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,19 @@ def delete(db: Session, import_id: str) -> bool:
raise RepositoryError(msg)

return True


def count(db: Session) -> Optional[int]:
"""
Counts the number of documents in the repository.

:param db Session: the database connection
:return Optional[int]: The number of documents in the repository or none.
"""
try:
n_documents = _get_query(db).count()
except NoResultFound as e:
_LOGGER.error(e)
return

return n_documents
16 changes: 16 additions & 0 deletions app/repository/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,19 @@ def get_organisation(db: Session, family_import_id: str) -> Optional[Organisatio

# TODO - can this be improved - we get warnings on integration tests ?
return db.query(Organisation).select_from(family_org).one_or_none()


def count(db: Session) -> Optional[int]:
"""
Counts the number of families in the repository.

:param db Session: the database connection
:return Optional[int]: The number of families in the repository or none.
"""
try:
n_families = _get_query(db).count()
except NoResultFound as e:
_LOGGER.error(e)
return

return n_families
44 changes: 44 additions & 0 deletions app/service/analytics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Analytics Service

This layer uses the document, family, and collection repos to handle querying
the count of available entities.
"""
import logging

from pydantic import ConfigDict, validate_call
from app.errors import RepositoryError
from app.model.analytics import SummaryDTO
import app.service.collection as collection_service
import app.service.document as document_service
import app.service.family as family_service
import app.clients.db.session as db_session
from sqlalchemy import exc


_LOGGER = logging.getLogger(__name__)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def summary() -> SummaryDTO:
"""
Gets an analytics summary from the repository.

:return SummaryDTO: The analytics summary found.
"""
try:
n_collections = collection_service.count()
n_families = family_service.count()
n_documents = document_service.count()
n_events = 0

return SummaryDTO(
n_documents=n_documents,
n_families=n_families,
n_collections=n_collections,
n_events=n_events,
)

except exc.SQLAlchemyError as e:
_LOGGER.error(e)
raise RepositoryError(str(e))
15 changes: 15 additions & 0 deletions app/service/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,18 @@ def delete(import_id: str, db: Session = db_session.get_db()) -> bool:
"""
id.validate(import_id)
return collection_repo.delete(db, import_id)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
katybaulch marked this conversation as resolved.
Show resolved Hide resolved
def count() -> Optional[int]:
"""
Gets a count of collections from the repository.

:return Optional[int]: The number of collections in the repository or none.
"""
try:
with db_session.get_db() as db:
return collection_repo.count(db)
except exc.SQLAlchemyError as e:
_LOGGER.error(e)
raise RepositoryError(str(e))
15 changes: 15 additions & 0 deletions app/service/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,18 @@ def delete(import_id: str, db: Session = db_session.get_db()) -> bool:
"""
id.validate(import_id)
return document_repo.delete(db, import_id)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
katybaulch marked this conversation as resolved.
Show resolved Hide resolved
def count() -> Optional[int]:
"""
Gets a count of documents from the repository.

:return Optional[int]: The number of documents in the repository or none.
"""
try:
with db_session.get_db() as db:
return document_repo.count(db)
except exc.SQLAlchemyError as e:
_LOGGER.error(e)
raise RepositoryError(str(e))
16 changes: 16 additions & 0 deletions app/service/family.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,19 @@ def delete(import_id: str, db: Session = db_session.get_db()) -> bool:
msg = f"Unable to delete family {import_id}"
_LOGGER.exception(msg)
raise RepositoryError(msg)
return family_repo.delete(db, import_id)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
katybaulch marked this conversation as resolved.
Show resolved Hide resolved
def count() -> Optional[int]:
"""
Gets a count of families from the repository.

:return Optional[int]: The number of families in the repository or none.
"""
try:
with db_session.get_db() as db:
return family_repo.count(db)
except exc.SQLAlchemyError as e:
_LOGGER.error(e)
raise RepositoryError(str(e))
Empty file.
113 changes: 113 additions & 0 deletions integration_tests/analytics/test_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from fastapi.testclient import TestClient
from fastapi import status
from sqlalchemy.orm import Session

from integration_tests.setup_db import (
EXPECTED_ANALYTICS_SUMMARY,
EXPECTED_ANALYTICS_SUMMARY_KEYS,
setup_db,
)


# --- GET ALL


def test_get_all_analytics(client: TestClient, test_db: Session, user_header_token):
setup_db(test_db)
response = client.get(
"/api/v1/analytics",
headers=user_header_token,
)
assert response.status_code == status.HTTP_404_NOT_FOUND


def test_get_all_analytics_when_not_authenticated(client: TestClient, test_db: Session):
setup_db(test_db)
response = client.get(
"/api/v1/analytics",
)
assert response.status_code == status.HTTP_404_NOT_FOUND


# --- GET SUMMARY


def test_get_analytics_summary(client: TestClient, test_db: Session, user_header_token):
setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
headers=user_header_token,
)
assert response.status_code == status.HTTP_200_OK

data = response.json()
assert type(data) is dict
assert list(data.keys()) == EXPECTED_ANALYTICS_SUMMARY_KEYS

assert data["n_events"] == 0
assert dict(sorted(data.items())) == dict(
sorted(EXPECTED_ANALYTICS_SUMMARY.items())
)


def test_get_analytics_summary_when_not_authenticated(
client: TestClient, test_db: Session
):
setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_get_analytics_summary_when_not_found(
client: TestClient, test_db: Session, collection_count_none, user_header_token
):
setup_db(test_db, configure_empty=True)
# setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
headers=user_header_token,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
data = response.json()
assert data["detail"] == "Analytics summary not found"


def test_get_analytics_when_collection_db_error(
client: TestClient, test_db: Session, bad_collection_repo, user_header_token
):
setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
headers=user_header_token,
)
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["detail"] == "Bad Repo"


def test_get_analytics_when_family_db_error(
client: TestClient, test_db: Session, bad_family_repo, user_header_token
):
setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
headers=user_header_token,
)
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["detail"] == "Bad Repo"


def test_get_analytics_when_document_db_error(
client: TestClient, test_db: Session, bad_document_repo, user_header_token
):
setup_db(test_db)
response = client.get(
"/api/v1/analytics/summary",
headers=user_header_token,
)
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
assert data["detail"] == "Bad Repo"
Loading
Loading