Skip to content

Commit e1cc706

Browse files
rkorsakkeithrfung
authored andcommitted
✨ Accumulate tally endpoint (#62)
* New endpoint for tally accumulation * Update the Postman collection with sample tally accumulation requests
1 parent baa69f6 commit e1cc706

File tree

3 files changed

+88
-39
lines changed

3 files changed

+88
-39
lines changed

app/api/v1/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from fastapi import APIRouter
22

3-
from app.api.v1.endpoints import ballot, election, key, ping
3+
from app.api.v1.endpoints import ballot, election, key, ping, tally
44

55
api_router = APIRouter()
66
api_router.include_router(ping.router, prefix="/ping", tags=["ping"])
77
api_router.include_router(election.router, prefix="/election", tags=["election"])
88
api_router.include_router(ballot.router, prefix="/ballot", tags=["ballot"])
99
api_router.include_router(key.router, prefix="/key", tags=["key"])
10+
api_router.include_router(tally.router, prefix="/tally", tags=["tally"])

app/api/v1/endpoints/tally.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from electionguard.ballot import CiphertextAcceptedBallot
2+
from electionguard.election import (
3+
CiphertextElectionContext,
4+
ElectionDescription,
5+
InternalElectionDescription,
6+
)
7+
from electionguard.tally import (
8+
publish_ciphertext_tally,
9+
CiphertextTally,
10+
PublishedCiphertextTally,
11+
)
12+
from fastapi import APIRouter, Body, HTTPException
13+
from pydantic import BaseModel
14+
from typing import Any, List, Optional
15+
16+
router = APIRouter()
17+
18+
19+
class AccumulateTallyRequest(BaseModel):
20+
ballots: List[Any]
21+
encrypted_tally: Optional[Any] = None
22+
description: Any
23+
context: Any
24+
25+
26+
@router.post("/accumulate")
27+
def accumulate_tally(request: AccumulateTallyRequest = Body(...)) -> Any:
28+
"""
29+
Accumulate ballots into a new or existing tally
30+
"""
31+
32+
ballots = [
33+
CiphertextAcceptedBallot.from_json_object(ballot) for ballot in request.ballots
34+
]
35+
description = ElectionDescription.from_json_object(request.description)
36+
internal_description = InternalElectionDescription(description)
37+
context = CiphertextElectionContext.from_json_object(request.context)
38+
published_tally: Optional[PublishedCiphertextTally] = (
39+
PublishedCiphertextTally.from_json_object(request.encrypted_tally)
40+
if request.encrypted_tally
41+
else None
42+
)
43+
44+
tally = _get_new_or_existing_tally(internal_description, context, published_tally)
45+
46+
tally_succeeded = tally.batch_append(ballots)
47+
48+
if tally_succeeded:
49+
published_tally = publish_ciphertext_tally(tally)
50+
return published_tally.to_json_object()
51+
else:
52+
raise HTTPException(
53+
status_code=500, detail="Tally accumulation was unsuccessful"
54+
)
55+
56+
57+
def _get_new_or_existing_tally(
58+
description: ElectionDescription,
59+
context: CiphertextElectionContext,
60+
published_tally: PublishedCiphertextTally,
61+
) -> CiphertextTally:
62+
if not published_tally:
63+
return CiphertextTally("election-results", description, context)
64+
65+
full_tally = CiphertextTally(published_tally.object_id, description, context)
66+
full_tally.cast = published_tally.cast
67+
68+
return full_tally

tests/ElectionGuard Web Api.postman_collection.json

Lines changed: 18 additions & 38 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)