Skip to content

Commit

Permalink
Merge pull request #2 from datahangar/turnilo_get_filter
Browse files Browse the repository at this point in the history
routes/turnilo: allow query params on GET dashs
  • Loading branch information
msune authored Jun 14, 2024
2 parents c07fe17 + df44fae commit 9376293
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 88 deletions.
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
requests==2.32.3
types-requests==2.31.0.10
fastapi==0.105.0
uvicorn==0.25.0
sqlalchemy-utils==0.41.1
sqlmodel==0.0.14
psycopg2==2.9.9
sqlalchemy-utils==0.41.2
sqlmodel==0.0.19
psycopg2-binary==2.9.9
jinja2==3.1.3
pyyaml==6.0.1
pytest
types-requests
78 changes: 3 additions & 75 deletions src/routes/routes.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,5 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from sqlalchemy.orm import Session
from models.turnilo_dashboard import TurniloDashboard
from services import turnilo_dashboards as td

import constants
import data.database as db
from fastapi import APIRouter
from routes.turnilo_dashboard_routes import turnilo_router

api_router = APIRouter()

### Dashboards ###

# GET


@api_router.get(
constants.URL_PATH + "/turnilo/dashboards/",
response_model=List[TurniloDashboard],
summary="Gets all Turnilo dashboards"
)
def turnilo_get_dashboards(db_session: Session = Depends(db.get_session)):
return td.dashboards_get_all(db_session)


