Skip to content

Commit

Permalink
Merge pull request #60 from ClimateCompatibleGrowth/development
Browse files Browse the repository at this point in the history
Re-implement app as FastAPI
  • Loading branch information
willu47 authored Jan 2, 2025
2 parents 357dc67 + 9b43303 commit 4af7342
Show file tree
Hide file tree
Showing 56 changed files with 2,296 additions and 657 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git*
**/*.pyc
.venv/
52 changes: 52 additions & 0 deletions .github/workflows/development_dev-research-index.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
# More GitHub Actions for Azure: https://github.com/Azure/actions

name: Build and deploy container app to Azure Web App - dev-research-index

on:
push:
branches:
- development
workflow_dispatch:

jobs:
build:
runs-on: 'ubuntu-latest'

steps:
- name: Checkout the development branch
uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Log in to registry
uses: docker/login-action@v2
with:
registry: https://index.docker.io/v1/
username: ${{ secrets.AzureAppService_ContainerUsername_9310ce663981441ab232b6b875b913e2 }}
password: ${{ secrets.AzureAppService_ContainerPassword_a6a4cadc786e40478e1d15d558c66044 }}

- name: Build and push container image to registry
uses: docker/build-push-action@v3
with:
push: true
tags: index.docker.io/${{ secrets.AzureAppService_ContainerUsername_9310ce663981441ab232b6b875b913e2 }}/ccg-research-index:${{ github.sha }}
file: ./Dockerfile

deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'production'
url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}

steps:
- name: Deploy to Azure Web App
id: deploy-to-webapp
uses: azure/webapps-deploy@v2
with:
app-name: 'dev-research-index'
slot-name: 'production'
publish-profile: ${{ secrets.AzureAppService_PublishProfile_a74f0a22a9c3465a91fa629137af0f81 }}
images: 'index.docker.io/${{ secrets.AzureAppService_ContainerUsername_9310ce663981441ab232b6b875b913e2 }}/ccg-research-index:${{ github.sha }}'
5 changes: 3 additions & 2 deletions .github/workflows/main_research-index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ jobs:
runs-on: 'ubuntu-latest'

steps:
- uses: actions/checkout@v2
- name: Checkout to the branch
uses: actions/checkout@v2

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
Expand All @@ -31,7 +32,7 @@ jobs:
with:
push: true
tags: index.docker.io/${{ secrets.AzureAppService_ContainerUsername_dce678b65c414723a5c407473fe6c430 }}/ccg-research-index:${{ github.sha }}
file: ./Dockerfile
file: Dockerfile

deploy:
runs-on: ubuntu-latest
Expand Down
23 changes: 9 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
# Use an official Python runtime as a parent image
FROM python:3.11.7-bookworm

# Set the working directory in the container to /app
WORKDIR /app
WORKDIR /research-index

# Add the current directory contents into the container at /app
ADD ./app /app
ADD requirements.txt /app/requirements.txt
ADD requirements.txt requirements.txt

# Install packages for the memgraph client
RUN apt update -y
RUN apt install -y python3-dev cmake make gcc g++ libssl-dev

# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80
ADD . .

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:80", "app:app"]
CMD ["fastapi", "run", "app/main.py", \
"--port", "8000", \
"--workers", "4", \
"--proxy-headers"]
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

## Development

Create a `.env` file in the project root with the following environment variables:
```sh
MG_HOST= # Memgraph host address
MG_PORT= # Default Memgraph port
MG_PORT_ALT= # Alternative port
```

To enter development mode of the website, with the memgraph database running in the background, run

python app/app.py
fastapi dev app/main.py

## Deployment

Expand All @@ -17,10 +24,12 @@ Once the VM is up and running, SSH into the VM, download and install memgraph

### 2. Build Docker container

docker build
docker build -t research_index_web_app:development .

Run the docker container in development mode to test

docker run -dp 8000:8000 research_index_web_app:development

docker run -dp 5001:80 -w /app -v "$(pwd):/app" research-index-gunicorn-311 sh -c "python app/app.py"

### 3. Deploy front-end to Azure and provision database
Expand Down
Empty file added app/api/__init__.py
Empty file.
43 changes: 43 additions & 0 deletions app/api/author.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from fastapi import APIRouter, HTTPException, Query, Path
from typing import Annotated
from uuid import UUID

from app.crud.author import Author
from app.schemas.author import AuthorListModel, AuthorOutputModel
from app.schemas.query import FilterWorkstream, FilterParams

router = APIRouter(prefix="/api/authors", tags=["authors"])


@router.get("")
def api_author_list(query: Annotated[FilterWorkstream, Query()]
) -> AuthorListModel:
try:
authors = Author()
if result := authors.get_authors(skip=query.skip,
limit=query.limit,
workstream=query.workstream):
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") from e

@router.get("/{id}")
def api_author(id: Annotated[UUID, Path(title="Unique author identifier")],
query: Annotated[FilterParams, Query()]
) -> AuthorOutputModel:
author = Author()
try:
result = author.get_author(id=id,
result_type=query.result_type,
skip=query.skip,
limit=query.limit)

except KeyError:
raise HTTPException(status_code=404,
detail=f"Author '{id}' not found")
except ValueError as e:
raise HTTPException(status_code=500,
detail=f"Database error: {str(e)}") from e
else:
return result

38 changes: 38 additions & 0 deletions app/api/country.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from fastapi import APIRouter, HTTPException, Query, Path
from typing import Annotated
from app.crud.country import Country
from app.schemas.country import CountryList, CountryOutputListModel
from app.schemas.query import FilterBase, FilterParams

router = APIRouter(prefix="/api/countries", tags=["countries"])


@router.get("")
def api_country_list(query: Annotated[FilterBase, Query()]
) -> CountryList:
country_model = Country()
try:
return country_model.get_countries(query.skip, query.limit)
except Exception as e:
raise HTTPException(status_code=500,
detail=f"Server error: {str(e)}") from e

@router.get("/{id}")
def api_country(id: Annotated[str, Path(examples=['KEN'], title="Country identifier", pattern="^([A-Z]{3})$")],
query: Annotated[FilterParams, Query()]
) -> CountryOutputListModel:
country_model = Country()
try:
result = country_model.get_country(id,
query.skip,
query.limit,
query.result_type)
except KeyError:
raise HTTPException(status_code=404,
detail=f"Country with id {id} not found")
except ValueError as e:
raise HTTPException(status_code=500,
detail=f"Database error: {str(e)}") from e

else:
return result
41 changes: 41 additions & 0 deletions app/api/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from fastapi import APIRouter, HTTPException, Query, Path
from fastapi.logger import logger
from typing import Annotated
from app.schemas.query import FilterOutputList
from uuid import UUID

from app.crud.output import Output
from app.schemas.output import OutputListModel, OutputModel

router = APIRouter(prefix="/api/outputs", tags=["outputs"])


@router.get("")
def api_output_list(
query: Annotated[FilterOutputList, Query()]
) -> OutputListModel:
"""Return a list of outputs"""
outputs = Output()
try:
return outputs.get_outputs(query.skip,
query.limit,
query.result_type,
query.country)
except ValueError as e:
raise HTTPException(status_code=500, detail=str(e)) from e


@router.get("/{id}")
def api_output(id: Annotated[UUID, Path(title="Unique output identifier")]) -> OutputModel:
output = Output()
try:
result = output.get_output(id)
except KeyError as e:
raise HTTPException(
status_code=404, detail=f"Output with id {id} not found"
) from e
except Exception as e:
logger.error(f"Error in api_output: {str(e)}")
raise HTTPException(status_code=400, detail=str(e)) from e
else:
return result
17 changes: 17 additions & 0 deletions app/api/topics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import List

from fastapi import APIRouter

from app.schemas.topic import TopicBaseModel

router = APIRouter(prefix="/api/topics", tags=["countries"])


@router.get("")
def api_topics_list() -> List[TopicBaseModel]:
raise NotImplementedError("Have not yet implemented topics in the database")


@router.get("/{id}")
def api_topics_list(id: str) -> TopicBaseModel:
raise NotImplementedError("Have not yet implemented topics in the database")
47 changes: 47 additions & 0 deletions app/api/workstream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import List, Annotated

from fastapi import APIRouter, HTTPException, Query, Path

from app.crud.workstream import Workstream
from app.schemas.workstream import WorkstreamDetailModel, WorkstreamListModel
from app.schemas.query import FilterBase

router = APIRouter(prefix="/api/workstreams", tags=["workstreams"])


@router.get("")
def list_workstreams(
query: Annotated[FilterBase, Query()]) -> WorkstreamListModel:
"""Return a list of workstreams
Returns
-------
app.schemas.workstream.WorkstreamListModel
"""
model = Workstream()
try:
results = model.get_all(skip=query.skip, limit=query.limit)
except KeyError as e:
raise HTTPException(status_code=500,
detail=f"Database error: {str(e)}") from e
else:
return results



@router.get("/{id}")
def get_workstream(
id: Annotated[str, Path(title="Unique workstream identifier")],
query: Annotated[FilterBase, Query()]
) -> WorkstreamDetailModel:
"""Return a single workstream
"""
model = Workstream()
try:
results = model.get(id, skip=query.skip, limit=query.limit)
except KeyError:
raise HTTPException(status_code=404,
detail=f"Workstream '{id}' not found")
else:
return results
Loading

0 comments on commit 4af7342

Please sign in to comment.