-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/api endpoints with 100% test coverage (#13)
* 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
Showing
7 changed files
with
313 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,3 +17,4 @@ __pycache__ | |
# Ignore python local install build symlinks et al | ||
.venv | ||
.tool-versions | ||
.coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/` |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |