Skip to content

Commit

Permalink
Feature/api endpoints with 100% test coverage (#13)
Browse files Browse the repository at this point in the history
* Add enpoint to request ballot

* Return alameda_ca.json at vote endpoint

* Update gitignore for test coverage, add version

* Use importlib for json data

* Use pytest fixture to capture VoteStoreID

* Update README: install & test

* Update README: uvicorn commands

* Remove stray character
  • Loading branch information
stratofax authored Mar 8, 2023
1 parent 840033f commit b27828f
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ __pycache__
# Ignore python local install build symlinks et al
.venv
.tool-versions
.coverage
86 changes: 82 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,85 @@
# Repo for a potential spring 2023 Votetracker+ demo
# VoteTrackerPlus Web API

This is a very experimental repository to hold the front and backend code to support a live participatory VoteTracker+ demo, potentially in the spring of 2023.
A FastAPI interface to the VoteTrackerPlus (VTP) backend, to support a live participatory demo in the spring of 2023.

There is an initial mind map located at [https://www.mindmeister.com/map/2534840002?t=2nMk3h9Uha](https://www.mindmeister.com/map/2534840002?t=2nMk3h9Uha)
## Useful Links

An initial design document can be found [docs/DesignNotes.md](docs/DesignNotes.md)
- For an overview of the demo project, check out [the project mind map](https://www.mindmeister.com/map/2534840002?t=2nMk3h9Uha).
- This repo also includes the official [Design Notes](docs/DesignNotes.md).
- The API endpoints in this project are a web interface to the [VoteTrackerPlus backend](https://github.com/TrustTheVote-Project/VoteTrackerPlus).

## Installation

This Python project uses [poetry](https://python-poetry.org/) for dependency and package management. To run the code in this repo, first [install poetry](https://python-poetry.org/docs/#installation) on your development workstation. Then,

1. Clone or copy the code to a directory where you keep your GitHub repositories.
2. Enter the `VTP-web-api` directory you just created, like this: `cd ~/repos/VTP-web-api`
3. To install the required Python packages, run `poetry install`
4. To use the new virtual environment you just created, and run the API server & tests (see below for details), run `poetry shell`

Note: if you want to use a certain Python version, you can tell poetry, like this:

```bash
poetry env use 3.9
```

This command will set up a virtual environment using Python 3.9. Note that the specified Python version must already be installed on your computer.

This project requires Python 3.9 or later.

## Run the API server

1. Once the installation is complete, go to the source code directory: `cd src/vtp/web/api`
2. Run the `uvicorn` server like this: `uvicorn main:app`

If the poetry shell is active (see **Installation** above for details), you should see some output that looks like this:

```bash
➤ uvicorn main:app
INFO: Started server process [288056]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
```

To test the API endpoints using the uvicorn server, go to the URL specified in your favorite browser. You'll see the version information for this API server.

Note that you can specify the IP address and port number you want the uvicorn server to use, but we're going to use the defaults here (<http://127.0.0.1:8000>).

If you want to update the code that controls the API endpoints, and see the changes on the uvicorn server as soon as you save your code, add the `--reload` switch, like this:

```bash
uvicorn main:app --reload
```

## Testing the API endpoints in your browser

Here are some examples of the API endpoints you can access when the uvicorn server is running. For the latest list of API endpoints, please review the code in `main.py`.

### Request a Voter ID

To request an empty ballot, the web client first needs to receive a VoteStoreID. This indicates that the VTP backend has created a git repository to store the voter's cast ballot. The VoteStoreID matches the voter with their vote store repository.

To request a VoteStoreID, go to this endpoint: `http://127.0.0.1:8000/vote/`

You'll receive a VoteStoreID, like this:

```json
{"VoteStoreID": "206203"}
```

For the next step, copy the VoteStoreID.

### Request a blank ballot

To request a blank ballot, the client needs to provide a valid VoteStoreID. If you've copied the VoteStoreID from the "/vote/" endpoint, you can request a ballot like this:`http://127.0.0.1:8000/vote/206203`

If you provide an ID that doesn't match an existing ID, you'll get an error message back, but if you provide a valid ID, you'll get an empty ballot back from the server in JSON format.

## Testing

If you'd rather not test the API by hand, you can use pytest. Note that you still need an active `poetry shell` environment.

1. Go to the root of the repo, like this: `cd ~/repos/VTP-web-api` -- of course, your path may vary!
2. Run `pytest`
3. To see a table of code coverage, run `pytest --cov-report term-missing --cov=src/`
37 changes: 36 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ python = "^3.9"
fastapi = "^0.92.0"
httpx = "^0.23.3"
uvicorn = "^0.20.0"
importlib-resources = "^5.12.0"

[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
Expand Down
146 changes: 146 additions & 0 deletions src/vtp/web/api/data/alameda_ca.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
{
"active_ggos": [
".",
"GGOs/states/California",
"GGOs/states/California/GGOs/counties/Alameda",
"GGOs/states/California/GGOs/towns/Alameda"
],
"ballot_node": "GGOs/states/California/GGOs/towns/Alameda",
"ballot_subdir": "GGOs/states/California/GGOs/towns/Alameda",
"contests": {
"GGOs/states/California": [
{
"US president": {
"choices": [
{
"name": "Mitt Romney",
"party": "Circle Party"
},
{
"name": "Phil Scott",
"party": "Circle Party"
},
{
"name": "Ron DeSantis",
"party": "Circle Party"
},
{
"name": "Kamala Harris",
"party": "Triangle Party"
},
{
"name": "Cory Booker",
"party": "Triangle Party"
},
{
"name": "Beta O'rourke",
"party": "Triangle Party"
}
],
"tally": "rcv",
"uid": "0000"
}
},
{
"US senate": {
"choices": [
{
"name": "Larry Hogan",
"party": "Circle Party"
},
{
"name": "Greg Abbott",
"party": "Circle Party"
},
{
"name": "Pramila Jayapal",
"party": "Triangle Circle Party"
},
{
"name": "Alexandria Ocasio-Cortez",
"party": "Triangle Party"
}
],
"tally": "rcv",
"uid": "0001"
}
},
{
"governor": {
"choices": [
{
"name": "Brian Kemp",
"party": "Circle Party"
},
{
"name": "Bernie Sanders",
"party": "Triangle Party"
}
],
"tally": "plurality",
"uid": "0002"
}
}
],
"GGOs/states/California/GGOs/counties/Alameda": [
{
"County Clerk": {
"choices": [
"Jean-Luc Picard",
"Huckleberry Finn",
"Peggy Carter"
],
"tally": "plurality",
"uid": "0003"
}
}
],
"GGOs/states/California/GGOs/towns/Alameda": [
{
"mayor": {
"choices": [
{
"name": "Twenty Seven",
"party": "Circle Party"
},
{
"name": "Twenty Eight",
"party": "Triangle Party"
},
{
"name": "Jane Doe",
"party": "Pentagon Party"
},
{
"name": "John Doe",
"party": "Rectangle Party"
}
],
"tally": "rcv",
"uid": "0005"
}
},
{
"Question 1 - school budget override": {
"choices": [
true,
false
],
"tally": "plurality",
"uid": "0006"
}
},
{
"Question 2 - new firehouse land purchase": {
"choices": [
true,
false
],
"tally": "plurality",
"uid": "0007",
"win-by": "2/3"
}
}
]
}
}
27 changes: 24 additions & 3 deletions src/vtp/web/api/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
"""API endpoints for the VoteTrackerPlus backend"""
import json
import random

from fastapi import FastAPI
from importlib_resources import files

import vtp.web.api.data

app = FastAPI()

# create a list to store VoteStoreIDs
vote_store_ids = []
# read empty ballot from JSON file
ballot_data = files(vtp.web.api.data).joinpath("alameda_ca.json").read_text()
empty_ballot = json.loads(ballot_data)


@app.get("/")
async def root() -> dict:
"""Test that API is working"""
return {"message": "Hello World"}
"""Demonstrate that API is working"""
return {"version": "0.1.0"}


@app.get("/vote/")
async def get_vote_store_id() -> dict:
"""Get a unique Vote Store ID for each client"""
"""Create and store a unique Vote Store ID for each client"""
vote_store_id = str(random.randrange(100000, 999999))
# add VoteStoreID to list
vote_store_ids.append(vote_store_id)
return {"VoteStoreID": vote_store_id}


@app.get("/vote/{vote_store_id}")
async def get_empty_ballot(vote_store_id: str) -> dict:
"""Return an empty ballot for a given Vote Store ID"""
if vote_store_id in vote_store_ids:
return {"ballot": f"{empty_ballot}"}
else:
return {"error": "VoteStoreID not found"}
25 changes: 23 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import pytest
from fastapi.testclient import TestClient

from vtp.web.api.main import app

client = TestClient(app)


def test_read_main():
def test_get_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
assert "version" in response.json()


@pytest.fixture
def test_get_vote_store_id():
response = client.get("/vote/")
assert response.status_code == 200
assert "VoteStoreID" in response.json()
# retrieve VoteStoreID from response
return response.json()["VoteStoreID"]


def test_get_empty_ballot(test_get_vote_store_id):
# test with invalid VoteStoreID
response = client.get("/vote/00000X")
assert response.status_code == 200
assert "error" in response.json()
# test with valid VoteStoreID
response = client.get(f"/vote/{test_get_vote_store_id}")
assert response.status_code == 200
assert "ballot" in response.json()

0 comments on commit b27828f

Please sign in to comment.