Skip to content

Commit

Permalink
tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
ColinBD committed Jan 20, 2022
1 parent a351587 commit d349800
Show file tree
Hide file tree
Showing 19 changed files with 225 additions and 238 deletions.
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
api/docs
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# IDEAFAST-ETL access settings
WP3API_AIRFLOW_PASS=""
AIRFLOW_SERVER="airflow-webserver"

# UCAM API
UCAM_URI=""
UCAM_USERNAME=""
UCAM_PASSWORD=""

# --- USERS ---
# for local development, when changing log in values
# be sure to clean the docker volumes before rebooting
_MONGO_INITDB_ROOT_USERNAME=
_MONGO_INITDB_ROOT_PASSWORD=
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ __pycache__

# secrets
*.env
ssh/*
!ssh/.gitkeep

# Exported reuirements, better to export locally
requirements.txt
Expand All @@ -13,3 +15,6 @@ requirements.txt
.pytest_cache
.mypy_cache
.nox

# Possible remnants of local dev
/api/docs
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim

RUN apt-get update && \
apt-get install -y --no-install-recommends git ssh \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY ./requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir -r /app/requirements.txt

COPY ./scripts /app
COPY ./api /app/api
76 changes: 59 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,88 @@
# IDEA-FAST WP3 Public Facing API

A public facing API used to feed the IDEA-FAST Study Dashboard. It includes some functionality embedded in the `IDEAFAST/middleware-service:consumer` repository, but has been started from scratch to interface with the `IDEAFAST/ideafast-etl` pipeline and newly set up Dashboard for simplicity and separation of tasks.
A public facing API used to feed the IDEA-FAST Study Dashboard. It includes some functionality embedded in the `IDEAFAST/middleware-service:consumer` repository, but has been started from scratch to interface with the `IDEAFAST/ideafast-etl` pipeline and newly set up Dashboard for simplicity and separation of concerns.

> The development setup, folder structure, and cli.py share a lot of commonalities with the `IDEAFAST/ideafast-etl` repository.
The primary task fo the API is to feed the IDEAFAST Study Dashboard with

## Run the API Locally
- information about enrolled participants (provided though the `UCAM` api),
- details about their (temporary) app logins,
- the status of the [IDEAFAST ETL pipeline](https://github.com/ideafast/ideafast-etl), and
- serving device documentation and FAQ pulled from a private GitHub repo.

## Run the API locally and remotely using Docker

By design, the API is containerised and can be easily deployed with a docker-compose as implemented for [ideafast/stack](https://github.com/ideafast/stack). As an image, it needs to be fed an `.env` file to authenticate with 3rd party services, and a `ssh key` to authenticate with the private GitHub repository to pull the latest documentation.

### Setup

Rename the `.env.example` to `.env` and update the variables/secrets as needed.
1. Rename the `.env.example` to `.env` and update the variables/secrets as needed.
2. Get the `ssh keys` from your colleague and store then in the [ssh](ssh) folder in this repository. If you, however, need a new one:
- Navigate into the [ssh](ssh) folder _(cd ssh)_
- Generate a key with the command below. _Note that the `-N ''` parameter results in a ssh key **without** password, which is generally not advised, but useful in an non-interactive container application as this one. The `-f ed25519` parameter results in the key being generated in the folder you navigated to_.
```shell
ssh-keygen -t ed25519 -f id_ed25519 -C "[email protected]" -N ''
```
- Go to the GitHub repository hosting the documentation (in this case github.com/ideafast/ideafast-devicesupportdocs-web, as you can see from the [scripts/prestart.sh](scripts/prestart.sh) script), navigate to _Settings_ and _Deploy keys_ and add this key as a read-only key.

### Run

### Containerised
The API is available as a Docker image, and can be spun up with:
Only meant as an example, but you can run the API locally with the example docker-compose file in this repository:

```shell
docker-compose up
docker-compose -f example.docker-compose.yml up
```

Open your browser and try out a few endpoints, e.g.
- http://localhost:8000/patients
- http://localhost:8000/docs
- http://localhost:8000/pipeline
- http://localhost/patients
- http://localhost/docs/axivity
- http://localhost/status

The docker-compose file will also spin up a mongo container. After following the steps below outlined in <a name="Inserting participant credentials into the database">Inserting participant credentials into the database</a> to populate the database with some patient credentials content, you can try accessing this at

- http://localhost/patients/credentials/*participant_id*

Note that for the pipeline endpoint, the [`ideafast-etl`](https://github.com/ideafast/ideafast-etl) docker image also needs to be running, as access is only provided locally.

### CLI
Trigger a pull for new docs for the GET /docs endpoint by running

```shell
curl -X POST -H "Content-Type: application/json" -d '{"sample":"dict"}' http://localhost/docs/update
```

Trigger a pull for new docs for the GET /docs endpoint by running
```shell
curl -X POST -H "Content-Type: application/json" -d '{"sample":"dict"}' http://localhost/docs/update
```

When deploying this API remotely, please implement the appropriate safety protocol (e.g. basic authentication) for access. IDEAFAST uses a reverse proxy with [traefik](https://traefik.io/) and restricts access to the API (such as the endpoint above) with basic authentication.

Note that all endpoints have dependencies on other (spun up) services with potential passwords (see [.example.env](.example.env)):
- GET /patients on the UCAM API _(the first request can take a while due to the serverless architecture used by UCAM)_
- GET /status on the the [`ideafast-etl`](https://github.com/ideafast/ideafast-etl) service
- GET /docs on the private GitHub repository, for which the ssh key is needed
----

## Development and hot-reloading

A CLI command sets up the API locally to enable hot-reloading as code changes, something the docker setup prevents. Please follow the advised steps below for local development.

### Preparations

[Poetry](https://python-poetry.org/) is used for dependency management during development and [pyenv](https://github.com/pyenv/pyenv) to manage python installations, so please install both on your local machine. We use python 3.8 by default, so please make sure this is installed via pyenv, e.g.

```shell
pyenv install 3.8.0 && pyenv global 3.8.0
```

Once done, clone the repo to your machine, install dependencies for this project, and quickstart the API which will watch for local changes _(useful for during development!)_:
Add the required environmental variables in the `.env` file and quickstart the API which will watch for local changes:

```shell
poetry install
poetry shell
python api/main.py
poetry run local
```

> Note that running `poetry run local` overrides some settings (see [/api/main.py](/api/main.py)) which are normally set in the _docker-compose.yaml_ when running the Docker image.
#### GET /docs
The documentation endpoint relies on a private git repository that needs to be loaded into the docker container at boot. This is handled by the [preshart.sh](scripts/prestart.sh) script. Locally, however, running this script outside a docker container will interfere with your local git and ssh setup. Instead, download the (private) repo as a .zip and place it into the api/docs folder for local development and testing.

## Local development

Expand Down
67 changes: 52 additions & 15 deletions api/docs.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,71 @@
import codecs
import subprocess # noqa
from enum import Enum
from pathlib import Path
from typing import List

from fastapi import APIRouter
from fastapi import APIRouter, BackgroundTasks, HTTPException
from fastapi.responses import HTMLResponse

router = APIRouter()

CURRENT_DIR = Path(__file__).parent
FILES_PATH = CURRENT_DIR / "docs/html"


class DEVICE(Enum):
"""Enum for device types"""

DRM = "dreem"
VTP = "vitalpatch"
AX6 = "axivity"
SMP = "smartphone"
WKS = "wildkeys"
CTB = "cantab"
DRM = "DRM"
VTP = "VTP"
AX6 = "AX6"
SMP = "SMP"
WKS = "WKS"
CTB = "CTB"

# we also want to host documentation about software platforms
UCAM = "UCAM"
DMP = "DMP"

# referring to documentation about how to update this documentsion
DOCS = "DOCS"


def load_doc(device: DEVICE, type: str = "docs") -> str:
"""Read the requested file into memory and return"""
try:
with codecs.open(f"{FILES_PATH}/{type}/{device.name}.html", "r") as f:
content = f.read()
return content
except FileNotFoundError as e:
raise HTTPException(status_code=500, detail="File not found") from e


def retrieve_latest_docs() -> None:
"""Run shell script to pull latest changes to the DOC/FAQ repo"""
subprocess.run(["git", "-C", "api/docs/", "pull"]) # noqa


@router.post("/update", status_code=202, include_in_schema=False)
def update_docs(payload: dict, background_tasks: BackgroundTasks) -> dict:
"""Trigger an update for the docs from Github Actions"""
background_tasks.add_task(retrieve_latest_docs)
return {"message": "Success: updating cache of Docs/API is scheduled"}


@router.get("/")
def docs() -> str:
"""Get information about all device documentation"""
return "got your some info about all devices"
@router.get("/list", response_model=List[str])
def list_devices() -> List[str]:
"""Return a list of possible devices/software to get docs for"""
return [d.name for d in DEVICE]


@router.get("/{device}")
@router.get("/{device}", response_class=HTMLResponse)
def device(device: DEVICE) -> str:
"""Get information about the device documentation"""
return f"got your some info about {device}"
return load_doc(device)


@router.get("/{device}/faq")
@router.get("/{device}/faq", response_class=HTMLResponse)
def faq(device: DEVICE) -> str:
"""Get list of FAQ from device documentation"""
return f"got your some FAQs about {device}"
return load_doc(device, "faq")
8 changes: 0 additions & 8 deletions api/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from fastapi import FastAPI
from fastapi_utils.tasks import repeat_every

from api.docs import router as docs
from api.patients import router as patients
Expand All @@ -10,10 +9,3 @@
api.include_router(patients, prefix="/patients")
api.include_router(docs, prefix="/docs")
api.include_router(pipeline, prefix="/status")


@api.on_event("startup")
@repeat_every(seconds=86400)
def get_latest_docs() -> None:
"""Clone the documentation repo on a daily basis to serve over API"""
print("cloning FAQ and DOC repository")
44 changes: 18 additions & 26 deletions api/utils/db.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
from pydantic.dataclasses import dataclass
from typing import List, Optional
from pymongo import MongoClient
import os
from typing import Optional

from dotenv import load_dotenv
from pydantic.dataclasses import dataclass
from pymongo import MongoClient

load_dotenv()

# setup mongodb connection
myclient = MongoClient(
host=["mongo_credentials:27017"],
username=os.getenv('_MONGO_INITDB_ROOT_USERNAME'),
password=os.getenv('_MONGO_INITDB_ROOT_PASSWORD')
host=["localhost:27017"],
username=os.getenv("_MONGO_INITDB_ROOT_USERNAME"),
password=os.getenv("_MONGO_INITDB_ROOT_PASSWORD"),
)
mydb = myclient["credentials_db"]
mycol = mydb["credentials_collection"]
mydb = myclient["mongo_test"]
mycol = mydb["mongo_test_collection"]


@dataclass
class PatientsCredentials():
class PatientsCredentials:
"""Patient credentials"""

_id: str
patient_id: str
dreem_email: str
dreem_password: str
Expand All @@ -26,25 +30,13 @@ class PatientsCredentials():
tfa_email: str
tfa_password: str


def get_patients_credentials(the_id: str) -> Optional[PatientsCredentials]:
"""Get credentials for one patient based on the ID"""
myquery = { "patient_id": the_id }
payload = mycol.find(myquery)
newPay = None
for x in payload:
newPay = x

if newPay is not None:
patient_credentials = PatientsCredentials(
patient_id = the_id,
dreem_email = newPay['dreem_email'],
dreem_password = newPay['dreem_password'],
wildkeys_email = newPay['wildkeys_email'],
wildkeys_password = newPay['wildkeys_password'],
tfa_email = newPay['tfa_email'],
tfa_password = newPay['tfa_password']
)
myquery = {"patient_id": the_id}
payload = list(mycol.find(myquery))
if payload:
patient_credentials = PatientsCredentials(**payload[0])
return patient_credentials
else:
return None

Loading

0 comments on commit d349800

Please sign in to comment.