@api_router.get(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Get a Turnilo dashboard by id (integer)"
)
def turnilo_get_dashboard_id(id: str, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_get_id(db_session, int_id)

# POST


@api_router.post(
constants.URL_PATH + "/turnilo/dashboards/",
response_model=TurniloDashboard,
summary="Create a Turnilo dashboard. A unique id will be assigned."
)
def turnilo_create_dashboard(dashboard: TurniloDashboard, db_session: Session = Depends(db.get_session)):
return td.dashboards_create(db_session, dashboard)


# PUT
@api_router.put(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Update/replace a Turnilo dashboard. The dashboard (id) must exist"
)
def turnilo_update_dashboard(id: str, dashboard: TurniloDashboard, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
dashboard.id = int_id
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_update(db_session, dashboard)

# DELETE


@api_router.delete(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Delete a Turnilo dashboard"
)
def turnilo_delete_dashboard(id: str, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_delete(db_session, int_id)
api_router.include_router(turnilo_router)
72 changes: 72 additions & 0 deletions src/routes/turnilo_dashboard_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from sqlmodel import Session
from models.turnilo_dashboard import TurniloDashboard
from services import turnilo_dashboards as td

import constants
import data.database as db

turnilo_router = APIRouter()

### Dashboards ###


@turnilo_router.get(
constants.URL_PATH + "/turnilo/dashboards/",
response_model=List[TurniloDashboard],
summary="Gets all Turnilo dashboards"
)
def turnilo_get_dashboards(db_session: Session = Depends(db.get_session),
query_params: td.GetQueryParams = Depends()):
query_params.validate()
return td.dashboards_get_all(db_session, query_params)


@turnilo_router.get(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Get a Turnilo dashboard by id (integer)"
)
def turnilo_get_dashboard_id(id: str, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_get_id(db_session, int_id)


@turnilo_router.post(
constants.URL_PATH + "/turnilo/dashboards/",
response_model=TurniloDashboard,
summary="Create a Turnilo dashboard. A unique id will be assigned."
)
def turnilo_create_dashboard(dashboard: TurniloDashboard, db_session: Session = Depends(db.get_session)):
return td.dashboards_create(db_session, dashboard)


@turnilo_router.put(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Update/replace a Turnilo dashboard. The dashboard (id) must exist"
)
def turnilo_update_dashboard(id: str, dashboard: TurniloDashboard, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
dashboard.id = int_id
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_update(db_session, dashboard)


@turnilo_router.delete(
constants.URL_PATH + "/turnilo/dashboards/{id}",
response_model=TurniloDashboard,
summary="Delete a Turnilo dashboard"
)
def turnilo_delete_dashboard(id: str, db_session: Session = Depends(db.get_session)):
try:
int_id = int(id)
except BaseException:
raise HTTPException(status_code=400, detail="Id is not an integer")
return td.dashboards_delete(db_session, int_id)
41 changes: 35 additions & 6 deletions src/services/turnilo_dashboards.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
from typing import List
from sqlalchemy.orm import Session
import re
from typing import List, Optional
from sqlmodel import Session, select
from pydantic import BaseModel, Field
from sqlalchemy import exc
from models.turnilo_dashboard import TurniloDashboard
from fastapi import HTTPException

# Turnilo Dashboards


def dashboards_get_all(session: Session) -> List[TurniloDashboard]:
return session.query(TurniloDashboard).all()
class GetQueryParams(BaseModel):
"""
Get query filtering params
"""
shortName: Optional[str] = Field(default=None, description="Dashboard's shortName")
dataCube: Optional[str] = Field(default=None, description="Dashboard's dataCube")

def is_valid_param(self, s: str) -> bool:
if len(s) > 256:
return False
pattern = r'^[a-zA-Z0-9_-]+$'
return bool(re.match(pattern, s))

def validate(self):
if self.shortName and not self.is_valid_param(self.shortName):
raise HTTPException(status_code=400, detail=f"Invalid shortName='{self.shortName}'")
if self.dataCube and not self.is_valid_param(self.dataCube):
raise HTTPException(status_code=400, detail=f"Invalid dataCube='{self.dataCube}'")


def dashboards_get_all(session: Session, query_params: GetQueryParams) -> List[TurniloDashboard]:
statement = select(TurniloDashboard)
if query_params.shortName:
statement = statement.where(TurniloDashboard.shortName == query_params.shortName)
if query_params.dataCube:
statement = statement.where(TurniloDashboard.dataCube == query_params.dataCube)
return list(session.exec(statement).all())


def _dashboards_return_single_obj(results: List[TurniloDashboard]):
Expand All @@ -20,7 +47,8 @@ def _dashboards_return_single_obj(results: List[TurniloDashboard]):


def dashboards_get_id(session: Session, _id: int) -> TurniloDashboard:
results: List[TurniloDashboard] = session.query(TurniloDashboard).filter_by(id=_id).all()
statement = select(TurniloDashboard).where(TurniloDashboard.id == _id)
results: List[TurniloDashboard] = list(session.exec(statement).all())
return _dashboards_return_single_obj(results)


Expand Down Expand Up @@ -62,7 +90,8 @@ def dashboards_update(session: Session, dashboard: TurniloDashboard) -> TurniloD
def dashboards_delete(session: Session, _id: int) -> TurniloDashboard:
dashboard = None
try:
dashboard = session.query(TurniloDashboard).filter_by(id=_id).one()
statement = select(TurniloDashboard).where(TurniloDashboard.id == _id)
dashboard = session.exec(statement).one()
except BaseException:
pass
if dashboard is None:
Expand Down
2 changes: 1 addition & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
test:
@rm -rf data/ || true
@mkdir -p data
@PYTHONPATH=`pwd`/../src/ pytest -v
@PYTHONPATH=`pwd`/../src/ pytest -Werror -v -s
12 changes: 10 additions & 2 deletions test/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import requests
import argparse
from urllib.parse import urlencode

DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8080
Expand Down Expand Up @@ -32,11 +33,18 @@ def delete_dashboard(host, port, dashboard_id) -> requests.Response:
return response


def get_dashboard(host, port, dashboard_id=None) -> requests.Response:
def get_dashboard(host, port, dashboard_id=None, shortName=None, dataCube=None) -> requests.Response:
if dashboard_id:
url = f"http://{host}:{port}/{DEFAULT_PATH}/{dashboard_id}"
else:
url = f"http://{host}:{port}/{DEFAULT_PATH}"
query = {}
if shortName:
query["shortName"] = shortName
if dataCube:
query["dataCube"] = dataCube
query_str = urlencode(query)
query_str = "?" + query_str if query_str != "" else ""
url = f"http://{host}:{port}/{DEFAULT_PATH}{query_str}"
response = requests.get(url)
print_response(response)
return response
Expand Down
99 changes: 99 additions & 0 deletions test/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,105 @@ def test_get_all_dashboards(sample_dashboard: dict[str, Any]) -> None:
assert res.status_code == 200


def test_get_all_dashboards_query_params(sample_dashboard: dict[str, Any]) -> None:
res = get_dashboard(HOST, PORT)
assert res.json() == []

# Create two dashboards
dashboard = sample_dashboard.copy()
res = create_dashboard(HOST, PORT, json.dumps(dashboard))
assert res.json()["id"] == 1

dashboard = sample_dashboard.copy()
dashboard["dataCube"] = "myDatacube"
dashboard["shortName"] = "shortName"
res = create_dashboard(HOST, PORT, json.dumps(dashboard))
assert res.json()["id"] == 2

# Try with invalid query params
res = get_dashboard(HOST, PORT, shortName=" ")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube=" ")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, shortName="name;test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube="name;test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, shortName="name?test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube="name?test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, shortName="name'test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube="name'test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, shortName="name\"test")
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube="name\"test")
assert res.status_code == 400
long_name = 's' * 280
res = get_dashboard(HOST, PORT, shortName=long_name)
assert res.status_code == 400
res = get_dashboard(HOST, PORT, dataCube=long_name)
assert res.status_code == 400

# Now validate functionality

# Name only
res = get_dashboard(HOST, PORT, shortName="simple_dashboard")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 1
res = get_dashboard(HOST, PORT, shortName="shortName")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 2
res = get_dashboard(HOST, PORT, shortName="shortNameA")
assert res.status_code == 200
assert len(res.json()) == 0
res = get_dashboard(HOST, PORT, shortName="shortName2")
assert res.status_code == 200
assert len(res.json()) == 0

# DataCube only
res = get_dashboard(HOST, PORT, dataCube="networkFlows")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 1
res = get_dashboard(HOST, PORT, dataCube="myDatacube")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 2
res = get_dashboard(HOST, PORT, dataCube="networkFlowsA")
assert res.status_code == 200
assert len(res.json()) == 0
res = get_dashboard(HOST, PORT, dataCube="networkFlows2")
assert res.status_code == 200
assert len(res.json()) == 0

# Both
res = get_dashboard(HOST, PORT, dataCube="networkFlows", shortName="simple_dashboard")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 1
res = get_dashboard(HOST, PORT, dataCube="myDatacube", shortName="shortName")
assert res.status_code == 200
assert len(res.json()) == 1
assert res.json()[0]["id"] == 2
res = get_dashboard(HOST, PORT, dataCube="networkFlowsA", shortName="shortNameA")
assert res.status_code == 200
assert len(res.json()) == 0
res = get_dashboard(HOST, PORT, dataCube="networkFlows2", shortName="shortName2")
assert res.status_code == 200
assert len(res.json()) == 0

# Cleanup
res = delete_dashboard(HOST, PORT, 1)
assert res.status_code == 200
res = delete_dashboard(HOST, PORT, 2)
assert res.status_code == 200


def test_update_dashboard(sample_dashboard: dict[str, Any]) -> None:
dashboard = sample_dashboard.copy()

Expand Down

0 comments on commit 9376293

Please sign in to comment.