diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 09647b1..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__ -*.pyc -*.pyo -*.pyd diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 07cf37c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: 🐞 Bug -description: Submit a bug if something isn't working as expected. -title: "🐞 " -labels: [bug, triage] -body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the bug you encountered. - options: - - label: I have searched the existing issues - required: true - - type: textarea - attributes: - label: Current Behavior - description: A concise description of what you're experiencing. - validations: - required: true - - type: textarea - attributes: - label: Expected Behavior - description: A concise description of what you expected to happen. - validations: - required: false - - type: textarea - attributes: - label: Steps To Reproduce - description: Steps to reproduce the behavior. - placeholder: | - 1. In this environment... - 2. With this config... - 3. Run '...' - 4. See error... - validations: - required: false - - type: textarea - attributes: - label: Environment - description: | - examples: - - **OS**: Ubuntu 20.04 - value: | - - OS: - render: markdown - validations: - required: false - - type: textarea - attributes: - label: Anything else? - description: | - Links? References? Screenshots? Possible Solution? Anything that will give us more context about the issue you are encountering! - - Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 9239782..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: Discussions - url: https://github.com/microsoft/electionguard/discussions - about: Discuss suggestions and new enhancements here. - - name: ElectionGuard Info - url: https://https://www.electionguard.vote/ - about: Learn more about ElectionGuard or email the team. diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml deleted file mode 100644 index 899d79f..0000000 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: ✨ Enhancement -description: Suggest an enhancement or an improvement. -title: "✨ <title>" -labels: [enhancement, triage] -body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the suggestion. - options: - - label: I have searched the existing issues - required: true - - type: textarea - attributes: - label: Suggestion - description: Tell us how we could improve ElectionGuard. - validations: - required: true - - type: textarea - attributes: - label: Possible Implementation - description: Not obligatory, but ideas as to the implementation of the suggestion. - validations: - required: false - - type: textarea - attributes: - label: Anything else? - description: | - What are you trying to accomplish? - - Links? References? Anything that will give us more context about the suggestion! - - Tip: You can attach images by clicking this area to highlight it and then dragging files in. - validations: - required: false diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index f851407..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,12 +0,0 @@ -[//]: # (🚨 Please review the CONTRIBUTING.md in this repository. 💔Thank you!) - -### Issue -*Link your PR to an issue* - -Fixes #___ - -### Description -*Please describe your pull request.* - -### Testing -*Describe the best way to test or validate your PR.* diff --git a/.github/workflows/azure_deploy.yml b/.github/workflows/azure_deploy.yml deleted file mode 100644 index 7085d16..0000000 --- a/.github/workflows/azure_deploy.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Azure_Deploy_Workflow -on: - push: - branches: - - main - milestone: - types: [closed] - repository_dispatch: - types: [milestone_closed] - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: "Checkout GitHub Action" - uses: actions/checkout@main - - - name: "Login via Azure CLI" - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: "Build and push image" - uses: azure/docker-login@v1 - with: - login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - - run: | - docker build . -t ${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-api-python:${{ github.sha }} -t ${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-api-python:latest - docker push ${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-api-python --all-tags - - - name: "Deploy All to Azure Container Instances" - uses: "pierreVH2/azure-containergroup-deploy@master" - with: - resource-group: ${{ secrets.RESOURCE_GROUP }} - group-name: electionguard-demo - registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - registry-username: ${{ secrets.REGISTRY_USERNAME }} - registry-password: ${{ secrets.REGISTRY_PASSWORD }} - location: "east us" - containers: '[ - { - "name": "electionguard-ui-app", - "image": "${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-ui:latest", - "command": "serve -l 3000 -s build", - "cpu": 0.5, - "memory": 1.5, - "ports": "3000" - }, - { - "name": "electionguard-ui-storybook", - "image": "${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-ui:latest", - "command": "serve -l 6006", - "cpu": 1, - "memory": 1.5, - "ports": "6006" - }, - { - "name": "electionguard-api-python-mediator", - "image": "${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-api-python:latest", - "environmentVariables": "API_MODE=\"mediator\" QUEUE_MODE=\"remote\" STORAGE_MODE=\"mongo\" PROJECT_NAME=\"ElectionGuard Mediator API\" PORT=8000 MESSAGEQUEUE_URI=\"amqp://guest:guest@electionguard-message-queue:5672\" MONGODB_URI=${{ secrets.COSMOSDB_URI }}", - "cpu": 1, - "memory": 1.5, - "ports": "8000" - }, - { - "name": "electionguard-api-python-guardian", - "image": "${{ secrets.DEPLOY_REGISTRY }}.azurecr.io/electionguard-api-python:latest", - "environmentVariables": "API_MODE=\"guardian\" QUEUE_MODE=\"remote\" STORAGE_MODE=\"mongo\" PROJECT_NAME=\"ElectionGuard Guardian API\" PORT=8001 MESSAGEQUEUE_URI=\"amqp://guest:guest@electionguard-message-queue:5672\" MONGODB_URI=${{ secrets.COSMOSDB_URI }}", - "cpu": 1, - "memory": 1.5, - "ports": "8001" - }, - { - "name": "electionguard-message-queue", - "image": "rabbitmq:3.8.16-management-alpine", - "cpu": 0.5, - "memory": 2, - "ports": "5672 15672" - }]' diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml deleted file mode 100644 index c60e12f..0000000 --- a/.github/workflows/pull_request.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Validate Pull Request - -on: - pull_request: - branches: - - main - repository_dispatch: - types: [pull_request] - -env: - PYTHON_VERSION: 3.9 - -jobs: - code_analysis: - name: Code Analysis - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Change Directory - run: cd ${{ github.workspace }} - - name: Setup Environment - run: make environment - - name: Lint - run: make lint - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: python - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 - - linux_check: - name: Linux Check - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Change Directory - run: cd ${{ github.workspace }} - - name: Setup Environment - run: make environment - - name: Run Integration Tests - run: make test-integration -# - name: Run Postman Tests -# run: make docker-postman-test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1faf9fb..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Release Build - -on: - milestone: - types: [closed] - repository_dispatch: - types: [milestone_closed] - -env: - PYTHON_VERSION: 3.9 - -jobs: - code_analysis: - name: Code Analysis - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Change Directory - run: cd ${{ github.workspace }} - - name: Setup Environment - run: make environment - - name: Lint - run: make lint - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: python - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 - - linux_check: - name: Linux Check - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Change Directory - run: cd ${{ github.workspace }} - - name: Setup Environment - run: make environment - - name: Run Integration Tests - run: make test-integration - - release: - name: Release - runs-on: ubuntu-latest - needs: [code_analysis, linux_check] - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v1 - with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Change Directory - run: cd ${{ github.workspace }} - - name: Poetry Install - run: pip install poetry - - name: Get Version - run: echo "name=PACKAGE_VERSION::$(echo $VERSION | poetry version | cut -c23-27)" >> $GITHUB_ENV - - name: Create Release - id: create_release - uses: actions/create-release@v1.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ env.PACKAGE_VERSION }} - release_name: Release ${{ env.PACKAGE_VERSION }} - draft: false - prerelease: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKER_TOKEN }} - - name: Build and Push to DockerHub - id: docker_build - uses: docker/build-push-action@v2 - with: - push: true - tags: | - electionguard/electionguard-web-api:latest - electionguard/electionguard-web-api:${{ env.PACKAGE_VERSION }} - - name: Verify Docker Hub image - run: echo ${{ steps.docker_build.outputs.digest }} - - name: Push to GitHub Packages - uses: docker/build-push-action@v1 - with: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - registry: docker.pkg.github.com - repository: microsoft/electionguard-web-api/electionguard-web-api - tag_with_ref: true - - name: Deploy Github Pages - run: make docs-deploy-ci diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9d3e9d9..0000000 --- a/.gitignore +++ /dev/null @@ -1,147 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# VS Code -*.code-workspace - -# Mac -.DS_Store - -# Project -storage/ diff --git a/app/api/__init__.py b/.nojekyll similarity index 100% rename from app/api/__init__.py rename to .nojekyll diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 46a915b..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. - // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp - - // List of extensions which should be recommended for users of this workspace. - "recommendations": [ - "ms-python.python", - "visualstudioexptteam.vscodeintellicode", - "magicstack.magicpython", - "hbenl.vscode-test-explorer", - "littlefoxteam.vscode-python-test-adapter", - "cschleiden.vscode-github-actions" - ], - // List of extensions recommended by VS Code that should not be recommended for users of this workspace. - "unwantedRecommendations": [] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index e3b7910..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Guardian Web API", - "type": "python", - "request": "launch", - "program": "${workspaceRoot}/app/main.py", - "console": "integratedTerminal", - "args": [ - "--port", - "8001" - ], - "env": { - "PYTHONPATH": "${workspaceRoot}", - "API_MODE": "guardian", - "QUEUE_MODE": "remote", - "STORAGE_MODE": "local_storage" - } - }, - { - "name": "Mediator Web API", - "type": "python", - "request": "launch", - "program": "${workspaceRoot}/app/main.py", - "console": "integratedTerminal", - "args": [ - "--port", - "8000" - ], - "env": { - "PYTHONPATH": "${workspaceRoot}", - "API_MODE": "mediator", - "QUEUE_MODE": "remote", - "STORAGE_MODE": "local_storage" - } - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 02aea16..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "python.formatting.provider": "black", - "python.linting.mypyEnabled": true, - "python.linting.pylintEnabled": true, - "python.linting.pylintUseMinimalCheckers": false, - "python.linting.pylintArgs": [ - "--extension-pkg-whitelist=pydantic" - ], - "python.linting.enabled": true, - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.pytestEnabled": true, - "python.testing.pytestArgs": [], - "python.testing.nosetestsEnabled": false, - "python.testing.unittestEnabled": false, - "pythonTestExplorer.testFramework": "pytest", - "files.exclude": { - "**/__pycache__": true, - "**/.hypothesis": true, - "**/.mypy_cache": true, - "**/.pytest_cache": true, - "**/**.egg-info": true - }, - "editor.formatOnSave": true, - "python.venvPath": ".venv", - "python.pythonPath": ".venv/bin/python" -} \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..8fdb99e --- /dev/null +++ b/404.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<!--[if IE 8]><html class="no-js lt-ie9" lang="en" > <![endif]--> +<!--[if gt IE 8]><!--> <html class="no-js" lang="en" > <!--<![endif]--> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <meta name="author" content="Microsoft"> + + <link rel="shortcut icon" href="/img/favicon.ico"> + <title>ElectionGuard Web Api + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+
    +
  • Docs »
  • + + +
  • + +
  • +
+ +
+
+
+
+ + +

404

+ +

Page not found

+ + +
+
+ + +
+
+ +
+ +
+ +
+ + + GitHub + + + + +
+ + + + + + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index c72a574..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,9 +0,0 @@ -# Microsoft Open Source Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). - -Resources: - -- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) -- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c97a969..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,27 +0,0 @@ -## Contributing - -This project welcomes contributions and suggestions. Before you get started, you should read the [readme](README.md). - -- 🤔 **CONSIDER** adding a unit test if your PR resolves an issue. -- ✅ **DO** check open PR's to avoid duplicates. -- ✅ **DO** keep pull requests small so they can be easily reviewed. -- ✅ **DO** build locally before pushing. -- ✅ **DO** make sure tests pass. -- ✅ **DO** make sure any new changes are documented. -- ✅ **DO** make sure not to introduce any compiler warnings. -- ❌**AVOID** breaking the continuous integration build. -- ❌**AVOID** making significant changes to the overall architecture. - -### Creating a Pull Request - -All pull requests should have an accompanying issue. Create one if there is not one matching your code. The code will be checked by continuous integration. Once this CI passes, the code will be reviewed, ideally approved, then merged. - -### CLA - -Open source contributions require you to agree to a standard Microsoft Contributor License Agreement (CLA) declaring that you grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. - -### Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8166243..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9 AS base -ENV PORT 8000 -RUN apt update && apt-get install -y \ - libgmp-dev \ - libmpfr-dev \ - libmpc-dev -RUN pip install 'poetry==1.1.6' -COPY ./pyproject.toml /tmp/ -COPY ./poetry.lock /tmp/ -RUN cd /tmp && poetry export -f requirements.txt > requirements.txt -RUN pip install -r /tmp/requirements.txt -EXPOSE $PORT - -FROM base AS dev -VOLUME [ "/app/app" ] -CMD /start-reload.sh - -FROM base AS prod -COPY ./app /app/app -# The base image will start gunicorn diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 3d8b93b..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE diff --git a/Makefile b/Makefile deleted file mode 100644 index a2763c1..0000000 --- a/Makefile +++ /dev/null @@ -1,165 +0,0 @@ -.PHONY: all environment lint start - -OS ?= $(shell python -c 'import platform; print(platform.system())') -IMAGE_NAME = electionguard_web_api -AZURE_LOCATION = eastus -RESOURCE_GROUP = EG-Deploy-Demo -DEPLOY_REGISTRY = deploydemoregistry -REGISTRY_SKU = Basic -ACI_CONTEXT = egacicontext -TENANT_ID = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -GROUP_EXISTS ?= $(shell az group exists --name $(RESOURCE_GROUP)) - -# Supports either "guardian" or "mediator" modes -API_MODE ?= mediator -ifeq ($(API_MODE), mediator) -PORT ?= 8000 -else -PORT ?= 8001 -endif - -all: environment lint start - -environment: no-windows - @echo 🔧 SETUP - make install-gmp - pip3 install 'poetry==1.1.6' - poetry config virtualenvs.in-project true - poetry install - -install: no-windows - @echo 🔧 INSTALL - poetry install - -install-gmp: no-windows - @echo 📦 Install Module - @echo Operating System identified as $(OS) -ifeq ($(OS), Linux) - make install-gmp-linux -endif -ifeq ($(OS), Darwin) - make install-gmp-mac -endif - -install-gmp-mac: no-windows - @echo 🍎 MACOS INSTALL -# gmpy2 requirements - brew install gmp || true - brew install mpfr || true - brew install libmpc || true - -install-gmp-linux: no-windows - @echo 🐧 LINUX INSTALL -# gmpy2 requirements - sudo apt-get install libgmp-dev - sudo apt-get install libmpfr-dev - sudo apt-get install libmpc-dev - -# install azure command line -install-azure-cli: - @echo Install Azure CLI -ifeq ($(OS), Linux) - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash -endif -ifeq ($(OS), Darwin) - brew install azure-cli - az upgrade -endif -ifeq ($(OS), Windows) - Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /quiet'; rm .\AzureCLI.msi -endif - -# deploy to azure -deploy-azure: - @echo Deploy to Azure - az login --tenant $(TENANT_ID) -ifeq ($(GROUP_EXISTS), false) - az group create -l $(AZURE_LOCATION) -n $(RESOURCE_GROUP) -endif - az acr create --resource-group $(RESOURCE_GROUP) --name $(DEPLOY_REGISTRY) --sku $(REGISTRY_SKU) - az acr login --name $(DEPLOY_REGISTRY) - docker context use default - docker build . -t $(DEPLOY_REGISTRY).azurecr.io/electionguard-api-python:latest - docker push $(DEPLOY_REGISTRY).azurecr.io/electionguard-api-python:latest - docker login azure --tenant-id $(TENANT_ID) -# docker context create aci $(ACI_CONTEXT) - docker context use $(ACI_CONTEXT) - docker compose -f docker-compose.azure.yml up - docker logout - docker context use default - az logout - -# Dev Server -start: no-windows - poetry run uvicorn app.main:app --reload --port $(PORT) - -start-server: - docker compose -f docker-compose.support.yml up -d - QUEUE_MODE=remote - STORAGE_MODE=mongo - poetry run uvicorn app.main:app --reload --port $(PORT) - -stop: - docker compose -f docker-compose.support.yml down - -# Docker -docker-build: - docker build -t $(IMAGE_NAME) . - -docker-run: - docker-compose up --build - -docker-dev: - docker-compose -f docker-compose.support.yml -f docker-compose.dev.yml up --build - -docker-postman-test: - @echo 🧪 RUNNING POSTMAN TESTS IN DOCKER - docker-compose \ - -f tests/postman/docker-compose.yml up \ - --build \ - --abort-on-container-exit \ - --exit-code-from test-runner - -# Linting -lint: - @echo 💚 LINT - @echo 1.Pylint - poetry run pylint --extension-pkg-whitelist=pydantic app tests - @echo 2.Black Formatting - poetry run black --diff --check app tests - @echo 3.Mypy Static Typing - poetry run mypy --config-file=pyproject.toml app tests - @echo 4.Documentation - poetry run mkdocs build --strict - -auto-lint: - poetry run black app tests - make lint - -test-integration: no-windows - @echo ✅ INTEGRATION TESTS - poetry run pytest -s . -x - -# Documentation -docs-serve: - poetry run mkdocs serve - -docs-build: - poetry run mkdocs build - -docs-deploy: - @echo 🚀 DEPLOY to Github Pages - poetry run mkdocs gh-deploy --force - -docs-deploy-ci: - @echo 🚀 DEPLOY to Github Pages - pip install mkdocs - mkdocs gh-deploy --force - -# PRIVATE RECIPIES - -no-windows: -ifeq ($(OS), Windows_NT) - @echo Windows is not supported. Instead run this command in WSL2. For more details see README.md. - exit 1 -endif diff --git a/README.md b/README.md deleted file mode 100644 index 2fa4033..0000000 --- a/README.md +++ /dev/null @@ -1,219 +0,0 @@ -![Microsoft Defending Democracy Program: ElectionGuard](https://raw.githubusercontent.com/microsoft/electionguard-web-api/main/images/electionguard-banner.svg) - -# 🗳️ ElectionGuard Web API - -[![docker version](https://img.shields.io/docker/v/electionguard/electionguard-web-api)](https://hub.docker.com/r/electionguard/electionguard-web-api) [![docker pulls](https://img.shields.io/docker/pulls/electionguard/electionguard-web-api)](https://hub.docker.com/r/electionguard/electionguard-web-api) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/microsoft/electionguard-web-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-web-api/context:python) [![Total alerts](https://img.shields.io/lgtm/alerts/g/microsoft/electionguard-web-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-web-api/alerts/) [![Documentation Status](https://readthedocs.org/projects/electionguard-web-api/badge/?version=latest)](https://electionguard-web-api.readthedocs.io) [![license](https://img.shields.io/github/license/microsoft/electionguard-web-api)](LICENSE) - -The ElectionGuard Web API is a python-based application that provides a **thin**, **stateless** wrapper around the [`electionguard-python`](https://github.com/microsoft/electionguard-python) library to perform ballot encryption, casting, spoiling, and tallying. This API is implemented using [FastAPI](https://fastapi.tiangolo.com/#interactive-api-docs). - -If you aren't familiar with ElectionGuard and its concepts, first take a stroll through [the official documentation](https://microsoft.github.io/electionguard-python/). - -## 👯‍♀️ Two APIs in One - -Before you begin you should be aware that the application can run in one of two modes: - -- `guardian` mode runs features used by Guardians (key ceremony actions, partial tally decryption, etc.) -- `mediator` mode runs features used by Mediators (ballot encryption, casting, spoiling, etc.) - -In practice, you will likely need to run at least one instance of each mode. We provide a single codebase and Docker image, but the mode can be set at runtime as described below. - -## ⭐ Getting Started - -This codebase can be run using one of three different approaches: - -1. 🐳 Running with Docker -2. 🐳 Developing with Docker -3. 🐍 Developing with Python - -## 🐳 1. Running with Docker - -This approach runs an official published image. This approach is not intended for development. It works on Windows, Mac, and Linux. - -For convenience the Docker image is hosted on both [Github Packages](https://github.com/microsoft/electionguard-web-api/packages/397920) and [DockerHub](https://hub.docker.com/r/electionguard/electionguard-web-api). You can choose whichever container image library works best for you. - -### 1.1 Pulling from Github Packages - -**Note:** _GitHub Packages requires authentication to retrieve the package. This requires a GitHub Access Token and using `docker login`. [Follow GitHub instructions](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-with-a-personal-access-token)._ - -```bash -# Pull the image from Github -docker pull docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main - -# Start a container for the API in mediator mode, exposed on port 80 of the host machine -docker run -d -p 80:8000 --env API_MODE=mediator docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main -``` - -### 1.2 Pulling from DockerHub - -Pulling from DockerHub is simpler as it requires no additional authentication. - -```bash -# Pull the image from DockerHub -docker pull electionguard/electionguard-web-api:latest - -# Start a container for the API in mediator mode, exposed on port 80 of the host machine -docker run -d -p 80:8000 --env API_MODE=mediator electionguard/electionguard-web-api:latest -``` - -## 🐳 2. Developing with Docker - -Developing with Docker is the fastest approach for getting started because it has virtually no local dependencies (e.g. Python). This approach works on Windows, Mac, and Linux. It uses a [Dockerfile](Dockerfile) and [docker-compose.yml](docker-compose.yml). - -### ✅ 2.1 Prerequisities - -- [GNU Make](https://www.gnu.org/software/make/manual/make.html) is required to simplify commands and GitHub Actions. For MacOS and Linux, no action is necessary as it pre-installed. For Windows, install via [Chocolatey](https://chocolatey.org/install) and the [make package](https://chocolatey.org/packages/make), or alternately [manually install](http://gnuwin32.sourceforge.net/packages/make.htm). -- [Docker Desktop](https://www.docker.com/products/docker-desktop) is required for Docker support - -### 🏃‍♀️ 2.2 Running 🏃‍♂️ - -To get started run both APIs at the same time: - -```bash -make docker-run -``` - -Or run both APIs in development mode, with automatic reloading on file change: - -```bash -make docker-dev -``` - -After either command, you will find the `mediator` API running at http://127.0.0.1:8000 and the `guardian` API at http://127.0.0.1:8001 - -## 🐍 3. Developing with Python - -Developing with Python provides the fastest developer inner loop (speed from code changes to seeing effects of changes), but is more work to set up initially. It works on Mac and Linux. It also works via [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/about) on Windows. - -### ✅ 3.1. Windows Prerequisites - -On Windows you can use an IDE of your choice in Windows, and run the make and Python commands in WSL which will expose API's in Windows. Developing with Python on Windows involves the following additional setup that is not required for Linux or Mac. - -1. Install [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install) -2. Install [Ubuntu](https://www.microsoft.com/en-us/p/ubuntu/9nblggh4msv6?ocid=9nblggh4msv6_ORSEARCH_Bing&rtc=1&activetab=pivot:overviewtab) (other Linux distributions should also work with minor modifications to the instructions below) -3. Install [pyenv prerequisites](https://github.com/pyenv/pyenv/wiki#suggested-build-environment). Technically you could just install Python and it would be simpler, but this approach will provide more flexibility. To install the prerequisites: - -```bash -sudo apt-get update -sudo apt-get install make build-essential libssl-dev zlib1g-dev \ - libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \ - libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev -``` - -4. Install pyenv via [pyenv-installer](https://github.com/pyenv/pyenv-installer) and add it the startup scripts: - -```bash -curl https://pyenv.run | bash -echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc -echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc -echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bashrc -sed -Ei -e '/^([^#]|$)/ {a \ - export PYENV_ROOT="$HOME/.pyenv" - a \ - export PATH="$PYENV_ROOT/bin:$PATH" - a \ - ' -e ':a' -e '$!{n;ba};}' ~/.profile -echo 'eval "$(pyenv init --path)"' >>~/.profile - -echo 'eval "$(pyenv init -)"' >> ~/.bashrc -``` - -5. Restart shell -6. Install Python 3.9 via pyenv - -```bash -pyenv install 3.9.9 -pyenv global 3.9.9 -``` - -### ✅ 3.2 Mac/Linux Prerequisites - -Install [Python 3.9](https://www.python.org/downloads/). We additionally recommend [pyenv](https://github.com/pyenv/pyenv) to assist with Python version management (see detailed instructions in the Windows section above starting with Step 3). - -### 🏃‍♀️ 3.2 Running 🏃‍♂️ - -Using [**make**](https://www.gnu.org/software/make/manual/make.html), install and setup the environment: - -```bash -make environment -``` - -Start the api as mediator - -```bash -make start API_MODE=mediator -``` - -OR as guardian - -```bash -make start API_MODE=guardian -``` - -### Debugging Mac/Linux - -For local debugging with Visual Studio Code, choose the `Guardian Web API` or `Mediator Web API` options from the dropdown in the Run menu. Once the server is up, you can easily hit your breakpoints. - -If the code fails to run, [make sure your Python interpreter is set](https://code.visualstudio.com/docs/python/environments) to use your poetry environment. - -### Debugging Windows - -With Visual Studio Code: - -1. Install the [Remote WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) extension -2. In the bottom left click the Green Icon and "New WSL Window using Distro", select Ubuntu -3. F5 -4. Choose either `Guardian Web API` or `Mediator Web API` - -## 🧪 Testing - -End-to-end integration tests can be found in the [`/tests/integration`](/tests/integration) folder. To see them in action, run: - -```bash -make test-integration -``` - -A Postman collection is also available to test the API located in the [`/tests/postman`](/tests/postman) folder. You can do a few things with this: - -- [Import into Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/#importing-data-into-postman) for easy manual testing. -- Run locally with the [Newman CLI](https://github.com/postmanlabs/newman). -- Run the APIs and tests entirely in Docker by running: - ```bash - make docker-postman-test - ``` - -## 📝 Documentation - -**FastApi** defaultly has API documentation built in. The following is available after running: - -- **[SwaggerUI](https://github.com/swagger-api/swagger-ui)** at [`http://127.0.0.1:8000/docs`](http://127.0.0.1:8000/docs) or [`http://127.0.0.1:8001/docs`](http://127.0.0.1:8001/docs), depending on the API mode - -- **[ReDoc](https://github.com/Redocly/redoc)** at [`http://127.0.0.1:8000/redoc`](http://127.0.0.1:8000/redoc) or [`http://127.0.0.1:8001/redoc`](http://127.0.0.1:8001/redoc) - -Overviews of the API itself are available on: - -- [GitHub Pages](https://microsoft.github.io/electionguard-web-api/) -- [Read the Docs](https://electionguard-web-api.readthedocs.io/) - -## 🗄 Archived - -As of 06/15/2020, the previous C wrapped implementation was transitioned to the python version. ElectionGuard development has transitioned to the [ElectionGuard-Python](https://github.com/microsoft/electionguard-python) Repo. The old version is available using the `dotnet-api` tag. - -## 🤝 Contributing - -This project encourages community contributions for development, testing, documentation, code review, and performance analysis, etc. For more information on how to contribute, see [the contribution guidelines](CONTRIBUTING.md) - -### Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -### Reporting Issues - -Please report any bugs, feature requests, or enhancements using the [GitHub Issue Tracker](https://github.com/microsoft/electionguard-web-api/issues). Please do not report any security vulnerabilities using the Issue Tracker. Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). See the [Security Documentation](SECURITY.md) for more information. - -### Have Questions? - -Electionguard would love for you to ask questions out in the open using GitHub Issues. If you really want to email the ElectionGuard team, reach out at electionguard@microsoft.com. - -## License - -This repository is licensed under the [MIT License](LICENSE) diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 7ab49eb..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). - - diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index 68cdeee..0000000 --- a/app/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "1.0.5" diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/v1/auth/__init__.py b/app/api/v1/auth/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1/auth/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1/auth/auth.py b/app/api/v1/auth/auth.py deleted file mode 100644 index e1ef5e6..0000000 --- a/app/api/v1/auth/auth.py +++ /dev/null @@ -1,159 +0,0 @@ -from typing import Any, List, Optional - -from datetime import datetime, timedelta - -from fastapi import ( - params, - APIRouter, - Depends, - HTTPException, - Request, - Security, - status, -) -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from jose import JWTError, jwt -from pydantic import ValidationError -from app.api.v1.models.auth import ErrorMessage - -from app.api.v1.models.user import UserScope -from app.core import Settings -from app.core.user import get_user_info - -from ..models import Token, TokenData - -from ....core import AuthenticationContext - -from ..tags import AUTHORIZE - -router = APIRouter() - -oauth2_scheme = OAuth2PasswordBearer( - tokenUrl="token", - scopes={ - UserScope.admin: "The admin role can execute administrative functions.", - UserScope.auditor: "The auditor role is a readonly role that can observe the election", - UserScope.guardian: "The guardian role can excute guardian functions.", - UserScope.voter: "The voter role can execute voting functions such as encrypt ballot.", - }, -) - - -class ScopedTo(params.Depends): - """Define a dependency on particular scope.""" - - username: str - - def __init__(self, scopes: List[UserScope]) -> None: - super().__init__(self.__call__) - self._scopes = scopes - - def __call__( - self, - token: str = Security(oauth2_scheme), - ) -> TokenData: - """Check scopes and return the current user.""" - data = validate_access_token(Settings(), token) - validate_access_token_authorization(data, self._scopes) - return data - - -def validate_access_token_authorization( - token_data: TokenData, scopes: List[UserScope] -) -> None: - """Validate that the access token is authorized to the requested resource.""" - if any(scopes): - scope_str = ",".join(scopes) - authenticate_value = f'Bearer scope="{scope_str}"' - else: - authenticate_value = "Bearer" - for scope in scopes: - if scope in token_data.scopes: - return - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not enough permissions", - headers={"WWW-Authenticate": authenticate_value}, - ) - - -def create_access_token( - data: dict, - expires_delta: Optional[timedelta] = None, - settings: Settings = Settings(), -) -> Any: - """Create an access token.""" - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta( - minutes=settings.AUTH_ACCESS_TOKEN_EXPIRE_MINUTES - ) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode( - to_encode, settings.AUTH_SECRET_KEY, algorithm=settings.AUTH_ALGORITHM - ) - return encoded_jwt - - -def validate_access_token( - settings: Settings = Settings(), token: str = Depends(oauth2_scheme) -) -> TokenData: - """validate the token contains a username and scopes""" - try: - payload = jwt.decode( - token, - settings.AUTH_SECRET_KEY, - algorithms=[settings.AUTH_ALGORITHM], - ) - username: str = payload.get("sub") - if username is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - token_scopes = payload.get("scopes") - token_data = TokenData(username=username, scopes=token_scopes) - except (JWTError, ValidationError) as internal_error: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credential scopes", - headers={"WWW-Authenticate": "Bearer"}, - ) from internal_error - return token_data - - -@router.post( - "/login", - response_model=Token, - tags=[AUTHORIZE], - responses={401: {"model": ErrorMessage}, 404: {"model": ErrorMessage}}, -) -async def login_for_access_token( - request: Request, form_data: OAuth2PasswordRequestForm = Depends() -) -> Token: - """Log in using the provided username and password.""" - authenticated = AuthenticationContext( - request.app.state.settings - ).authenticate_credential(form_data.username, form_data.password) - if not authenticated: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - # get the database cached user info - user_info = get_user_info(form_data.username, request.app.state.settings) - access_token_expires = timedelta( - minutes=request.app.state.settings.AUTH_ACCESS_TOKEN_EXPIRE_MINUTES - ) - access_token = create_access_token( - data={"sub": form_data.username, "scopes": user_info.scopes}, - expires_delta=access_token_expires, - ) - return Token(access_token=access_token, token_type="bearer") - - -# TODO: add refresh support diff --git a/app/api/v1/auth/routes.py b/app/api/v1/auth/routes.py deleted file mode 100644 index 7b9a42a..0000000 --- a/app/api/v1/auth/routes.py +++ /dev/null @@ -1,8 +0,0 @@ -from fastapi import APIRouter -from . import auth -from . import user - -router = APIRouter() - -router.include_router(auth.router, prefix="/auth") -router.include_router(user.router, prefix="/user") diff --git a/app/api/v1/auth/user.py b/app/api/v1/auth/user.py deleted file mode 100644 index 3a8efb3..0000000 --- a/app/api/v1/auth/user.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import Any -from base64 import b64encode, b16decode -from fastapi import APIRouter, Body, Depends, HTTPException, Request, status -from electionguard.serializable import write_json_object - -from electionguard.group import rand_q - -from app.api.v1.models.user import ( - CreateUserResponse, - UserQueryRequest, - UserQueryResponse, -) - -from .auth import ScopedTo - -from ..models import ( - AuthenticationCredential, - UserInfo, - UserScope, -) - -from ....core import ( - AuthenticationContext, - get_user_info, - set_user_info, - filter_user_info, - get_auth_credential, - set_auth_credential, - update_auth_credential, -) - -from ..tags import USER - -router = APIRouter() - - -@router.post( - "/find", - response_model=UserQueryResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[USER], -) -async def find_users( - request: Request, - skip: int = 0, - limit: int = 100, - data: UserQueryRequest = Body(...), -) -> UserQueryResponse: - """ - Find users. - - Search the repository for users that match the filter criteria specified in the request body. - If no filter criteria is specified the API will return all users. - """ - filter = write_json_object(data.filter) if data.filter else {} - users = filter_user_info(filter, skip, limit, request.app.state.settings) - return UserQueryResponse(users=users) - - -scoped_to_any = ScopedTo( - [UserScope.admin, UserScope.auditor, UserScope.guardian, UserScope.voter] -) - - -@router.get( - "/me", - response_model=UserInfo, - dependencies=[ - ScopedTo( - [UserScope.admin, UserScope.auditor, UserScope.guardian, UserScope.voter] - ) - ], - tags=[USER], -) -async def me( - request: Request, token_data: ScopedTo = Depends(scoped_to_any) -) -> UserInfo: - """ - Get user info for the current logged in user. - """ - - if token_data.username is None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="User not specified" - ) - - current_user = get_user_info(token_data.username, request.app.state.settings) - - if current_user.disabled: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user" - ) - return current_user - - -@router.put( - "", - response_model=CreateUserResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[USER], -) -def create_user( - request: Request, - user_info: UserInfo = Body(...), -) -> CreateUserResponse: - """Create a new user.""" - - if any( - filter_user_info( - filter={"username": user_info.username}, - skip=0, - limit=1, - settings=request.app.state.settings, - ) - ): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User already exists" - ) - - # TODO: generate passwords differently - new_password = b64encode(b16decode(rand_q().to_hex()[0:16])) - hashed_password = AuthenticationContext( - request.app.state.settings - ).get_password_hash(new_password) - credential = AuthenticationCredential( - username=user_info.username, hashed_password=hashed_password - ) - - set_auth_credential(credential, request.app.state.settings) - set_user_info(user_info, request.app.state.settings) - - return CreateUserResponse(user_info=user_info, password=new_password) - - -@router.post( - "/reset_password", - dependencies=[ScopedTo([UserScope.admin])], - tags=[USER], -) -async def reset_password(request: Request, username: str) -> Any: - """Reset a user's password.""" - - credential = get_auth_credential( - username, - settings=request.app.state.settings, - ) - - # TODO: generate passwords differently - new_password = b64encode(b16decode(rand_q().to_hex()[0:16])) - credential.hashed_password = AuthenticationContext( - request.app.state.settings - ).get_password_hash(new_password) - - update_auth_credential(credential, request.app.state.settings) - - return {"username": username, "password": new_password} diff --git a/app/api/v1/common/__init__.py b/app/api/v1/common/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1/common/ping.py b/app/api/v1/common/ping.py deleted file mode 100644 index 1c1ded7..0000000 --- a/app/api/v1/common/ping.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any - -from fastapi import APIRouter - -from ..tags import UTILITY - -router = APIRouter() - - -@router.get("", response_model=str, tags=[UTILITY]) -def ping() -> Any: - """ - Ensure API can be pinged - """ - return "pong" diff --git a/app/api/v1/common/routes.py b/app/api/v1/common/routes.py deleted file mode 100644 index 87ede48..0000000 --- a/app/api/v1/common/routes.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter -from . import ping - -router = APIRouter() - -router.include_router(ping.router, prefix="/ping") diff --git a/app/api/v1/common/type_mapper.py b/app/api/v1/common/type_mapper.py deleted file mode 100644 index 8e5b591..0000000 --- a/app/api/v1/common/type_mapper.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Union -from electionguard.group import ( - ElementModP, - ElementModQ, - hex_to_p, - hex_to_q, - int_to_p, - int_to_q, -) - - -def string_to_element_mod_p(value: Union[int, str]) -> ElementModP: - element = int_to_p(value) if isinstance(value, int) else hex_to_p(value) - if element is None: - raise ValueError(type_error_message(str(value), "element_mod_p")) - return element - - -def string_to_element_mod_q(value: Union[int, str]) -> ElementModQ: - element = int_to_q(value) if isinstance(value, int) else hex_to_q(value) - if element is None: - raise ValueError(type_error_message(str(value), "element_mod_q")) - return element - - -def type_error_message(value: str, type: str) -> str: - return f"{value} cannot be converted to {type}." diff --git a/app/api/v1/guardian/__init__.py b/app/api/v1/guardian/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1/guardian/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1/guardian/ballot.py b/app/api/v1/guardian/ballot.py deleted file mode 100644 index 588065c..0000000 --- a/app/api/v1/guardian/ballot.py +++ /dev/null @@ -1,48 +0,0 @@ -from electionguard.ballot import SubmittedBallot -from electionguard.decryption import compute_decryption_share_for_ballot -from electionguard.election import CiphertextElectionContext -from electionguard.key_ceremony import ElectionKeyPair -from electionguard.scheduler import Scheduler -from electionguard.serializable import read_json_object, write_json_object -from fastapi import APIRouter, Body, Depends - -from app.core.scheduler import get_scheduler -from ..models import ( - DecryptBallotSharesRequest, - DecryptBallotSharesResponse, -) -from ..tags import TALLY - -router = APIRouter() - - -@router.post( - "/decrypt-shares", response_model=DecryptBallotSharesResponse, tags=[TALLY] -) -def decrypt_ballot_shares( - request: DecryptBallotSharesRequest = Body(...), - scheduler: Scheduler = Depends(get_scheduler), -) -> DecryptBallotSharesResponse: - """ - Decrypt this guardian's share of one or more ballots - """ - ballots = [ - SubmittedBallot.from_json_object(ballot) for ballot in request.encrypted_ballots - ] - context = CiphertextElectionContext.from_json_object(request.context) - election_key_pair = read_json_object( - request.guardian.election_keys, ElectionKeyPair - ) - - shares = [ - compute_decryption_share_for_ballot( - election_key_pair, ballot, context, scheduler - ) - for ballot in ballots - ] - - response = DecryptBallotSharesResponse( - shares=[write_json_object(share) for share in shares] - ) - - return response diff --git a/app/api/v1/guardian/guardian.py b/app/api/v1/guardian/guardian.py deleted file mode 100644 index 8498980..0000000 --- a/app/api/v1/guardian/guardian.py +++ /dev/null @@ -1,292 +0,0 @@ -import traceback -import sys -from typing import Dict, List -from fastapi import APIRouter, Body, status, HTTPException, Request - -from electionguard.auxiliary import AuxiliaryKeyPair -from electionguard.election_polynomial import ElectionPolynomial -from electionguard.group import hex_to_q_unchecked -from electionguard.key_ceremony import ( - PublicKeySet, - ElectionPartialKeyBackup, - ElectionPartialKeyChallenge, - generate_election_key_pair, - generate_rsa_auxiliary_key_pair, - generate_election_partial_key_backup, - generate_election_partial_key_challenge, - verify_election_partial_key_backup, - verify_election_partial_key_challenge, -) -from electionguard.rsa import rsa_decrypt, rsa_encrypt -from electionguard.serializable import read_json_object, write_json_object -from electionguard.type import GUARDIAN_ID - -from app.api.v1.models.base import BaseQueryRequest -from app.api.v1.models.guardian import ApiGuardianQueryResponse - -from ....core.client import get_client_id -from ....core.guardian import get_guardian, update_guardian -from ....core.repository import get_repository, DataCollection -from ..models import ( - BaseResponse, - BackupChallengeRequest, - BackupChallengeResponse, - BackupVerificationRequest, - BackupVerificationResponse, - ChallengeVerificationRequest, - Guardian, - CreateGuardianRequest, - GuardianPublicKeysResponse, - GuardianBackupResponse, - GuardianBackupRequest, - to_sdk_guardian, -) -from ..tags import GUARDIAN - -router = APIRouter() - -identity = lambda message, key: message - - -@router.get("", response_model=Guardian, tags=[GUARDIAN]) -def fetch_guardian(request: Request, guardian_id: str) -> Guardian: - """ - Fetch a guardian. The response includes the private key information of the guardian. - """ - return get_guardian(guardian_id, request.app.state.settings) - - -@router.get("/public-keys", response_model=GuardianPublicKeysResponse, tags=[GUARDIAN]) -def fetch_public_keys(request: Request, guardian_id: str) -> GuardianPublicKeysResponse: - """ - Fetch the public key information for a guardian. - """ - guardian = get_guardian(guardian_id, request.app.state.settings) - sdk_guardian = to_sdk_guardian(guardian) - - return GuardianPublicKeysResponse( - public_keys=write_json_object(sdk_guardian.share_public_keys()), - ) - - -@router.post("", response_model=GuardianPublicKeysResponse, tags=[GUARDIAN]) -def create_guardian( - request: Request, - data: CreateGuardianRequest = Body(...), -) -> GuardianPublicKeysResponse: - """ - Create a guardian for the election process with the associated keys. - """ - election_keys = generate_election_key_pair( - data.guardian_id, - data.sequence_order, - data.quorum, - hex_to_q_unchecked(data.nonce) if data.nonce is not None else None, - ) - if data.auxiliary_key_pair is None: - auxiliary_keys = generate_rsa_auxiliary_key_pair( - data.guardian_id, data.sequence_order - ) - else: - auxiliary_keys = read_json_object(data.auxiliary_key_pair, AuxiliaryKeyPair) - if not election_keys: - raise HTTPException( - status_code=500, - detail="Election keys failed to be generated", - ) - if not auxiliary_keys: - raise HTTPException( - status_code=500, detail="Auxiliary keys failed to be generated" - ) - guardian = Guardian( - guardian_id=data.guardian_id, - name=data.name, - sequence_order=data.sequence_order, - number_of_guardians=data.number_of_guardians, - quorum=data.quorum, - election_keys=write_json_object(election_keys), - auxiliary_keys=write_json_object(auxiliary_keys), - ) - sdk_guardian = to_sdk_guardian(guardian) - - try: - with get_repository( - get_client_id(), DataCollection.GUARDIAN, request.app.state.settings - ) as repository: - query_result = repository.get({"guardian_id": data.guardian_id}) - if not query_result: - repository.set(guardian.dict()) - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Already exists {data.guardian_id}", - ) - except HTTPException: - raise - except Exception as error: - traceback.print_exc() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Submit guardian failed", - ) from error - - return GuardianPublicKeysResponse( - public_keys=write_json_object(sdk_guardian.share_public_keys()), - ) - - -@router.post("/backup", response_model=GuardianBackupResponse, tags=[GUARDIAN]) -def create_guardian_backup( - request: Request, data: GuardianBackupRequest -) -> GuardianBackupResponse: - """ - Generate election partial key backups by using the public keys included in the request. - """ - guardian = get_guardian(data.guardian_id, request.app.state.settings) - polynomial = read_json_object( - guardian.election_keys["polynomial"], ElectionPolynomial - ) - - encrypt = identity if data.override_rsa else rsa_encrypt - backups: Dict[GUARDIAN_ID, ElectionPartialKeyBackup] = {} - cohort_public_keys: Dict[GUARDIAN_ID, PublicKeySet] = {} - for key_set in data.public_keys: - cohort_key_set = read_json_object(key_set, PublicKeySet) - cohort_owner_id = cohort_key_set.election.owner_id - - backup = generate_election_partial_key_backup( - guardian.guardian_id, - polynomial, - cohort_key_set.auxiliary, - encrypt, - ) - if not backup: - raise HTTPException( - status_code=500, - detail=f"Backup failed to be generated for {cohort_owner_id}", - ) - backups[cohort_owner_id] = backup - cohort_public_keys[cohort_owner_id] = cohort_key_set - - guardian.backups = { - owner_id: write_json_object(backup) for (owner_id, backup) in backups.items() - } - guardian.cohort_public_keys = { - owner_id: write_json_object(key_set) - for (owner_id, key_set) in cohort_public_keys.items() - } - update_guardian(guardian.guardian_id, guardian, request.app.state.settings) - - return GuardianBackupResponse( - guardian_id=data.guardian_id, - backups=[write_json_object(backup) for (id, backup) in backups.items()], - ) - - -@router.post("/backup/verify", response_model=BaseResponse, tags=[GUARDIAN]) -def verify_backup(request: Request, data: BackupVerificationRequest) -> BaseResponse: - """Receive and verify election partial key backup value is in polynomial.""" - guardian = get_guardian(data.guardian_id, request.app.state.settings) - auxiliary_keys = read_json_object(guardian.auxiliary_keys, AuxiliaryKeyPair) - backup = read_json_object(data.backup, ElectionPartialKeyBackup) - cohort_keys = read_json_object( - guardian.cohort_public_keys[backup.owner_id], PublicKeySet - ) - decrypt = identity if data.override_rsa else rsa_decrypt - verification = verify_election_partial_key_backup( - data.guardian_id, - backup, - cohort_keys.election, - auxiliary_keys, - decrypt, - ) - if not verification: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Backup verification process failed", - ) - - guardian.cohort_backups[backup.owner_id] = write_json_object(backup) - guardian.cohort_verifications[backup.owner_id] = write_json_object(verification) - update_guardian(guardian.guardian_id, guardian, request.app.state.settings) - - return BaseResponse() - - -@router.post("/challenge", response_model=BaseResponse, tags=[GUARDIAN]) -def create_backup_challenge( - request: Request, data: BackupChallengeRequest -) -> BaseResponse: - """Publish election backup challenge of election partial key verification.""" - guardian = get_guardian(data.guardian_id, request.app.state.settings) - polynomial = read_json_object( - guardian.election_keys["polynomial"], ElectionPolynomial - ) - backup = read_json_object(data.backup, ElectionPartialKeyBackup) - - challenge = generate_election_partial_key_challenge( - backup, - polynomial, - ) - if not challenge: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Backup challenge generation failed", - ) - - guardian.cohort_challenges[backup.owner_id] = write_json_object(challenge) - update_guardian(guardian.guardian_id, guardian, request.app.state.settings) - return BackupChallengeResponse(challenge=write_json_object(challenge)) - - -@router.post( - "/challenge/verify", response_model=BackupVerificationResponse, tags=[GUARDIAN] -) -def verify_challenge( - request: ChallengeVerificationRequest, -) -> BackupVerificationResponse: - """Verify challenge of previous verification of election partial key.""" - verification = verify_election_partial_key_challenge( - request.verifier_id, - read_json_object(request.challenge, ElectionPartialKeyChallenge), - ) - if not verification: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Challenge verification process failed", - ) - return BackupVerificationResponse(verification=write_json_object(verification)) - - -@router.post("/find", response_model=ApiGuardianQueryResponse, tags=[GUARDIAN]) -def find_guardians( - request: Request, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> ApiGuardianQueryResponse: - """ - Find Guardians. - - Search the repository for guardians that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - try: - print("start") - filter = write_json_object(data.filter) if data.filter else {} - with get_repository( - get_client_id(), DataCollection.GUARDIAN, request.app.state.settings - ) as repository: - cursor = repository.find(filter, skip, limit) - guardians: List[Guardian] = [] - print("before append") - for item in cursor: - guardians.append(item) - print("before return") - return ApiGuardianQueryResponse(guardians=guardians) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find guardians failed", - ) from error diff --git a/app/api/v1/guardian/routes.py b/app/api/v1/guardian/routes.py deleted file mode 100644 index 2800c77..0000000 --- a/app/api/v1/guardian/routes.py +++ /dev/null @@ -1,10 +0,0 @@ -from fastapi import APIRouter -from . import ballot -from . import guardian -from . import tally_decrypt - -router = APIRouter() - -router.include_router(guardian.router, prefix="/guardian") -router.include_router(ballot.router, prefix="/ballot") -router.include_router(tally_decrypt.router, prefix="/tally") diff --git a/app/api/v1/guardian/tally_decrypt.py b/app/api/v1/guardian/tally_decrypt.py deleted file mode 100644 index 108a676..0000000 --- a/app/api/v1/guardian/tally_decrypt.py +++ /dev/null @@ -1,99 +0,0 @@ -# pylint: disable=unused-argument -from datetime import datetime -from typing import Dict -from electionguard.scheduler import Scheduler -from electionguard.manifest import ElectionType, Manifest, InternalManifest -from electionguard.tally import CiphertextTally, CiphertextTallyContest -from electionguard.serializable import read_json_object, write_json_object -from electionguard.type import CONTEST_ID -from fastapi import APIRouter, Body, Depends, HTTPException, Request, status - -from app.core.scheduler import get_scheduler -from app.core.guardian import get_guardian -from app.core.tally_decrypt import get_decryption_share, set_decryption_share -from ..models import ( - to_sdk_guardian, - DecryptTallyShareRequest, - CiphertextTallyDecryptionShare, - DecryptionShareResponse, -) -from ..tags import TALLY_DECRYPT - -router = APIRouter() - - -@router.get( - "/decrypt-share", response_model=DecryptionShareResponse, tags=[TALLY_DECRYPT] -) -def fetch_decrypt_share( - request: Request, - election_id: str, - tally_name: str, -) -> DecryptionShareResponse: - """ - Fetch A decryption share for a given tally - """ - share = get_decryption_share(election_id, tally_name, request.app.state.settings) - return DecryptionShareResponse(shares=[share]) - - -@router.post( - "/decrypt-share", response_model=DecryptionShareResponse, tags=[TALLY_DECRYPT] -) -def decrypt_share( - request: Request, - data: DecryptTallyShareRequest = Body(...), - scheduler: Scheduler = Depends(get_scheduler), -) -> DecryptionShareResponse: - """ - Decrypt a single guardian's share of a tally - """ - guardian = get_guardian(data.guardian_id, request.app.state.settings) - context = data.context.to_sdk_format() - - # TODO: HACK: Remove The Empty Manifest - # Note: The CiphertextTally requires an internal manifest passed into its constructor - # but it is not actually used when executing `compute_decryption_share` so we create a fake. - # see: https://github.com/microsoft/electionguard-python/issues/391 - internal_manifest = InternalManifest( - Manifest( - "", - "", - ElectionType.other, - datetime.now(), - datetime.now(), - [], - [], - [], - [], - [], - ) - ) - tally = CiphertextTally(data.encrypted_tally.tally_name, internal_manifest, context) - contests: Dict[CONTEST_ID, CiphertextTallyContest] = { - contest_id: read_json_object(contest, CiphertextTallyContest) - for contest_id, contest in data.encrypted_tally.tally["contests"].items() - } - tally.contests = contests - - # TODO: modify compute_tally_share to include an optional scheduler param - sdk_guardian = to_sdk_guardian(guardian) - sdk_tally_share = sdk_guardian.compute_tally_share(tally, context) - - if not sdk_tally_share: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Could not compute tally share", - ) - - share = CiphertextTallyDecryptionShare( - election_id=data.encrypted_tally.election_id, - tally_name=data.encrypted_tally.tally_name, - guardian_id=guardian.guardian_id, - tally_share=write_json_object(sdk_tally_share), - # TODO: include spoiled ballots - ) - - set_decryption_share(share, request.app.state.settings) - - return DecryptionShareResponse(shares=[share]) diff --git a/app/api/v1/mediator/__init__.py b/app/api/v1/mediator/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1/mediator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1/mediator/ballot.py b/app/api/v1/mediator/ballot.py deleted file mode 100644 index fd58f46..0000000 --- a/app/api/v1/mediator/ballot.py +++ /dev/null @@ -1,345 +0,0 @@ -from typing import List, Optional, Tuple, cast -from logging import getLogger -import sys - -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.ballot import ( - SubmittedBallot, - CiphertextBallot, - from_ciphertext_ballot, -) -from electionguard.ballot_box import BallotBoxState -from electionguard.ballot_validator import ballot_is_valid_for_election -from electionguard.election import CiphertextElectionContext -from electionguard.manifest import InternalManifest, Manifest -from electionguard.serializable import write_json_object -from app.api.v1.auth.auth import ScopedTo - -from app.api.v1.models.ballot import BallotInventory, SubmitBallotsRequestDto -from app.api.v1.models.user import UserScope - -from ....core.ballot import ( - filter_ballots, - get_ballot, - set_ballots, - get_ballot_inventory, - upsert_ballot_inventory, -) -from ....core.election import get_election -from ....core.repository import get_repository, DataCollection -from ....core.settings import Settings -from ....core.queue import get_message_queue, IMessageQueue -from ..models import ( - BaseResponse, - BaseQueryRequest, - BaseBallotRequest, - BallotInventoryResponse, - BallotQueryResponse, - CastBallotsRequest, - SpoilBallotsRequest, - SubmitBallotsRequest, - ValidateBallotRequest, -) -from ..tags import BALLOTS - -logger = getLogger(__name__) -router = APIRouter() - - -@router.get("", response_model=BallotQueryResponse, tags=[BALLOTS]) -def fetch_ballot( - request: Request, election_id: str, ballot_id: str -) -> BallotQueryResponse: - """ - Fetch A Ballot for a specific election. - """ - ballot = get_ballot(election_id, ballot_id, request.app.state.settings) - return BallotQueryResponse( - election_id=election_id, - ballots=[ballot.to_json_object()], - ) - - -@router.get("/inventory", response_model=BallotInventoryResponse, tags=[BALLOTS]) -def fetch_ballot_inventory( - request: Request, election_id: str -) -> BallotInventoryResponse: - """ - Fetch the Ballot Inventory for a specific election. - """ - inventory = get_ballot_inventory(election_id, request.app.state.settings) - - return BallotInventoryResponse( - inventory=inventory, - ) - - -@router.post("/find", response_model=BallotQueryResponse, tags=[BALLOTS]) -def find_ballots( - request: Request, - election_id: str, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> BallotQueryResponse: - """ - Find Ballots. - - Search the repository for ballots that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - filter = write_json_object(data.filter) if data.filter else {} - ballots = filter_ballots( - election_id, filter, skip, limit, request.app.state.settings - ) - return BallotQueryResponse( - election_id=election_id, ballots=[ballot.to_json_object() for ballot in ballots] - ) - - -@router.post( - "/cast", - response_model=BaseResponse, - tags=[BALLOTS], - dependencies=[ScopedTo([UserScope.admin])], - status_code=status.HTTP_202_ACCEPTED, -) -def cast_ballots( - request: Request, - election_id: Optional[str] = None, - data: CastBallotsRequest = Body(...), -) -> BaseResponse: - """ - Cast ballot - """ - logger.info(f"casting ballot for election {election_id}") - manifest, context, election_id = _get_election_parameters(election_id, data) - ballots = [ - from_ciphertext_ballot( - CiphertextBallot.from_json_object(ballot), BallotBoxState.CAST - ) - for ballot in data.ballots - ] - - for ballot in ballots: - validation_request = ValidateBallotRequest( - ballot=ballot.to_json_object(), manifest=manifest, context=context - ) - _validate_ballot(validation_request) - - logger.info(f"all {len(ballots)} ballots validated successfully") - return _submit_ballots(election_id, ballots, request.app.state.settings) - - -@router.post( - "/spoil", - response_model=BaseResponse, - tags=[BALLOTS], - dependencies=[ScopedTo([UserScope.admin])], - status_code=status.HTTP_202_ACCEPTED, -) -def spoil_ballots( - request: Request, - election_id: Optional[str] = None, - data: SpoilBallotsRequest = Body(...), -) -> BaseResponse: - """ - Spoil ballot - """ - manifest, context, election_id = _get_election_parameters(election_id, data) - ballots = [ - from_ciphertext_ballot( - CiphertextBallot.from_json_object(ballot), BallotBoxState.SPOILED - ) - for ballot in data.ballots - ] - - for ballot in ballots: - validation_request = ValidateBallotRequest( - ballot=ballot.to_json_object(), manifest=manifest, context=context - ) - _validate_ballot(validation_request) - - return _submit_ballots(election_id, ballots, request.app.state.settings) - - -@router.put( - "/submit", - response_model=BaseResponse, - tags=[BALLOTS], - dependencies=[ScopedTo([UserScope.admin])], - status_code=status.HTTP_202_ACCEPTED, -) -def submit_ballots( - request: Request, - election_id: str, - data: SubmitBallotsRequestDto = Body(...), -) -> BaseResponse: - """ - Submit ballots for an election. - - This method expects an `election_id` is provided either in the query string or the request body. - If both are provied, the query string will override. - """ - - logger.info(f"Submitting ballots for {election_id}") - - settings = request.app.state.settings - election_sdk = get_election(election_id, settings) - manifest_sdk = election_sdk.manifest - context_dto = election_sdk.context - context_sdk = context_dto.to_sdk_format() - - res: str = "" - logger.info(f"Converting {len(data.ballots)} ballots to sdk format") - ballots_sdk = list(map(lambda b: b.to_sdk_format(), data.ballots)) - for ballot in ballots_sdk: - if ballot.state == BallotBoxState.UNKNOWN: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Submitted ballot {ballot.object_id} must have a cast or spoil state", - ) - ballot_json = ballot.to_json_object() - validation_request = ValidateBallotRequest( - ballot=ballot_json, - manifest=manifest_sdk, - context=context_sdk, - ) - logger.info("about to validate ballots") - _validate_ballot(validation_request) - logger.info("validated ballots successfully") - res += str(ballot.state) - - return _submit_ballots(election_id, ballots_sdk, request.app.state.settings) - - -@router.post("/validate", response_model=BaseResponse, tags=[BALLOTS]) -def validate_ballot( - request: ValidateBallotRequest = Body(...), -) -> BaseResponse: - """ - Validate a ballot for the given election data - """ - _validate_ballot(request) - return BaseResponse(message="Ballot is valid for election") - - -def _get_election_parameters( - election_id: Optional[str], - request_data: BaseBallotRequest, - settings: Settings = Settings(), -) -> Tuple[Manifest, CiphertextElectionContext, str]: - """Get the election parameters either from the data cache or from the request body.""" - - # Check an election is assigned - if not election_id: - election_id = request_data.election_id - - if not election_id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="specify election_id in the query parameter or request body.", - ) - - election = get_election(election_id, settings) - - if request_data.manifest: - manifest = request_data.manifest - else: - manifest = Manifest.from_json_object(election.manifest) - - if request_data.context: - context = cast(CiphertextElectionContext, request_data.context) - else: - context = election.context.to_sdk_format() - - return manifest, context, election_id - - -def _submit_ballots( - election_id: str, ballots: List[SubmittedBallot], settings: Settings = Settings() -) -> BaseResponse: - logger.info("submitting ballots") - set_response = set_ballots(election_id, ballots, settings) - if set_response.is_success(): - logger.info(f"successfully set ballots: {str(set_response)}") - inventory = get_ballot_inventory(election_id, settings) - if inventory is None: - inventory = BallotInventory( - election_id=election_id, - cast_ballot_count=0, - spoiled_ballot_count=0, - cast_ballots=[], - spoiled_ballots=[], - ) - for ballot in ballots: - if ballot.state == BallotBoxState.CAST: - inventory.cast_ballot_count += 1 - inventory.cast_ballots[ballot.code.to_hex()] = ballot.object_id - elif ballot.state == BallotBoxState.SPOILED: - inventory.spoiled_ballot_count += 1 - inventory.spoiled_ballots[ballot.code.to_hex()] = ballot.object_id - upsert_ballot_inventory(election_id, inventory, settings) - - return set_response - - -def _validate_ballot(request: ValidateBallotRequest) -> None: - ballot = CiphertextBallot.from_json_object(request.ballot) - manifest = Manifest.from_json_object(request.manifest) - internal_manifest = InternalManifest(manifest) - context = CiphertextElectionContext.from_json_object(request.context) - - if not ballot_is_valid_for_election(ballot, internal_manifest, context): - raise HTTPException( - status_code=status.HTTP_406_NOT_ACCEPTABLE, - detail=f"ballot {ballot.object_id} is not valid.", - ) - - -@router.put("/test/submit_queue", tags=[BALLOTS], status_code=status.HTTP_202_ACCEPTED) -def test_submit_ballot( - request: SubmitBallotsRequest = Body(...), -) -> BaseResponse: - """ - Submit a single ballot using a queue. For testing purposes only. - """ - # TODO: complete the implementation - - if not request.election_id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Must submit an election id.", - ) - - try: - ballots = [ - SubmittedBallot.from_json_object(ballot) for ballot in request.ballots - ] - with get_message_queue("submitted-ballots", "submitted-ballots") as queue: - for ballot in ballots: - queue.publish(ballot.to_json()) - _process_ballots(queue, request.election_id) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=500, - detail="Ballot failed to be submitted.", - ) from error - return BaseResponse(message="Ballot Successfully Submitted.") - - -def _process_ballots(queue: IMessageQueue, election_id: str) -> None: - try: - with get_repository(election_id, DataCollection.SUBMITTED_BALLOT) as repository: - for message in queue.subscribe(): - ballot = SubmittedBallot.from_json(message) - key = repository.set(ballot.to_json_object()) - print(f"process_ballots: key {key}") - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=500, - detail="Ballot failed to be processed.", - ) from error diff --git a/app/api/v1/mediator/decrypt.py b/app/api/v1/mediator/decrypt.py deleted file mode 100644 index b5db9ac..0000000 --- a/app/api/v1/mediator/decrypt.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Any, Dict, List - -from electionguard.ballot import SubmittedBallot -from electionguard.ballot_box import BallotBoxState -from electionguard.decrypt_with_shares import decrypt_ballot -from electionguard.decryption_share import DecryptionShare -from electionguard.election import CiphertextElectionContext -from electionguard.serializable import read_json_object, write_json_object -from electionguard.type import BALLOT_ID, GUARDIAN_ID -from fastapi import APIRouter, Body, HTTPException - -from ..models import DecryptBallotsWithSharesRequest -from ..tags import ENCRYPT - -router = APIRouter() - - -@router.post("/decrypt", tags=[ENCRYPT]) -def decrypt_ballots(request: DecryptBallotsWithSharesRequest = Body(...)) -> Any: - """Decrypt ballots with the provided shares""" - ballots = [ - SubmittedBallot.from_json_object(ballot) for ballot in request.encrypted_ballots - ] - context: CiphertextElectionContext = CiphertextElectionContext.from_json_object( - request.context - ) - - # only decrypt spoiled ballots using this API - for ballot in ballots: - if ballot.state != BallotBoxState.SPOILED: - raise HTTPException( - status_code=400, - detail=f"Ballot {ballot.object_id} must be spoiled", - ) - - all_shares: List[DecryptionShare] = [ - read_json_object(share, DecryptionShare) - for shares in request.shares.values() - for share in shares - ] - shares_by_ballot = index_shares_by_ballot(all_shares) - - extended_base_hash = context.crypto_extended_base_hash - decrypted_ballots = { - ballot.object_id: decrypt_ballot( - ballot, shares_by_ballot[ballot.object_id], extended_base_hash - ) - for ballot in ballots - } - - return write_json_object(decrypted_ballots) - - -def index_shares_by_ballot( - shares: List[DecryptionShare], -) -> Dict[BALLOT_ID, Dict[GUARDIAN_ID, DecryptionShare]]: - """ - Construct a lookup by ballot ID containing the dictionary of shares needed - to decrypt that ballot. - """ - shares_by_ballot: Dict[str, Dict[str, DecryptionShare]] = {} - for share in shares: - ballot_shares = shares_by_ballot.setdefault(share.object_id, {}) - ballot_shares[share.guardian_id] = share - - return shares_by_ballot diff --git a/app/api/v1/mediator/election.py b/app/api/v1/mediator/election.py deleted file mode 100644 index d1b9d01..0000000 --- a/app/api/v1/mediator/election.py +++ /dev/null @@ -1,289 +0,0 @@ -from typing import Any -from uuid import uuid4 - - -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.election import ( - ElectionConstants, - make_ciphertext_election_context, -) -from electionguard.group import ElementModP, ElementModQ -from electionguard.manifest import Manifest -from electionguard.serializable import read_json_object, write_json_object -from electionguard.utils import get_optional -from app.api.v1.auth.auth import ScopedTo - -from app.api.v1.models.election import ElectionListResponseDto, ElectionSummaryDto -from app.api.v1.models.user import UserScope - -from .manifest import get_manifest -from ....core.ballot import get_ballot_inventory, upsert_ballot_inventory -from ....core.key_ceremony import get_key_ceremony -from ....core.election import ( - get_election, - set_election, - update_election_state, - filter_elections, -) -from ..models import ( - BaseResponse, - BallotInventory, - Election, - ElectionState, - ElectionQueryRequest, - ElectionQueryResponse, - MakeElectionContextRequest, - MakeElectionContextResponse, - SubmitElectionRequest, -) -from ..tags import ELECTION - -router = APIRouter() - - -@router.get("/constants", dependencies=[ScopedTo([UserScope.admin])], tags=[ELECTION]) -def get_election_constants() -> Any: - """ - Get the constants defined for an election. - """ - constants = ElectionConstants() - return constants.to_json_object() - - -@router.get( - "", - response_model=ElectionQueryResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def fetch_election(request: Request, election_id: str) -> ElectionQueryResponse: - """Get an election by election id.""" - election = get_election(election_id, request.app.state.settings) - return ElectionQueryResponse( - elections=[election], - ) - - -@router.put( - "", - response_model=BaseResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def create_election( - request: Request, - data: SubmitElectionRequest = Body(...), -) -> BaseResponse: - """ - Submit an election. - - Method expects a manifest to already be submitted or to optionally be provided - as part of the request body. If a manifest is provided as part of the body - then it will override any cached value, however the hash must match the hash - contained in the CiphertextelectionContext. - """ - if data.election_id: - election_id = data.election_id - else: - election_id = str(uuid4()) - - key_ceremony = get_key_ceremony(data.key_name, request.app.state.settings) - try: - context = data.context.to_sdk_format() - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=str(e) - ) from e - - # if a manifest is provided use it, but don't cache it - if data.manifest: - sdk_manifest = Manifest.from_json_object(data.manifest) - else: - api_manifest = get_manifest(context.manifest_hash, request.app.state.settings) - sdk_manifest = Manifest.from_json_object(api_manifest.manifest) - - # validate that the context was built against the correct manifest - if context.manifest_hash != sdk_manifest.crypto_hash().to_hex(): - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail="manifest hash does not match provided context hash", - ) - - # validate that the context provided matches a known key ceremony - if context.elgamal_public_key != key_ceremony.elgamal_public_key: - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail="key ceremony public key does not match provided context public key", - ) - - election = Election( - election_id=election_id, - key_name=data.key_name, - state=ElectionState.CREATED, - context=context.to_json_object(), - manifest=sdk_manifest.to_json_object(), - ) - - return set_election(election, request.app.state.settings) - - -def to_election_summary(election: Election) -> ElectionSummaryDto: - return ElectionSummaryDto( - election_id=election.election_id, - name=election.get_name(), - state=election.state, - number_of_guardians=election.context.number_of_guardians, - quorum=election.context.quorum, - cast_ballot_count=0, - spoiled_ballot_count=0, - index=0, - ) - - -@router.post( - "/list", - response_model=ElectionListResponseDto, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def list_elections( - request: Request, -) -> ElectionListResponseDto: - """ - List all elections including state and number of ballots cast or submitted if any. - """ - - result = ElectionListResponseDto() - elections = filter_elections(filter={}, settings=request.app.state.settings) - result.elections = [to_election_summary(e) for e in elections] - index = 0 - for election in result.elections: - election.index = index - inventory = get_ballot_inventory( - election.election_id, request.app.state.settings - ) - if inventory is not None: - election.cast_ballot_count = inventory.cast_ballot_count - election.spoiled_ballot_count = inventory.spoiled_ballot_count - index = index + 1 - result.elections.sort(key=lambda e: e.index, reverse=True) - - return result - - -@router.post( - "/find", - response_model=ElectionQueryResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def find_elections( - request: Request, - skip: int = 0, - limit: int = 100, - data: ElectionQueryRequest = Body(...), -) -> ElectionQueryResponse: - """ - Find elections. - - Search the repository for elections that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - filter = write_json_object(data.filter) if data.filter else {} - elections = filter_elections(filter, skip, limit, request.app.state.settings) - return ElectionQueryResponse(elections=elections) - - -@router.post( - "/open", - response_model=BaseResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def open_election(request: Request, election_id: str) -> BaseResponse: - """ - Open an election. - """ - # create the ballot inventory on election open - ballot_inventory = BallotInventory(election_id=election_id) - upsert_ballot_inventory(election_id, ballot_inventory, request.app.state.settings) - - return update_election_state( - election_id, ElectionState.OPEN, request.app.state.settings - ) - - -@router.post( - "/close", - response_model=BaseResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def close_election(request: Request, election_id: str) -> BaseResponse: - """ - Close an election. - """ - return update_election_state( - election_id, ElectionState.CLOSED, request.app.state.settings - ) - - -@router.post( - "/publish", - response_model=BaseResponse, - dependencies=[ScopedTo([UserScope.admin])], - tags=[ELECTION], -) -def publish_election(request: Request, election_id: str) -> BaseResponse: - """ - Publish an election. - """ - return update_election_state( - election_id, ElectionState.PUBLISHED, request.app.state.settings - ) - - -@router.post( - "/context", - response_model=MakeElectionContextResponse, - tags=[ELECTION], -) -def build_election_context( - request: Request, - data: MakeElectionContextRequest = Body(...), -) -> MakeElectionContextResponse: - """ - Build a CiphertextElectionContext for a given election and returns it. - - Caller must specify the manifest to build against - by either providing the manifest hash in the request body; - or by providing the manifest directly in the request body. - """ - - if data.manifest: - sdk_manifest = Manifest.from_json_object(data.manifest) - else: - manifest_hash = read_json_object(get_optional(data.manifest_hash), ElementModQ) - api_manifest = get_manifest( - manifest_hash, - request.app.state.settings, - ) - sdk_manifest = Manifest.from_json_object(api_manifest.manifest) - - elgamal_public_key: ElementModP = read_json_object( - data.elgamal_public_key, ElementModP - ) - commitment_hash = read_json_object(data.commitment_hash, ElementModQ) - number_of_guardians = data.number_of_guardians - quorum = data.quorum - - context = make_ciphertext_election_context( - number_of_guardians, - quorum, - elgamal_public_key, - commitment_hash, - sdk_manifest.crypto_hash(), - ) - - return MakeElectionContextResponse(context=context.to_json_object()) diff --git a/app/api/v1/mediator/encrypt.py b/app/api/v1/mediator/encrypt.py deleted file mode 100644 index e5bde4b..0000000 --- a/app/api/v1/mediator/encrypt.py +++ /dev/null @@ -1,55 +0,0 @@ -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.ballot import PlaintextBallot -from electionguard.manifest import InternalManifest, Manifest -from electionguard.encrypt import encrypt_ballot -from electionguard.group import ElementModQ -from electionguard.serializable import read_json_object, write_json_object -from electionguard.utils import get_optional - -from app.core.election import get_election -from ..models import ( - EncryptBallotsRequest, - EncryptBallotsResponse, -) -from ..tags import ENCRYPT - -router = APIRouter() - - -@router.post("/encrypt", tags=[ENCRYPT]) -def encrypt_ballots( - request: Request, - data: EncryptBallotsRequest = Body(...), -) -> EncryptBallotsResponse: - """ - Encrypt one or more ballots. - - This function is primarily used for testing and does not modify internal state. - """ - election = get_election(data.election_id, request.app.state.settings) - manifest = InternalManifest(Manifest.from_json_object(election.manifest)) - context = election.context.to_sdk_format() - seed_hash = read_json_object(data.seed_hash, ElementModQ) - - ballots = [PlaintextBallot.from_json_object(ballot) for ballot in data.ballots] - - encrypted_ballots = [] - current_hash = seed_hash - - for ballot in ballots: - encrypted_ballot = encrypt_ballot(ballot, manifest, context, current_hash) - if not encrypted_ballot: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Ballot failed to encrypt", - ) - encrypted_ballots.append(encrypted_ballot) - current_hash = get_optional(encrypted_ballot.crypto_hash) - - response = EncryptBallotsResponse( - message="Successfully encrypted ballots", - encrypted_ballots=[ballot.to_json_object() for ballot in encrypted_ballots], - next_seed_hash=write_json_object(current_hash), - ) - return response diff --git a/app/api/v1/mediator/key_admin.py b/app/api/v1/mediator/key_admin.py deleted file mode 100644 index e606e2c..0000000 --- a/app/api/v1/mediator/key_admin.py +++ /dev/null @@ -1,328 +0,0 @@ -from typing import List -import sys -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.hash import hash_elems -from electionguard.key_ceremony import ( - PublicKeySet, - ElectionPublicKey, - ElectionPartialKeyVerification, - ElectionPartialKeyChallenge, - verify_election_partial_key_challenge, -) -from electionguard.elgamal import elgamal_combine_public_keys -from electionguard.serializable import write_json_object, read_json_object -from electionguard.group import ElementModP - -from ....core.client import get_client_id -from ....core.key_guardian import get_key_guardian -from ....core.key_ceremony import ( - key_ceremony_from_query, - get_key_ceremony, - update_key_ceremony, - update_key_ceremony_state, - validate_can_publish, -) -from ....core.repository import get_repository, DataCollection -from ..models import ( - BaseQueryRequest, - BaseResponse, - KeyCeremony, - KeyCeremonyState, - KeyCeremonyGuardian, - KeyCeremonyGuardianStatus, - KeyCeremonyGuardianState, - KeyCeremonyCreateRequest, - KeyCeremonyStateResponse, - KeyCeremonyQueryResponse, - KeyCeremonyVerifyChallengesResponse, - PublishElectionJointKeyRequest, - ElectionJointKeyResponse, -) -from ..tags import KEY_CEREMONY_ADMIN - -router = APIRouter() - - -@router.get( - "/ceremony", response_model=KeyCeremonyQueryResponse, tags=[KEY_CEREMONY_ADMIN] -) -def fetch_ceremony( - request: Request, - key_name: str, -) -> KeyCeremonyQueryResponse: - """ - Get a specific key ceremony by key_name. - """ - key_ceremony = get_key_ceremony(key_name, request.app.state.settings) - return KeyCeremonyQueryResponse(key_ceremonies=[key_ceremony]) - - -@router.put("/ceremony", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN]) -def create_ceremony( - request: Request, - data: KeyCeremonyCreateRequest = Body(...), -) -> BaseResponse: - """ - Create a Key Ceremony. - - Calling this method for an existing key_name will overwrite an existing one. - """ - - ceremony = KeyCeremony( - key_name=data.key_name, - state=KeyCeremonyState.CREATED, - number_of_guardians=data.number_of_guardians, - quorum=data.quorum, - guardian_ids=data.guardian_ids, - guardian_status={ - guardian_id: KeyCeremonyGuardianState() for guardian_id in data.guardian_ids - }, - ) - - try: - with get_repository( - get_client_id(), DataCollection.KEY_CEREMONY, request.app.state.settings - ) as repository: - query_result = repository.get({"key_name": data.key_name}) - if not query_result: - repository.set(ceremony.dict()) - return BaseResponse() - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Already exists {data.key_name}", - ) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Create Key Ceremony Failed", - ) from error - - -@router.get( - "/ceremony/state", - response_model=KeyCeremonyStateResponse, - tags=[KEY_CEREMONY_ADMIN], -) -def fetch_ceremony_state( - request: Request, - key_name: str, -) -> KeyCeremonyStateResponse: - """ - Get a specific key ceremony state by key_name. - """ - ceremony = get_key_ceremony(key_name, request.app.state.settings) - - return KeyCeremonyStateResponse( - key_name=key_name, - state=ceremony.state, - guardian_status=ceremony.guardian_status, - ) - - -@router.post( - "/ceremony/find", response_model=KeyCeremonyQueryResponse, tags=[KEY_CEREMONY_ADMIN] -) -def find_ceremonies( - request: Request, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> KeyCeremonyQueryResponse: - """ - Find Key Ceremonies according ot the filter criteria. - """ - try: - filter = write_json_object(data.filter) if data.filter else {} - with get_repository( - get_client_id(), DataCollection.KEY_CEREMONY, request.app.state.settings - ) as repository: - cursor = repository.find(filter, skip, limit) - key_ceremonies: List[KeyCeremony] = [] - for item in cursor: - key_ceremonies.append(key_ceremony_from_query(item)) - return KeyCeremonyQueryResponse(key_ceremonies=key_ceremonies) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find guardians failed", - ) from error - - -@router.post("/ceremony/open", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN]) -def open_ceremony(request: Request, key_name: str) -> BaseResponse: - """ - Open a key ceremony for participation. - """ - return update_key_ceremony_state( - key_name, KeyCeremonyState.OPEN, request.app.state.settings - ) - - -@router.post("/ceremony/close", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN]) -def close_ceremony(request: Request, key_name: str) -> BaseResponse: - """ - Close a key ceremony for participation. - """ - return update_key_ceremony_state( - key_name, KeyCeremonyState.CLOSED, request.app.state.settings - ) - - -@router.post( - "/ceremony/challenge", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN] -) -def challenge_ceremony(request: Request, key_name: str) -> BaseResponse: - """ - Mark the key ceremony challenged. - """ - return update_key_ceremony_state( - key_name, KeyCeremonyState.CHALLENGED, request.app.state.settings - ) - - -@router.get( - "/ceremony/challenge/verify", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN] -) -def verify_ceremony_challenges(request: Request, key_name: str) -> BaseResponse: - """ - Verify a challenged key ceremony. - """ - ceremony = get_key_ceremony(key_name, request.app.state.settings) - challenge_guardians: List[KeyCeremonyGuardian] = [] - for guardian_id, state in ceremony.guardian_status.items(): - if state.backups_verified == KeyCeremonyGuardianStatus.ERROR: - challenge_guardians.append(get_key_guardian(key_name, guardian_id)) - - if not any(challenge_guardians): - return BaseResponse(message="no challenges exist") - - verifications: List[ElectionPartialKeyVerification] = [] - for guardian in challenge_guardians: - if not guardian.challenges: - continue - for challenge in guardian.challenges: - verifications.append( - verify_election_partial_key_challenge( - "API", - read_json_object(challenge, ElectionPartialKeyChallenge), - ) - ) - - return KeyCeremonyVerifyChallengesResponse(verifications=verifications) - - -@router.post("/ceremony/cancel", response_model=BaseResponse, tags=[KEY_CEREMONY_ADMIN]) -def cancel_ceremony(request: Request, key_name: str) -> BaseResponse: - """ - Cancel a Key Ceremony. - """ - return update_key_ceremony_state( - key_name, KeyCeremonyState.CANCELLED, request.app.state.settings - ) - - -@router.get( - "/ceremony/joint_key", - response_model=ElectionJointKeyResponse, - tags=[KEY_CEREMONY_ADMIN], -) -def fetch_joint_key( - request: Request, - key_name: str, -) -> ElectionJointKeyResponse: - """ - Get The Joint Election Key - """ - ceremony = get_key_ceremony(key_name, request.app.state.settings) - if not ceremony.elgamal_public_key: - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail=f"No joint key for {key_name}", - ) - if not ceremony.commitment_hash: - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail=f"No commitment hash for {key_name}", - ) - - return ElectionJointKeyResponse( - elgamal_public_key=write_json_object(ceremony.elgamal_public_key), - commitment_hash=write_json_object(ceremony.commitment_hash), - ) - - -@router.post( - "/ceremony/combine", - response_model=ElectionJointKeyResponse, - tags=[KEY_CEREMONY_ADMIN], -) -def combine_election_keys( - data: PublishElectionJointKeyRequest, -) -> ElectionJointKeyResponse: - """ - Combine public election keys into a final one without mutating the state of the key ceremony. - :return: Combine Election key - """ - election_public_keys: List[ElementModP] = [] - coefficient_commitments: List[ElementModP] = [] - for public_key in data.election_public_keys: - key = read_json_object(public_key, ElectionPublicKey) - election_public_keys.append(key.key) - for commitment in key.coefficient_commitments: - coefficient_commitments.append(commitment) - return _elgamal_combine_keys(election_public_keys, coefficient_commitments) - - -# FINAL: Publish joint public election key -@router.post( - "/ceremony/publish", - response_model=ElectionJointKeyResponse, - tags=[KEY_CEREMONY_ADMIN], -) -def publish_joint_key( - request: Request, - key_name: str, -) -> ElectionJointKeyResponse: - """ - Publish joint election key from the public keys of all guardians. - """ - ceremony = get_key_ceremony(key_name, request.app.state.settings) - - validate_can_publish(ceremony) - - election_public_keys: List[ElementModP] = [] - coefficient_commitments: List[ElementModP] = [] - for guardian_id in ceremony.guardian_ids: - guardian = get_key_guardian(key_name, guardian_id, request.app.state.settings) - if not guardian.public_keys: - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail=f"Could not find guardian public key {guardian_id}", - ) - - public_keys = read_json_object(guardian.public_keys, PublicKeySet) - election_public_keys.append(public_keys.election.key) - for commitment in public_keys.election.coefficient_commitments: - coefficient_commitments.append(commitment) - - response = _elgamal_combine_keys(election_public_keys, coefficient_commitments) - - ceremony.elgamal_public_key = response.elgamal_public_key - ceremony.commitment_hash = response.commitment_hash - update_key_ceremony(key_name, ceremony, request.app.state.settings) - - return response - - -def _elgamal_combine_keys( - election_public_keys: List[ElementModP], coefficient_commitments: List[ElementModP] -) -> ElectionJointKeyResponse: - elgamal_public_key = elgamal_combine_public_keys(election_public_keys) - commitment_hash = hash_elems(coefficient_commitments) - return ElectionJointKeyResponse( - elgamal_public_key=write_json_object(elgamal_public_key), - commitment_hash=write_json_object(commitment_hash), - ) diff --git a/app/api/v1/mediator/key_ceremony.py b/app/api/v1/mediator/key_ceremony.py deleted file mode 100644 index 2d8cd54..0000000 --- a/app/api/v1/mediator/key_ceremony.py +++ /dev/null @@ -1,171 +0,0 @@ -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.key_ceremony import ( - PublicKeySet, - ElectionPartialKeyBackup, - ElectionPartialKeyVerification, - ElectionPartialKeyChallenge, -) -from electionguard.serializable import write_json_object, read_json_object - -from ....core.key_guardian import get_key_guardian, update_key_guardian -from ....core.key_ceremony import get_key_ceremony, update_key_ceremony -from ..models import ( - BaseResponse, - GuardianAnnounceRequest, - GuardianSubmitBackupRequest, - GuardianSubmitVerificationRequest, - GuardianSubmitChallengeRequest, - KeyCeremony, - KeyCeremonyState, - KeyCeremonyGuardian, - KeyCeremonyGuardianStatus, -) -from ..tags import KEY_CEREMONY - -router = APIRouter() - - -# ROUND 1: Announce guardians with public keys -@router.post("/guardian/announce", response_model=BaseResponse, tags=[KEY_CEREMONY]) -def announce_guardian( - request: Request, - data: GuardianAnnounceRequest = Body(...), -) -> BaseResponse: - """ - Announce the guardian as present and participating in the Key Ceremony. - """ - keyset = read_json_object(data.public_keys, PublicKeySet) - guardian_id = keyset.election.owner_id - - ceremony = get_key_ceremony(data.key_name, request.app.state.settings) - guardian = get_key_guardian(data.key_name, guardian_id, request.app.state.settings) - - _validate_can_participate(ceremony, guardian) - - guardian.public_keys = write_json_object(keyset) - ceremony.guardian_status[ - guardian_id - ].public_key_shared = KeyCeremonyGuardianStatus.COMPLETE - - update_key_guardian( - data.key_name, guardian_id, guardian, request.app.state.settings - ) - return update_key_ceremony(data.key_name, ceremony, request.app.state.settings) - - -# ROUND 2: Share Election Partial Key Backups for compensating -@router.post("/guardian/backup", response_model=BaseResponse, tags=[KEY_CEREMONY]) -def share_backups( - request: Request, - data: GuardianSubmitBackupRequest = Body(...), -) -> BaseResponse: - """ - Share Election Partial Key Backups to be distributed to the other guardians. - """ - ceremony = get_key_ceremony(data.key_name, request.app.state.settings) - guardian = get_key_guardian( - data.key_name, data.guardian_id, request.app.state.settings - ) - - _validate_can_participate(ceremony, guardian) - - backups = [ - read_json_object(backup, ElectionPartialKeyBackup) for backup in data.backups - ] - - guardian.backups = [write_json_object(backup) for backup in backups] - ceremony.guardian_status[ - data.guardian_id - ].backups_shared = KeyCeremonyGuardianStatus.COMPLETE - - update_key_guardian( - data.key_name, data.guardian_id, guardian, request.app.state.settings - ) - return update_key_ceremony(data.key_name, ceremony, request.app.state.settings) - - -# ROUND 3: Share verifications of backups -@router.post("/guardian/verify", response_model=BaseResponse, tags=[KEY_CEREMONY]) -def verify_backups( - request: Request, - data: GuardianSubmitVerificationRequest = Body(...), -) -> BaseResponse: - """ - Share the results of verifying the other guardians' backups. - """ - ceremony = get_key_ceremony(data.key_name, request.app.state.settings) - guardian = get_key_guardian( - data.key_name, data.guardian_id, request.app.state.settings - ) - - _validate_can_participate(ceremony, guardian) - - verifications = [ - read_json_object(verification, ElectionPartialKeyVerification) - for verification in data.verifications - ] - - guardian.verifications = [ - write_json_object(verification) for verification in verifications - ] - # pylint: disable=use-a-generator - ceremony.guardian_status[data.guardian_id].backups_verified = ( - KeyCeremonyGuardianStatus.COMPLETE - if all([verification.verified for verification in verifications]) - else KeyCeremonyGuardianStatus.ERROR - ) - - update_key_guardian( - data.key_name, data.guardian_id, guardian, request.app.state.settings - ) - return update_key_ceremony(data.key_name, ceremony, request.app.state.settings) - - -# ROUND 4 (Optional): If a verification fails, guardian must issue challenge -@router.post("/guardian/challenge", response_model=BaseResponse, tags=[KEY_CEREMONY]) -def challenge_backups( - request: Request, - data: GuardianSubmitChallengeRequest = Body(...), -) -> BaseResponse: - """ - Submit challenges to the other guardians' backups. - """ - ceremony = get_key_ceremony(data.key_name, request.app.state.settings) - guardian = get_key_guardian( - data.key_name, data.guardian_id, request.app.state.settings - ) - - _validate_can_participate(ceremony, guardian) - - challenges = [ - read_json_object(challenge, ElectionPartialKeyChallenge) - for challenge in data.challenges - ] - - guardian.challenges = [write_json_object(challenge) for challenge in challenges] - ceremony.guardian_status[ - data.guardian_id - ].backups_verified = KeyCeremonyGuardianStatus.ERROR - - update_key_guardian( - data.key_name, data.guardian_id, guardian, request.app.state.settings - ) - return update_key_ceremony(data.key_name, ceremony, request.app.state.settings) - - -def _validate_can_participate( - ceremony: KeyCeremony, guardian: KeyCeremonyGuardian -) -> None: - # TODO: better validation - if ceremony.state != KeyCeremonyState.OPEN: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"Cannot announce for key ceremony state {ceremony.state}", - ) - - if guardian.guardian_id not in ceremony.guardian_ids: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=f"Guardian {guardian.guardian_id} not in ceremony", - ) diff --git a/app/api/v1/mediator/key_guardian.py b/app/api/v1/mediator/key_guardian.py deleted file mode 100644 index cf33c53..0000000 --- a/app/api/v1/mediator/key_guardian.py +++ /dev/null @@ -1,109 +0,0 @@ -import traceback -from typing import List -import sys -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.serializable import write_json_object, read_json_object - -from ....core.client import get_client_id -from ....core.key_guardian import get_key_guardian, update_key_guardian -from ....core.repository import get_repository, DataCollection -from ..models import ( - BaseQueryRequest, - BaseResponse, - GuardianQueryResponse, - KeyCeremonyGuardian, -) -from ..tags import KEY_GUARDIAN - -router = APIRouter() - - -@router.get("", response_model=GuardianQueryResponse, tags=[KEY_GUARDIAN]) -def fetch_key_ceremony_guardian( - request: Request, key_name: str, guardian_id: str -) -> GuardianQueryResponse: - """ - Get a key ceremony guardian. - """ - guardian = get_key_guardian(key_name, guardian_id, request.app.state.settings) - return GuardianQueryResponse(guardians=[guardian]) - - -@router.put("", response_model=BaseResponse, tags=[KEY_GUARDIAN]) -def create_key_ceremony_guardian( - request: Request, - data: KeyCeremonyGuardian = Body(...), -) -> BaseResponse: - """ - Create a Key Ceremony Guardian. - - In order for a guardian to participate they must be associated with the key ceremony first. - """ - try: - with get_repository( - get_client_id(), DataCollection.KEY_GUARDIAN, request.app.state.settings - ) as repository: - query_result = repository.get( - {"key_name": data.key_name, "guardian_id": data.guardian_id} - ) - if not query_result: - repository.set(data.dict()) - return BaseResponse() - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Already exists {data.guardian_id}", - ) - except Exception as error: - traceback.print_exc() - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Submit ballots failed", - ) from error - - -@router.post("", response_model=BaseResponse, tags=[KEY_GUARDIAN]) -def update_key_ceremony_guardian( - request: Request, - data: KeyCeremonyGuardian = Body(...), -) -> BaseResponse: - """ - Update a Key Ceremony Guardian. - - This API is primarily for administrative purposes. - """ - return update_key_guardian( - data.key_name, data.guardian_id, data, request.app.state.settings - ) - - -@router.post("/find", response_model=GuardianQueryResponse, tags=[KEY_GUARDIAN]) -def find_key_ceremony_guardians( - request: Request, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> GuardianQueryResponse: - """ - Find Guardians. - - Search the repository for guardians that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - try: - filter = write_json_object(data.filter) if data.filter else {} - with get_repository( - get_client_id(), DataCollection.KEY_GUARDIAN, request.app.state.settings - ) as repository: - cursor = repository.find(filter, skip, limit) - guardians: List[KeyCeremonyGuardian] = [] - for item in cursor: - guardians.append(read_json_object(item, KeyCeremonyGuardian)) - return GuardianQueryResponse(guardians=guardians) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find guardians failed", - ) from error diff --git a/app/api/v1/mediator/manifest.py b/app/api/v1/mediator/manifest.py deleted file mode 100644 index 7ce6e0a..0000000 --- a/app/api/v1/mediator/manifest.py +++ /dev/null @@ -1,138 +0,0 @@ -from typing import Any, Optional, Tuple - -from fastapi import APIRouter, Body, Depends, HTTPException, Request, status - -from electionguard.group import hex_to_q -from electionguard.manifest import Manifest as sdk_manifest -from electionguard.schema import validate_json_schema -from electionguard.serializable import write_json_object -from electionguard.utils import get_optional - -from app.core.schema import get_description_schema - -from ....core.manifest import get_manifest, set_manifest, filter_manifests -from ..models import ( - Manifest, - BaseQueryRequest, - ManifestQueryResponse, - ManifestSubmitResponse, - ValidateManifestRequest, - ValidateManifestResponse, - ResponseStatus, -) -from ..tags import MANIFEST - -router = APIRouter() - - -@router.get("", response_model=ManifestQueryResponse, tags=[MANIFEST]) -def fetch_manifest(request: Request, manifest_hash: str) -> ManifestQueryResponse: - """Get an election manifest by hash.""" - crypto_hash = hex_to_q(manifest_hash) - if not crypto_hash: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="manifest hash not valid" - ) - manifest = get_manifest(crypto_hash, request.app.state.settings) - return ManifestQueryResponse( - manifests=[manifest], - ) - - -@router.put( - "", - response_model=ManifestSubmitResponse, - tags=[MANIFEST], - status_code=status.HTTP_202_ACCEPTED, -) -def submit_manifest( - request: Request, - data: ValidateManifestRequest = Body(...), - schema: Any = Depends(get_description_schema), -) -> ManifestSubmitResponse: - """ - Submit a manifest for storage. - """ - manifest, validation = _validate_manifest(data, schema) - if not manifest or validation.status == ResponseStatus.FAIL: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail=validation.details - ) - api_manifest = Manifest( - manifest_hash=write_json_object(manifest.crypto_hash()), - manifest=manifest.to_json_object(), - ) - return set_manifest(api_manifest, request.app.state.settings) - - -@router.post("/find", response_model=ManifestQueryResponse, tags=[MANIFEST]) -def find_manifests( - request: Request, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> ManifestQueryResponse: - """ - Find manifests. - - Search the repository for manifests that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - filter = write_json_object(data.filter) if data.filter else {} - return filter_manifests(filter, skip, limit, request.app.state.settings) - - -@router.post("/validate", response_model=ValidateManifestResponse, tags=[MANIFEST]) -def validate_manifest( - request: ValidateManifestRequest = Body(...), - schema: Any = Depends(get_description_schema), -) -> ValidateManifestResponse: - """ - Validate an Election manifest for a given election. - """ - - _, response = _validate_manifest(request, schema) - return response - - -def _deserialize_manifest(data: object) -> Optional[sdk_manifest]: - try: - return sdk_manifest.from_json_object(data) - except Exception: # pylint: disable=broad-except - # TODO: some sort of information why it failed - return None - - -def _validate_manifest( - request: ValidateManifestRequest, schema: Any -) -> Tuple[Optional[sdk_manifest], ValidateManifestResponse]: - # Check schema - schema = request.schema_override if request.schema_override else schema - (schema_success, schema_details) = validate_json_schema(request.manifest, schema) - - # Check object parse - manifest = _deserialize_manifest(request.manifest) - serialize_success = bool(manifest) - valid_success = bool(serialize_success and get_optional(manifest).is_valid()) - - # build response - success = schema_success and serialize_success and valid_success - - if success: - return manifest, ValidateManifestResponse( - message="Manifest successfully validated", - manifest_hash=get_optional(manifest).crypto_hash().to_hex(), - ) - - return manifest, ValidateManifestResponse( - status=ResponseStatus.FAIL, - message="Manifest failed validation", - details=str( - { - "schema_success": schema_success, - "serialize_success": serialize_success, - "valid_success": valid_success, - "schema_details": schema_details, - } - ), - ) diff --git a/app/api/v1/mediator/routes.py b/app/api/v1/mediator/routes.py deleted file mode 100644 index ee2b755..0000000 --- a/app/api/v1/mediator/routes.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import APIRouter -from . import ballot -from . import decrypt -from . import election -from . import encrypt -from . import key_admin -from . import key_ceremony -from . import key_guardian -from . import manifest -from . import tally -from . import tally_decrypt - -router = APIRouter() - -router.include_router(key_guardian.router, prefix="/guardian") -router.include_router(key_ceremony.router, prefix="/key") -router.include_router(key_admin.router, prefix="/key") -router.include_router(election.router, prefix="/election") -router.include_router(manifest.router, prefix="/manifest") -router.include_router(ballot.router, prefix="/ballot") -router.include_router(decrypt.router, prefix="/ballot") -router.include_router(encrypt.router, prefix="/ballot") -router.include_router(tally.router, prefix="/tally") -router.include_router(tally_decrypt.router, prefix="/tally/decrypt") diff --git a/app/api/v1/mediator/tally.py b/app/api/v1/mediator/tally.py deleted file mode 100644 index 96d5dbe..0000000 --- a/app/api/v1/mediator/tally.py +++ /dev/null @@ -1,304 +0,0 @@ -# pylint: disable=unused-argument -from typing import Dict -from logging import getLogger -from datetime import datetime -import sys - -from fastapi import ( - APIRouter, - BackgroundTasks, - Body, - Depends, - HTTPException, - Request, - Response, - status, -) - -from electionguard.ballot import BallotBoxState -from electionguard.decrypt_with_shares import decrypt_tally as decrypt -from electionguard.decryption_share import DecryptionShare -from electionguard.manifest import ElectionType, InternalManifest, Manifest -from electionguard.scheduler import Scheduler -from electionguard.serializable import read_json_object, write_json_object -from electionguard.type import CONTEST_ID -import electionguard.tally - - -from app.core.scheduler import get_scheduler -from app.core.settings import Settings -from app.core.ballot import get_ballot_inventory, filter_ballots -from app.core.election import get_election -from app.core.tally import ( - get_ciphertext_tally, - set_ciphertext_tally, - filter_ciphertext_tallies, - set_plaintext_tally, - filter_plaintext_tallies, - update_plaintext_tally, -) -from app.core.tally_decrypt import filter_decryption_shares -from ..models import ( - BaseQueryRequest, - CiphertextTallyQueryResponse, - DecryptTallyRequest, - CiphertextTally, - PlaintextTally, - PlaintextTallyState, - PlaintextTallyQueryResponse, -) -from ..tags import TALLY - - -router = APIRouter() -logger = getLogger(__name__) - - -@router.get("", response_model=CiphertextTally, tags=[TALLY]) -def fetch_ciphertext_tally( - request: Request, - election_id: str, - tally_name: str, -) -> CiphertextTally: - """ - Fetch a specific ciphertext tally. - """ - tally = get_ciphertext_tally(election_id, tally_name, request.app.state.settings) - return tally - - -@router.post("", response_model=CiphertextTally, tags=[TALLY]) -def tally_ballots( - request: Request, - election_id: str, - tally_name: str, - scheduler: Scheduler = Depends(get_scheduler), -) -> CiphertextTally: - """ - Start a new ciphertext tally of a collection of ballots. - - An election can have more than one tally. Each tally must have a unique name. - Each tally correlates to a snapshot of all ballots submitted for a given election. - """ - election = get_election(election_id, request.app.state.settings) - manifest = Manifest.from_json_object(election.manifest) - context = election.context.to_sdk_format() - - # get the cast and spoiled ballots by checking the current ballot inventory - # and filtering the table for - inventory = get_ballot_inventory(election_id, request.app.state.settings) - if inventory is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find a ballot inventory with election_id {election_id}", - ) - cast_ballots = filter_ballots( - election_id, - {"state": BallotBoxState.CAST.name}, - 0, - inventory.cast_ballot_count, - request.app.state.settings, - ) - spoiled_ballots = filter_ballots( - election_id, - {"state": BallotBoxState.SPOILED.name}, - 0, - inventory.spoiled_ballot_count, - request.app.state.settings, - ) - # TODO: check inventory list matches find result above and throw if it does not. - - # append the ballots to the eg library tally - sdk_tally = electionguard.tally.CiphertextTally( - f"{election_id}-{tally_name}", InternalManifest(manifest), context - ) - sdk_tally.batch_append( - [(ballot.object_id, ballot) for ballot in cast_ballots], scheduler - ) - sdk_tally.batch_append( - [(ballot.object_id, ballot) for ballot in spoiled_ballots], scheduler - ) - - # create and cache the api tally. - api_tally = CiphertextTally( - election_id=election_id, - tally_name=tally_name, - created=datetime.now(), - tally=sdk_tally.to_json_object(), - ) - - set_ciphertext_tally(api_tally, request.app.state.settings) - - return api_tally - - -@router.post("/find", response_model=CiphertextTallyQueryResponse, tags=[TALLY]) -def find_ciphertext_tallies( - request: Request, - election_id: str, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> CiphertextTallyQueryResponse: - """ - Find tallies. - - Search the repository for tallies that match the filter criteria specified in the request body. - If no filter criteria is specified the API will iterate all available data. - """ - filter = write_json_object(data.filter) if data.filter else {} - tallies = filter_ciphertext_tallies( - election_id, filter, skip, limit, request.app.state.settings - ) - return CiphertextTallyQueryResponse(tallies=tallies) - - -@router.post("/decrypt", response_model=PlaintextTallyQueryResponse, tags=[TALLY]) -async def decrypt_tally( - request: Request, - response: Response, - background_tasks: BackgroundTasks, - restart: bool = False, - data: DecryptTallyRequest = Body(...), -) -> PlaintextTallyQueryResponse: - """ - Decrypt a tally from a collection of decrypted guardian shares. - - Requires that all guardian shares have been submitted. - - The decryption process can take some time, - so the method returns immediately and continues processing in the background. - """ - - # if we already have a value cached, then return it. - plaintext_tallies = filter_plaintext_tallies( - data.election_id, - {"election_id": data.election_id, "tally_name": data.tally_name}, - 0, - 1, - request.app.state.settings, - ) - if not restart and len(plaintext_tallies) > 0: - logger.info("returning plaintext tally from cache") - return PlaintextTallyQueryResponse(tallies=plaintext_tallies) - - logger.info("no tally exists in cache") - - tally = PlaintextTally( - election_id=data.election_id, - tally_name=data.tally_name, - created=datetime.now(), - state=PlaintextTallyState.CREATED, - ) - set_plaintext_tally(tally, request.app.state.settings) - - # queue a background task to execute the tally - # TODO: determine whether we wait or continue - # asyncio.create_task(_decrypt_tally(tally, request.app.state.settings)) - await _decrypt_tally(tally, request.app.state.settings) - - response.status_code = status.HTTP_202_ACCEPTED - return PlaintextTallyQueryResponse( - message="tally computing, check back in a few minutes.", tallies=[tally] - ) - - -async def _decrypt_tally( - api_plaintext_tally: PlaintextTally, settings: Settings = Settings() -) -> None: - - try: - # set the tally state to processing - api_plaintext_tally.state = PlaintextTallyState.PROCESSING - update_plaintext_tally(api_plaintext_tally, settings) - - api_ciphertext_tally = get_ciphertext_tally( - api_plaintext_tally.election_id, api_plaintext_tally.tally_name, settings - ) - - election = get_election(api_plaintext_tally.election_id, settings) - context = election.context.to_sdk_format() - - # filter the guardian shares - query_shares = filter_decryption_shares( - api_plaintext_tally.tally_name, - None, - 0, - context.number_of_guardians, - settings, - ) - - # validate we have all of the guardian shares - # TODO: support thresholding - if len(query_shares) != context.number_of_guardians: - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail=f"{len(query_shares)} of {context.number_of_guardians} guardians have submitted shares", - ) - - # transform the tally shares - tally_shares = { - share.guardian_id: read_json_object(share.tally_share, DecryptionShare) - for share in query_shares - } - - # TODO: HACK: Remove The Empty Manifest - # Note: The CiphertextTally requires an internal manifest passed into its constructor - # but it is not actually used when executing `compute_decryption_share` so we create a fake. - # see: https://github.com/microsoft/electionguard-python/issues/391 - internal_manifest = InternalManifest( - Manifest( - "", - "", - ElectionType.other, - datetime.now(), - datetime.now(), - [], - [], - [], - [], - [], - ) - ) - sdk_ciphertext_tally = electionguard.tally.CiphertextTally( - api_ciphertext_tally.tally_name, internal_manifest, context - ) - contests: Dict[CONTEST_ID, electionguard.tally.CiphertextTallyContest] = { - contest_id: read_json_object( - contest, electionguard.tally.CiphertextTallyContest - ) - for contest_id, contest in api_ciphertext_tally.tally["contests"].items() - } - sdk_ciphertext_tally.contests = contests - - # decrypt - sdk_plaintext_tally = decrypt( - sdk_ciphertext_tally, - tally_shares, - context.crypto_extended_base_hash, - ) - if not sdk_plaintext_tally: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Unable to decrypt tally", - ) - - # cache the tally plaintext tally - api_plaintext_tally.tally = write_json_object(sdk_plaintext_tally) - api_plaintext_tally.state = PlaintextTallyState.COMPLETE - update_plaintext_tally(api_plaintext_tally, settings) - - except HTTPException: - api_plaintext_tally.state = PlaintextTallyState.ERROR - update_plaintext_tally(api_plaintext_tally, settings) - print(sys.exc_info()) - raise - except Exception as error: - api_plaintext_tally.state = PlaintextTallyState.ERROR - update_plaintext_tally(api_plaintext_tally, settings) - logger.exception(sys.exc_info()) - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="decrypt plaintext tally failed", - ) from error diff --git a/app/api/v1/mediator/tally_decrypt.py b/app/api/v1/mediator/tally_decrypt.py deleted file mode 100644 index cda633d..0000000 --- a/app/api/v1/mediator/tally_decrypt.py +++ /dev/null @@ -1,106 +0,0 @@ -from fastapi import APIRouter, Body, HTTPException, Request, status - -from electionguard.key_ceremony import PublicKeySet -from electionguard.decryption_share import DecryptionShare -from electionguard.serializable import read_json_object -from electionguard.tally import CiphertextTallyContest -from electionguard.utils import get_optional - -from app.core.election import get_election -from app.core.key_guardian import get_key_guardian -from app.core.tally import get_ciphertext_tally -from app.core.tally_decrypt import ( - get_decryption_share, - set_decryption_share, - filter_decryption_shares, -) -from ..models import ( - BaseResponse, - DecryptionShareResponse, - BaseQueryRequest, - DecryptionShareRequest, -) -from ..tags import TALLY_DECRYPT - - -router = APIRouter() - - -@router.get("", response_model=DecryptionShareResponse, tags=[TALLY_DECRYPT]) -def fetch_decryption_share( - request: Request, election_id: str, tally_name: str, guardian_id: str -) -> DecryptionShareResponse: - """Get a decryption share for a specific tally for a specific guardian.""" - share = get_decryption_share( - election_id, tally_name, guardian_id, request.app.state.settings - ) - return DecryptionShareResponse( - shares=[share], - ) - - -@router.post("/submit-share", response_model=BaseResponse, tags=[TALLY_DECRYPT]) -def submit_share( - request: Request, - data: DecryptionShareRequest = Body(...), -) -> BaseResponse: - """ - Announce a guardian participating in a tally decryption by submitting a decryption share. - """ - - election = get_election(data.share.election_id, request.app.state.settings) - context = election.context.to_sdk_format() - guardian = get_key_guardian( - election.key_name, data.share.guardian_id, request.app.state.settings - ) - public_keys = read_json_object(get_optional(guardian.public_keys), PublicKeySet) - - api_tally = get_ciphertext_tally( - data.share.election_id, data.share.tally_name, request.app.state.settings - ) - tally_share = read_json_object(data.share.tally_share, DecryptionShare) - - # TODO: spoiled ballot shares - # ballot_shares = [ - # read_json_object(ballot_share, DecryptionShare) - # for ballot_share in data.share.ballot_shares - # ] - - # validate the decryption share data matches the expectations in the tally - # TODO: use the SDK for validation - # sdk_tally = read_json_object(api_tally.tally, electionguard.tally.CiphertextTally) - for contest_id, contest in api_tally.tally["contests"].items(): - tally_contest = read_json_object(contest, CiphertextTallyContest) - contest_share = tally_share.contests[contest_id] - for selection_id, selection in tally_contest.selections.items(): - selection_share = contest_share.selections[selection_id] - if not selection_share.is_valid( - selection.ciphertext, - public_keys.election.key, - context.crypto_extended_base_hash, - ): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"decryption share failed valitation for contest: {contest_id} selection: {selection_id}", - ) - - # TODO: validate spoiled ballot shares - - return set_decryption_share(data.share, request.app.state.settings) - - -@router.post("/find", response_model=DecryptionShareResponse, tags=[TALLY_DECRYPT]) -def find_decryption_shares( - request: Request, - tally_name: str, - skip: int = 0, - limit: int = 100, - data: BaseQueryRequest = Body(...), -) -> DecryptionShareResponse: - """Find descryption shares for a specific tally.""" - shares = filter_decryption_shares( - tally_name, data.filter, skip, limit, request.app.state.settings - ) - return DecryptionShareResponse( - shares=shares, - ) diff --git a/app/api/v1/models/__init__.py b/app/api/v1/models/__init__.py deleted file mode 100644 index 0996ee4..0000000 --- a/app/api/v1/models/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .auth import * -from .ballot import * -from .base import * -from .decrypt import * -from .encrypt import * -from .election import * -from .guardian import * -from .key_ceremony import * -from .key_guardian import * -from .manifest import * -from .tally import * -from .tally_decrypt import * -from .user import * diff --git a/app/api/v1/models/auth.py b/app/api/v1/models/auth.py deleted file mode 100644 index eb20eba..0000000 --- a/app/api/v1/models/auth.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import List, Optional - -from pydantic import BaseModel - -from app.api.v1.models.user import UserScope - -__all__ = [ - "AuthenticationCredential", - "Token", - "TokenData", -] - - -class AuthenticationCredential(BaseModel): - """Authentication credential used to authenticate users.""" - - username: str - hashed_password: str - - -class Token(BaseModel): - """An access token and its type.""" - - access_token: str - token_type: str - - -class TokenData(BaseModel): - """The payload of an access token.""" - - username: Optional[str] = None - scopes: List[UserScope] = [] - - -class ErrorMessage(BaseModel): - """Returns error messages to the client.""" - - detail: str diff --git a/app/api/v1/models/ballot.py b/app/api/v1/models/ballot.py deleted file mode 100644 index 90934f2..0000000 --- a/app/api/v1/models/ballot.py +++ /dev/null @@ -1,294 +0,0 @@ -from typing import Any, Dict, List, Optional -from electionguard.ballot import ( - SubmittedBallot, - CiphertextBallotContest, - CiphertextBallotSelection, -) -from electionguard.chaum_pedersen import DisjunctiveChaumPedersenProof -from electionguard.elgamal import ElGamalCiphertext -from electionguard.chaum_pedersen import ConstantChaumPedersenProof -from electionguard.ballot_box import BallotBoxState -from electionguard.proof import ProofUsage - - -from app.api.v1.common.type_mapper import ( - string_to_element_mod_p, - string_to_element_mod_q, -) -from app.api.v1.models.election import CiphertextElectionContextDto -from app.api.v1_1.models.election import AnyCiphertextElectionContext - -from .base import Base, BaseRequest, BaseResponse, BaseValidationRequest -from .manifest import ElectionManifest, ElementModQ - -__all__ = [ - "BallotQueryResponse", - "BallotInventory", - "BallotInventoryResponse", - "BaseBallotRequest", - "CastBallotsRequest", - "SpoilBallotsRequest", - "SubmitBallotsRequest", - "ValidateBallotRequest", -] - -DecryptionShare = Any -AnySubmittedBallot = Any -AnyCiphertextBallot = Any -PlaintextBallot = Any - -BALLOT_CODE = str -BALLOT_URL = str - - -class ElementModQDto(Base): - data: str - - def to_sdk_format(self) -> ElementModQ: - return string_to_element_mod_q(self.data) - - -class ElementModPDto(Base): - data: str - - def to_sdk_format(self) -> ElementModQ: - return string_to_element_mod_p(self.data) - - -class BallotInventory(Base): - """ - The Ballot Inventory retains metadata about ballots in an election, - including mappings of ballot tracking codes to ballot id's - """ - - election_id: str - cast_ballot_count: int = 0 - spoiled_ballot_count: int = 0 - cast_ballots: Dict[BALLOT_CODE, BALLOT_URL] = {} - """ - Collection of cast ballot codes mapped to a route that is accessible. - - Note: the BALLOT_URL is storage dependent and is not meant to be shared with the election record - """ - spoiled_ballots: Dict[BALLOT_CODE, BALLOT_URL] = {} - """ - Collection of spoiled ballot codes mapped to a route that is accessible. - - Note: the BALLOT_URL is storage dependent and is not meant to be shared with the election record - """ - - -class BallotInventoryResponse(BaseResponse): - inventory: BallotInventory - - -class BallotQueryResponse(BaseResponse): - election_id: str - ballots: List[AnyCiphertextBallot] = [] - - -class BaseBallotRequest(BaseRequest): - election_id: Optional[str] = None - manifest: Optional[ElectionManifest] = None - context: Optional[CiphertextElectionContextDto] = None - - -class CastBallotsRequest(BaseBallotRequest): - """Cast the enclosed ballots.""" - - ballots: List[AnyCiphertextBallot] - - -class SpoilBallotsRequest(BaseBallotRequest): - """Spoil the enclosed ballots.""" - - ballots: List[AnyCiphertextBallot] - - -class SubmitBallotsRequest(BaseBallotRequest): - """Submit a ballot against a specific election.""" - - ballots: List[AnySubmittedBallot] - - -class ElGamalCiphertextDto(Base): - pad: ElementModPDto - data: ElementModPDto - - def to_sdk_format(self) -> ElGamalCiphertext: - pad = self.pad.to_sdk_format() - data = self.data.to_sdk_format() - result = ElGamalCiphertext(pad, data) - return result - - -class ConstantChaumPedersenProofDto(Base): - pad: ElementModPDto - data: ElementModPDto - challenge: ElementModQDto - response: ElementModQDto - constant: int - usage: str - - def to_sdk_format(self) -> ConstantChaumPedersenProof: - pad = self.pad.to_sdk_format() - data = self.data.to_sdk_format() - challenge = self.challenge.to_sdk_format() - proof_response = self.response.to_sdk_format() - usage = ProofUsage(self.usage) - result = ConstantChaumPedersenProof( - pad, data, challenge, proof_response, self.constant, usage - ) - return result - - -class DisjunctiveChaumPedersenProofDto(Base): - proof_zero_pad: ElementModPDto - proof_zero_data: ElementModPDto - proof_one_pad: ElementModPDto - proof_one_data: ElementModPDto - proof_zero_challenge: ElementModQDto - proof_one_challenge: ElementModQDto - challenge: ElementModQDto - proof_zero_response: ElementModQDto - proof_one_response: ElementModQDto - usage: str - - def to_sdk_format(self) -> DisjunctiveChaumPedersenProof: - proof_zero_pad = self.proof_zero_pad.to_sdk_format() - proof_zero_data = self.proof_zero_data.to_sdk_format() - proof_one_pad = self.proof_one_pad.to_sdk_format() - proof_one_data = self.proof_one_data.to_sdk_format() - proof_zero_challenge = self.proof_zero_challenge.to_sdk_format() - proof_one_challenge = self.proof_one_challenge.to_sdk_format() - challenge = self.challenge.to_sdk_format() - proof_zero_response = self.proof_zero_response.to_sdk_format() - proof_one_response = self.proof_one_response.to_sdk_format() - usage = ProofUsage(self.usage) - - result = DisjunctiveChaumPedersenProof( - proof_zero_pad, - proof_zero_data, - proof_one_pad, - proof_one_data, - proof_zero_challenge, - proof_one_challenge, - challenge, - proof_zero_response, - proof_one_response, - usage, - ) - return result - - -class BallotSelectionDto(Base): - object_id: str - sequence_order: int - description_hash: ElementModQDto - ciphertext: ElGamalCiphertextDto - crypto_hash: ElementModQDto - is_placeholder_selection: bool - nonce: Optional[ElementModQDto] = None - proof: DisjunctiveChaumPedersenProofDto - extended_data: Optional[ElGamalCiphertextDto] = None - - def to_sdk_format(self) -> CiphertextBallotSelection: - description_hash = self.description_hash.to_sdk_format() - ciphertext = self.ciphertext.to_sdk_format() - crypto_hash = self.crypto_hash.to_sdk_format() - nonce = None if self.nonce is None else self.nonce.to_sdk_format() - proof = self.proof.to_sdk_format() - extended_data = ( - None if self.extended_data is None else self.extended_data.to_sdk_format() - ) - result = CiphertextBallotSelection( - self.object_id, - description_hash, - ciphertext, - crypto_hash, - self.is_placeholder_selection, - nonce, - proof, - extended_data, - ) - return result - - -class ContestDto(Base): - object_id: str - description_hash: ElementModQDto - ciphertext_accumulation: ElGamalCiphertextDto - crypto_hash: ElementModQDto - nonce: Optional[ElementModQDto] = None - proof: ConstantChaumPedersenProofDto - ballot_selections: List[BallotSelectionDto] - - def to_sdk_format(self) -> CiphertextBallotContest: - description_hash = self.description_hash.to_sdk_format() - crypto_hash = self.crypto_hash.to_sdk_format() - ballot_selections = list( - map(lambda s: s.to_sdk_format(), self.ballot_selections) - ) - ciphertext_accumulation = self.ciphertext_accumulation.to_sdk_format() - nonce = None if self.nonce is None else self.nonce.to_sdk_format() - proof = self.proof.to_sdk_format() - result = CiphertextBallotContest( - self.object_id, - description_hash, - ballot_selections, - ciphertext_accumulation, - crypto_hash, - nonce, - proof, - ) - return result - - -class SubmittedBallotDto(Base): - state: int - code: ElementModQDto - object_id: str - style_id: str - manifest_hash: ElementModQDto - code_seed: ElementModQDto - crypto_hash: ElementModQDto - nonce: Optional[ElementModQDto] = None - timestamp: int - contests: List[ContestDto] - - def to_sdk_format(self) -> SubmittedBallot: - state = BallotBoxState(self.state) - code = self.code.to_sdk_format() - manifest_hash = self.manifest_hash.to_sdk_format() - code_seed = self.code_seed.to_sdk_format() - crypto_hash = self.crypto_hash.to_sdk_format() - contests = list(map(lambda c: c.to_sdk_format(), self.contests)) - nonce = None if self.nonce is None else self.nonce.to_sdk_format() - - ballot = SubmittedBallot( - self.object_id, - self.style_id, - manifest_hash, - code_seed, - contests, - code, - self.timestamp, - crypto_hash, - nonce, - state, - ) - return ballot - - -class SubmitBallotsRequestDto(BaseValidationRequest): - """Submit a ballot against a specific election.""" - - ballots: List[SubmittedBallotDto] - - -class ValidateBallotRequest(BaseValidationRequest): - """Submit a ballot against a specific election description and contest to determine if it is accepted.""" - - ballot: AnyCiphertextBallot - manifest: ElectionManifest - context: AnyCiphertextElectionContext diff --git a/app/api/v1/models/base.py b/app/api/v1/models/base.py deleted file mode 100644 index b662817..0000000 --- a/app/api/v1/models/base.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Any, Optional -from enum import Enum -from pydantic import BaseModel - -__all__ = [ - "Base", - "BaseRequest", - "BaseResponse", - "BaseQueryRequest", - "BaseValidationRequest", - "BaseValidationResponse", - "ResponseStatus", -] - -Schema = Any - - -class ResponseStatus(str, Enum): - FAIL = "fail" - SUCCESS = "success" - - -class Base(BaseModel): - "A basic model object" - - -class BaseRequest(BaseModel): - """A basic request""" - - -class BaseResponse(BaseModel): - """A basic response""" - - status: ResponseStatus = ResponseStatus.SUCCESS - """The status of the response""" - - message: Optional[str] = None - """An optional message describing the response""" - - def is_success(self) -> bool: - return self.status == ResponseStatus.SUCCESS - - -class BaseQueryRequest(BaseRequest): - """Find something""" - - filter: Optional[Any] = None - - -class BaseValidationRequest(BaseRequest): - """Base validation request""" - - schema_override: Optional[Schema] = None - """Optionally specify a schema to validate against""" - - -class BaseValidationResponse(BaseResponse): - """Response for validating models""" - - details: Optional[str] = None - """Optional details of the validation result""" diff --git a/app/api/v1/models/decrypt.py b/app/api/v1/models/decrypt.py deleted file mode 100644 index ed1dbbb..0000000 --- a/app/api/v1/models/decrypt.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Dict, List - -from .base import BaseRequest -from .election import CiphertextElectionContextDto -from .guardian import Guardian, GuardianId - -__all__ = [ - "DecryptBallotSharesRequest", - "DecryptBallotSharesResponse", - "DecryptBallotsWithSharesRequest", -] - -DecryptionShare = Any -SubmittedBallot = Any - - -class DecryptBallotsWithSharesRequest(BaseRequest): - """ - Decrypt the provided ballots with the provided shares - """ - - encrypted_ballots: List[SubmittedBallot] - shares: Dict[GuardianId, List[DecryptionShare]] - context: CiphertextElectionContextDto - - -class DecryptBallotSharesRequest(BaseRequest): - encrypted_ballots: List[SubmittedBallot] - guardian: Guardian - context: CiphertextElectionContextDto - - -class DecryptBallotSharesResponse(BaseRequest): - shares: List[DecryptionShare] = [] diff --git a/app/api/v1/models/election.py b/app/api/v1/models/election.py deleted file mode 100644 index 4064b19..0000000 --- a/app/api/v1/models/election.py +++ /dev/null @@ -1,151 +0,0 @@ -from typing import Any, List, Optional -from enum import Enum -from electionguard.election import CiphertextElectionContext - -from app.api.v1.common.type_mapper import ( - string_to_element_mod_p, - string_to_element_mod_q, -) - -from .base import Base, BaseRequest, BaseResponse -from .manifest import ElectionManifest - - -__all__ = [ - "Election", - "ElectionState", - "ElectionQueryRequest", - "ElectionQueryResponse", - "MakeElectionContextRequest", - "MakeElectionContextResponse", - "SubmitElectionRequest", -] - - -class CiphertextElectionContextDto(Base): - """The meta-data required for an election including keys, manifest, number of guardians, and quorum""" - - number_of_guardians: int - """ - The number of guardians necessary to generate the public key - """ - quorum: int - """ - The quorum of guardians necessary to decrypt an election. Must be less than `number_of_guardians` - """ - - elgamal_public_key: str - """the `joint public key (K)` in the [ElectionGuard Spec](https://github.com/microsoft/electionguard/wiki)""" - - commitment_hash: str - """ - the `commitment hash H(K 1,0 , K 2,0 ... , K n,0 )` of the public commitments - guardians make to each other in the [ElectionGuard Spec](https://github.com/microsoft/electionguard/wiki) - """ - - manifest_hash: str - """The hash of the election metadata""" - - crypto_base_hash: str - """The `base hash code (𝑄)` in the [ElectionGuard Spec](https://github.com/microsoft/electionguard/wiki)""" - - crypto_extended_base_hash: str - """The `extended base hash code (𝑄')` in [ElectionGuard Spec](https://github.com/microsoft/electionguard/wiki)""" - - def to_sdk_format(self) -> CiphertextElectionContext: - sdk_context = CiphertextElectionContext( - self.number_of_guardians, - self.quorum, - string_to_element_mod_p(self.elgamal_public_key), - string_to_element_mod_q(self.commitment_hash), - string_to_element_mod_q(self.manifest_hash), - string_to_element_mod_q(self.crypto_base_hash), - string_to_element_mod_q(self.crypto_extended_base_hash), - ) - return sdk_context - - -class ElectionState(str, Enum): - CREATED = "CREATED" - OPEN = "OPEN" - CLOSED = "CLOSED" - PUBLISHED = "PUBLISHED" - - -class Election(Base): - """An election object.""" - - def get_name(self) -> str: - text = self.manifest["name"]["text"] - # todo: replace "en" with user's current culture - enText = [t["value"] for t in text if t["language"] == "en"] - return str(enText[0]) - - election_id: str - key_name: str - state: ElectionState - context: CiphertextElectionContextDto - manifest: ElectionManifest - - -class ElectionQueryRequest(BaseRequest): - """A request for elections using the specified filter.""" - - filter: Optional[Any] = None - """ - a json object filter that will be applied to the search. - """ - - class Config: - schema_extra = {"example": {"filter": {"election_id": "some-election-id-123"}}} - - -class ElectionQueryResponse(BaseResponse): - """A collection of elections.""" - - elections: List[Election] = [] - - -class ElectionSummaryDto(Base): - election_id: str - name: Any - state: str - number_of_guardians: int - quorum: int - cast_ballot_count: int - spoiled_ballot_count: int - index: int - - -class ElectionListResponseDto(BaseResponse): - """A collection of elections.""" - - elections: List[ElectionSummaryDto] = [] - - -class SubmitElectionRequest(BaseRequest): - """Submit an election.""" - - election_id: str - key_name: str - context: CiphertextElectionContextDto - manifest: Optional[ElectionManifest] = None - - -class MakeElectionContextRequest(BaseRequest): - """ - A request to build an Election Context for a given election. - """ - - elgamal_public_key: str - commitment_hash: str - number_of_guardians: int - quorum: int - manifest_hash: Optional[str] = None - manifest: Optional[ElectionManifest] = None - - -class MakeElectionContextResponse(BaseResponse): - """A Ciphertext Election Context.""" - - context: CiphertextElectionContextDto diff --git a/app/api/v1/models/encrypt.py b/app/api/v1/models/encrypt.py deleted file mode 100644 index ccca3dd..0000000 --- a/app/api/v1/models/encrypt.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any, List - -from .base import BaseRequest, BaseResponse - - -__all__ = [ - "EncryptBallotsRequest", - "EncryptBallotsResponse", -] - -CiphertextBallot = Any -PlaintextBallot = Any - - -class EncryptBallotsRequest(BaseRequest): - """A request to encrypt the enclosed ballots.""" - - election_id: str - seed_hash: str - ballots: List[PlaintextBallot] - - -class EncryptBallotsResponse(BaseResponse): - encrypted_ballots: List[CiphertextBallot] - """The encrypted representations of the plaintext ballots.""" - next_seed_hash: str - """A seed hash which can optionally be used for the next call to encrypt.""" diff --git a/app/api/v1/models/guardian.py b/app/api/v1/models/guardian.py deleted file mode 100644 index 1ac1213..0000000 --- a/app/api/v1/models/guardian.py +++ /dev/null @@ -1,214 +0,0 @@ -from typing import Any, Dict, List, Optional - -import electionguard.auxiliary -import electionguard.election_polynomial -import electionguard.elgamal -import electionguard.group -import electionguard.guardian -import electionguard.key_ceremony -import electionguard.schnorr -from electionguard.serializable import read_json_object -from electionguard.type import GUARDIAN_ID - -from .base import Base, BaseRequest, BaseResponse - -__all__ = [ - "ElectionPolynomial", - "ElectionPartialKeyBackup", - "ElectionPartialKeyChallenge", - "Guardian", - "CreateGuardianRequest", - "GuardianPublicKeysResponse", - "GuardianBackupRequest", - "GuardianBackupResponse", - "BackupVerificationRequest", - "BackupVerificationResponse", - "BackupChallengeRequest", - "BackupChallengeResponse", - "ChallengeVerificationRequest", - "to_sdk_guardian", - "ApiGuardianQueryResponse", -] - -ElectionPolynomial = Any -ElectionPartialKeyBackup = Any -ElectionPartialKeyChallenge = Any -ElectionPartialKeyVerification = Any -GuardianId = Any - -ElectionKeyPair = Any -AuxiliaryKeyPair = Any - -AuxiliaryPublicKey = Any -ElectionPublicKey = Any - -PublicKeySet = Any - - -class Guardian(Base): - """The API guardian tracks the state of a guardain's interactions with other guardians.""" - - guardian_id: str - name: str - sequence_order: int - number_of_guardians: int - quorum: int - election_keys: ElectionKeyPair - auxiliary_keys: AuxiliaryKeyPair - backups: Dict[GUARDIAN_ID, ElectionPartialKeyBackup] = {} - cohort_public_keys: Dict[GUARDIAN_ID, PublicKeySet] = {} - cohort_backups: Dict[GUARDIAN_ID, ElectionPartialKeyBackup] = {} - cohort_verifications: Dict[GUARDIAN_ID, ElectionPartialKeyVerification] = {} - cohort_challenges: Dict[GUARDIAN_ID, ElectionPartialKeyChallenge] = {} - - -class CreateGuardianRequest(BaseRequest): - """Request to create a Guardain.""" - - guardian_id: str - name: Optional[str] = None - sequence_order: int - number_of_guardians: int - quorum: int - nonce: Optional[str] = None - auxiliary_key_pair: Optional[AuxiliaryKeyPair] = None - - -class CreateElectionKeyPairRequest(BaseRequest): - """Request to create an Election Key Pair.""" - - owner_id: str - sequence_order: int - quorum: int - nonce: Optional[str] = None - - -class CreateElectionKeyPairResponse(BaseResponse): - """Returns an ElectionKeyPair.""" - - election_key_pair: ElectionKeyPair - - -class CreateAuxiliaryKeyPairRequest(BaseRequest): - """Request to create an AuxiliaryKeyPair.""" - - owner_id: str - sequence_order: int - - -class CreateAuxiliaryKeyPairResponse(BaseResponse): - """Returns an AuxiliaryKeyPair.""" - - auxiliary_key_pair: AuxiliaryKeyPair - - -class GuardianPublicKeysResponse(BaseResponse): - """Returns a set of public auxiliary and election keys.""" - - public_keys: PublicKeySet - - -class GuardianBackupRequest(BaseRequest): - """Request to generate ElectionPartialKeyBackups for the given PublicKeySets.""" - - guardian_id: str - quorum: int - public_keys: List[PublicKeySet] - override_rsa: bool = False - - -class GuardianBackupResponse(BaseResponse): - """Returns a collection of ElectionPartialKeyBackups to be shared with other guardians.""" - - guardian_id: str - backups: List[ElectionPartialKeyBackup] - - -class BackupVerificationRequest(BaseRequest): - """Request to verify the associated backups shared with the guardian.""" - - guardian_id: str - backup: ElectionPartialKeyBackup - override_rsa: bool = False - - -class BackupVerificationResponse(BaseResponse): - """Returns a collection of verifications.""" - - verification: ElectionPartialKeyVerification - - -class BackupChallengeRequest(BaseRequest): - """Request to challenge a specific backup.""" - - guardian_id: str - backup: ElectionPartialKeyBackup - - -class BackupChallengeResponse(BaseResponse): - """Returns a challenge to a given backup.""" - - challenge: ElectionPartialKeyChallenge - - -class ChallengeVerificationRequest(BaseRequest): - """Request to verify a challenge.""" - - verifier_id: str - challenge: ElectionPartialKeyChallenge - - -class ApiGuardianQueryResponse(BaseResponse): - """Returns a collection of KeyCeremonyGuardians.""" - - guardians: List[Guardian] - - -# pylint:disable=protected-access -def to_sdk_guardian(api_guardian: Guardian) -> electionguard.guardian.Guardian: - """ - Convert an API Guardian model to a fully-hydrated SDK Guardian model. - """ - - guardian = electionguard.guardian.Guardian( - api_guardian.guardian_id, - api_guardian.sequence_order, - api_guardian.number_of_guardians, - api_guardian.quorum, - ) - - guardian._auxiliary_keys = read_json_object( - api_guardian.auxiliary_keys, electionguard.auxiliary.AuxiliaryKeyPair - ) - guardian._election_keys = read_json_object( - api_guardian.election_keys, electionguard.key_ceremony.ElectionKeyPair - ) - - cohort_keys = { - owner_id: read_json_object(key_set, electionguard.key_ceremony.PublicKeySet) - for (owner_id, key_set) in api_guardian.cohort_public_keys.items() - } - - guardian._guardian_auxiliary_public_keys = { - owner_id: key_set.auxiliary for (owner_id, key_set) in cohort_keys.items() - } - - guardian._guardian_election_public_keys = { - owner_id: key_set.election for (owner_id, key_set) in cohort_keys.items() - } - - guardian._guardian_election_partial_key_backups = { - owner_id: read_json_object( - backup, electionguard.key_ceremony.ElectionPartialKeyBackup - ) - for (owner_id, backup) in api_guardian.cohort_backups.items() - } - - guardian._guardian_election_partial_key_verifications = { - owner_id: read_json_object( - verification, electionguard.key_ceremony.ElectionPartialKeyVerification - ) - for (owner_id, verification) in api_guardian.cohort_verifications.items() - } - - return guardian diff --git a/app/api/v1/models/key_ceremony.py b/app/api/v1/models/key_ceremony.py deleted file mode 100644 index f29fbc0..0000000 --- a/app/api/v1/models/key_ceremony.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Any, Dict, List, Optional -from enum import Enum - -from electionguard.type import GUARDIAN_ID - -from .base import Base, BaseRequest, BaseResponse -from .key_guardian import KeyCeremonyGuardianState, ElectionPartialKeyVerification - -__all__ = [ - "KeyCeremony", - "KeyCeremonyState", - "KeyCeremonyCreateRequest", - "KeyCeremonyStateResponse", - "KeyCeremonyQueryResponse", - "KeyCeremonyVerifyChallengesResponse", - "PublishElectionJointKeyRequest", - "ElectionJointKeyResponse", -] - -ElectionPublicKey = Any -ElGamalKeyPair = Any - -ElectionJointKey = Any -ElementModQ = Any - - -class KeyCeremonyState(str, Enum): - """Enumeration expressing the state of the key caremony.""" - - CREATED = "CREATED" - OPEN = "OPEN" - CLOSED = "CLOSED" - CHALLENGED = "CHALLENGED" - CANCELLED = "CANCELLED" - - -class KeyCeremony(Base): - """The Key Ceremony is a record of the state of a key ceremony.""" - - key_name: str - state: KeyCeremonyState - number_of_guardians: int - quorum: int - guardian_ids: List[GUARDIAN_ID] - guardian_status: Dict[GUARDIAN_ID, KeyCeremonyGuardianState] - elgamal_public_key: Optional[ElectionJointKey] = None - commitment_hash: Optional[ElementModQ] = None - - -class KeyCeremonyStateResponse(Base): - """Returns a subset of KeyCeremony data that describes only the state.""" - - key_name: str - state: KeyCeremonyState - guardian_status: Dict[GUARDIAN_ID, KeyCeremonyGuardianState] - - -class KeyCeremonyQueryResponse(BaseResponse): - """Returns a collection of Key Ceremonies.""" - - key_ceremonies: List[KeyCeremony] - - -class KeyCeremonyVerifyChallengesResponse(BaseResponse): - """Returns a collection of Partial Key Verifications.""" - - verifications: List[ElectionPartialKeyVerification] - - -class KeyCeremonyCreateRequest(BaseRequest): - """Request to create a new key ceremony.""" - - key_name: str - number_of_guardians: int - quorum: int - guardian_ids: List[str] - - -class PublishElectionJointKeyRequest(BaseRequest): - """Request to publish the election joint key.""" - - key_name: str - election_public_keys: List[ElectionPublicKey] - - -class ElectionJointKeyResponse(BaseResponse): - """Response object containing the Election Joint Key.""" - - elgamal_public_key: ElectionJointKey - commitment_hash: ElementModQ diff --git a/app/api/v1/models/key_guardian.py b/app/api/v1/models/key_guardian.py deleted file mode 100644 index 1605171..0000000 --- a/app/api/v1/models/key_guardian.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Any, List, Optional -from enum import Enum - -from electionguard.type import GUARDIAN_ID - -from .base import Base, BaseRequest, BaseResponse - - -__all__ = [ - "GuardianAnnounceRequest", - "GuardianSubmitBackupRequest", - "GuardianQueryResponse", - "GuardianSubmitVerificationRequest", - "GuardianSubmitChallengeRequest", - "KeyCeremonyGuardian", - "KeyCeremonyGuardianStatus", - "KeyCeremonyGuardianState", -] - -PublicKeySet = Any - -ElectionPartialKeyBackup = Any -ElectionPartialKeyVerification = Any -ElectionPartialKeyChallenge = Any - - -class KeyCeremonyGuardianStatus(str, Enum): - """Enumeration expressing the status of a guardian's operations.""" - - INCOMPLETE = "INCOMPLETE" - ERROR = "ERROR" - COMPLETE = "COMPLETE" - - -class KeyCeremonyGuardianState(Base): - """Describes the operations each guardian must fulfill to complete a key ceremony.""" - - public_key_shared: KeyCeremonyGuardianStatus = KeyCeremonyGuardianStatus.INCOMPLETE - backups_shared: KeyCeremonyGuardianStatus = KeyCeremonyGuardianStatus.INCOMPLETE - backups_verified: KeyCeremonyGuardianStatus = KeyCeremonyGuardianStatus.INCOMPLETE - - -class KeyCeremonyGuardian(Base): - """ - A record of the public data exchanged between guardians. - """ - - key_name: str - guardian_id: GUARDIAN_ID - name: str - sequence_order: int - number_of_guardians: int - quorum: int - public_keys: Optional[PublicKeySet] = None - backups: List[ElectionPartialKeyBackup] = [] - verifications: List[ElectionPartialKeyVerification] = [] - challenges: List[ElectionPartialKeyChallenge] = [] - - -class GuardianAnnounceRequest(BaseRequest): - """A set of public auxiliary and election keys.""" - - key_name: str - """The Key Ceremony to announce""" - public_keys: PublicKeySet - - -class GuardianQueryResponse(BaseResponse): - """Returns a collection of KeyCeremonyGuardians.""" - - guardians: List[KeyCeremonyGuardian] - - -class GuardianSubmitBackupRequest(BaseRequest): - """Submit a collection of backups for a guardian.""" - - key_name: str - guardian_id: str - backups: List[ElectionPartialKeyBackup] - - -class GuardianSubmitVerificationRequest(BaseRequest): - """Submit a collection of verifications for a guardian.""" - - key_name: str - guardian_id: str - verifications: List[ElectionPartialKeyVerification] - - -class GuardianSubmitChallengeRequest(BaseRequest): - """Submit a collection of challenges for a guardian.""" - - key_name: str - guardian_id: str - challenges: List[ElectionPartialKeyChallenge] diff --git a/app/api/v1/models/manifest.py b/app/api/v1/models/manifest.py deleted file mode 100644 index 300d56e..0000000 --- a/app/api/v1/models/manifest.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Any, List, Optional - -from .base import ( - Base, - BaseResponse, - BaseValidationRequest, - BaseValidationResponse, -) - -__all__ = [ - "Manifest", - "ManifestSubmitResponse", - "ManifestQueryResponse", - "ValidateManifestRequest", - "ValidateManifestResponse", -] - -ElectionManifest = Any -ElementModQ = Any - - -class Manifest(Base): - manifest_hash: ElementModQ - manifest: ElectionManifest - - -class ManifestSubmitResponse(BaseResponse): - manifest_hash: ElementModQ - - -class ManifestQueryResponse(BaseResponse): - manifests: List[Manifest] - - -class ValidateManifestRequest(BaseValidationRequest): - """ - A request to validate an Election Description. - """ - - manifest: ElectionManifest - """The manifest to validate.""" - - -class ValidateManifestResponse(BaseValidationResponse): - manifest_hash: Optional[str] = None diff --git a/app/api/v1/models/tally.py b/app/api/v1/models/tally.py deleted file mode 100644 index af3cea8..0000000 --- a/app/api/v1/models/tally.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Any, List, Optional -from enum import Enum -from datetime import datetime - -from .base import BaseResponse, BaseRequest, Base - -__all__ = [ - "CiphertextTally", - "CiphertextTallyQueryResponse", - "DecryptTallyRequest", - "PlaintextTally", - "PlaintextTallyState", - "PlaintextTallyQueryResponse", -] - -ElectionGuardCiphertextTally = Any -ElectionGuardPlaintextTally = Any - - -class CiphertextTally(Base): - """A Tally for a specific election.""" - - election_id: str - tally_name: str - created: datetime - tally: ElectionGuardCiphertextTally - """The full electionguard CiphertextTally that includes the cast and spoiled ballot id's.""" - - -class PlaintextTallyState(str, Enum): - CREATED = "CREATED" - PROCESSING = "PROCESSING" - ERROR = "ERROR" - COMPLETE = "COMPLETE" - - -class PlaintextTally(Base): - """A plaintext tally for a specific election.""" - - election_id: str - tally_name: str - created: datetime - state: PlaintextTallyState - tally: Optional[ElectionGuardPlaintextTally] = None - - -class CiphertextTallyQueryResponse(BaseResponse): - """A collection of Ciphertext Tallies.""" - - tallies: List[CiphertextTally] = [] - - -class PlaintextTallyQueryResponse(BaseResponse): - """A collection of Plaintext Tallies.""" - - tallies: List[PlaintextTally] = [] - - -class DecryptTallyRequest(BaseRequest): - """A request to decrypt a specific tally. Can optionally include the tally to decrypt.""" - - election_id: str - tally_name: str diff --git a/app/api/v1/models/tally_decrypt.py b/app/api/v1/models/tally_decrypt.py deleted file mode 100644 index fb3de8d..0000000 --- a/app/api/v1/models/tally_decrypt.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Any, Dict, List - -from electionguard.type import BALLOT_ID - -from .base import BaseResponse, BaseRequest, Base -from .election import CiphertextElectionContextDto -from .tally import CiphertextTally - -__all__ = [ - "CiphertextTallyDecryptionShare", - "DecryptTallyShareRequest", - "DecryptionShareRequest", - "DecryptionShareResponse", -] - -DecryptionShare = Any -ElectionGuardCiphertextTally = Any - - -class CiphertextTallyDecryptionShare(Base): - """ - A DecryptionShare provided by a guardian for a specific tally. - - Optionally can include ballot_shares for challenge ballots. - """ - - election_id: str # TODO: not needed since we have the tally_name? - tally_name: str - guardian_id: str - tally_share: DecryptionShare - """The EG Decryptionshare that includes a share for each contest in the election.""" - ballot_shares: Dict[BALLOT_ID, DecryptionShare] = {} - """A collection of shares for each challenge ballot.""" - - -class DecryptTallyShareRequest(BaseRequest): - """A request to partially decrypt a tally and generate a DecryptionShare.""" - - guardian_id: str - encrypted_tally: CiphertextTally - context: CiphertextElectionContextDto - - -class DecryptionShareRequest(BaseRequest): - """A request to submit a decryption share.""" - - share: CiphertextTallyDecryptionShare - - -class DecryptionShareResponse(BaseResponse): - """A response that includes a collection of decryption shares.""" - - shares: List[CiphertextTallyDecryptionShare] diff --git a/app/api/v1/models/user.py b/app/api/v1/models/user.py deleted file mode 100644 index ead37f6..0000000 --- a/app/api/v1/models/user.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import Any, List, Optional -from enum import Enum - -from pydantic import BaseModel - -from .base import BaseRequest, BaseResponse - -__all__ = [ - "UserScope", - "UserInfo", -] - - -class UserScope(str, Enum): - admin = "admin" - """The admin role can execute administrative functions.""" - auditor = "auditor" - """The auditor role is a readonly role that can observe the election.""" - guardian = "guardian" - """The guardian role can excute guardian functions.""" - voter = "voter" - """ - The voter role can execute voting functions such as encrypt ballot. - The voting endpoints are useful for testing only and are not recommended for production. - """ - - -class UserInfo(BaseModel): - """A specific user in the system""" - - username: str - first_name: str - last_name: str - scopes: List[UserScope] = [] - email: Optional[str] = None - disabled: Optional[bool] = None - - -class CreateUserResponse(BaseResponse): - user_info: UserInfo - password: str - - -class UserQueryRequest(BaseRequest): - """A request for users using the specified filter.""" - - filter: Optional[Any] = None - """ - a json object filter that will be applied to the search. Leave empty to retrieve all users. - """ - - class Config: - schema_extra = {"example": {"filter": {"name": "Jane Doe"}}} - - -class UserQueryResponse(BaseModel): - """Returns a collection of Users.""" - - users: List[UserInfo] diff --git a/app/api/v1/routes.py b/app/api/v1/routes.py deleted file mode 100644 index 3686186..0000000 --- a/app/api/v1/routes.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter -from app.core.settings import ApiMode, Settings -from . import common -from . import guardian -from . import mediator -from . import auth - - -def get_v1_routes(settings: Settings) -> APIRouter: - api_router = APIRouter() - - api_router.include_router(auth.router) - - if settings.API_MODE == ApiMode.GUARDIAN: - api_router.include_router(guardian.router) - elif settings.API_MODE == ApiMode.MEDIATOR: - api_router.include_router(mediator.router) - else: - raise ValueError(f"Unknown API mode: {settings.API_MODE}") - - api_router.include_router(common.router) - - return api_router diff --git a/app/api/v1/tags.py b/app/api/v1/tags.py deleted file mode 100644 index 1d06e3e..0000000 --- a/app/api/v1/tags.py +++ /dev/null @@ -1,14 +0,0 @@ -AUTHORIZE = "Authentication & Authorization" -ELECTION = "Configure Election" -MANIFEST = "Election Manifest" -GUARDIAN = "Guardian" -KEY_CEREMONY = "Key Ceremony" -KEY_CEREMONY_ADMIN = "Key Ceremony Admin" -KEY_GUARDIAN = "Key Ceremony Participant" -BALLOTS = "Query Ballots" -ENCRYPT = "Encrypt Ballots" -TALLY = "Tally Results" -TALLY_DECRYPT = "Tally Decrypt" -PUBLISH = "Publish Results" -UTILITY = "Utility Functions" -USER = "User Information" diff --git a/app/api/v1_1/common/__init__.py b/app/api/v1_1/common/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1_1/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1_1/common/ping.py b/app/api/v1_1/common/ping.py deleted file mode 100644 index 1c1ded7..0000000 --- a/app/api/v1_1/common/ping.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any - -from fastapi import APIRouter - -from ..tags import UTILITY - -router = APIRouter() - - -@router.get("", response_model=str, tags=[UTILITY]) -def ping() -> Any: - """ - Ensure API can be pinged - """ - return "pong" diff --git a/app/api/v1_1/common/routes.py b/app/api/v1_1/common/routes.py deleted file mode 100644 index 87ede48..0000000 --- a/app/api/v1_1/common/routes.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter -from . import ping - -router = APIRouter() - -router.include_router(ping.router, prefix="/ping") diff --git a/app/api/v1_1/guardian/__init__.py b/app/api/v1_1/guardian/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1_1/guardian/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1_1/guardian/ping.py b/app/api/v1_1/guardian/ping.py deleted file mode 100644 index 1c1ded7..0000000 --- a/app/api/v1_1/guardian/ping.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any - -from fastapi import APIRouter - -from ..tags import UTILITY - -router = APIRouter() - - -@router.get("", response_model=str, tags=[UTILITY]) -def ping() -> Any: - """ - Ensure API can be pinged - """ - return "pong" diff --git a/app/api/v1_1/guardian/routes.py b/app/api/v1_1/guardian/routes.py deleted file mode 100644 index 87ede48..0000000 --- a/app/api/v1_1/guardian/routes.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter -from . import ping - -router = APIRouter() - -router.include_router(ping.router, prefix="/ping") diff --git a/app/api/v1_1/mediator/__init__.py b/app/api/v1_1/mediator/__init__.py deleted file mode 100644 index 3a27ef1..0000000 --- a/app/api/v1_1/mediator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .routes import router diff --git a/app/api/v1_1/mediator/election.py b/app/api/v1_1/mediator/election.py deleted file mode 100644 index 4a7cb91..0000000 --- a/app/api/v1_1/mediator/election.py +++ /dev/null @@ -1,27 +0,0 @@ -from fastapi import APIRouter, Body, Request - -from ..models import ( - BaseResponse, - CreateElectionRequest, -) -from ..tags import ELECTION - -router = APIRouter() - - -@router.put("", response_model=BaseResponse, tags=[ELECTION]) -def create_election( - request: Request, - data: CreateElectionRequest = Body(...), -) -> BaseResponse: - """ - 1. Create an election - - Takes only an optional name parameter and returns a surrogate key so that - users can subsequently add a manifest and key to it prior to opening the - election. - """ - - return BaseResponse( - message=f"This endpoint isn't yet implemented, but eventually it will add the '{data.name}' election", - ) diff --git a/app/api/v1_1/mediator/routes.py b/app/api/v1_1/mediator/routes.py deleted file mode 100644 index 63bc775..0000000 --- a/app/api/v1_1/mediator/routes.py +++ /dev/null @@ -1,6 +0,0 @@ -from fastapi import APIRouter -from . import election - -router = APIRouter() - -router.include_router(election.router, prefix="/election") diff --git a/app/api/v1_1/models/__init__.py b/app/api/v1_1/models/__init__.py deleted file mode 100644 index a6c17fb..0000000 --- a/app/api/v1_1/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .election import * -from .base import * diff --git a/app/api/v1_1/models/base.py b/app/api/v1_1/models/base.py deleted file mode 100644 index b662817..0000000 --- a/app/api/v1_1/models/base.py +++ /dev/null @@ -1,61 +0,0 @@ -from typing import Any, Optional -from enum import Enum -from pydantic import BaseModel - -__all__ = [ - "Base", - "BaseRequest", - "BaseResponse", - "BaseQueryRequest", - "BaseValidationRequest", - "BaseValidationResponse", - "ResponseStatus", -] - -Schema = Any - - -class ResponseStatus(str, Enum): - FAIL = "fail" - SUCCESS = "success" - - -class Base(BaseModel): - "A basic model object" - - -class BaseRequest(BaseModel): - """A basic request""" - - -class BaseResponse(BaseModel): - """A basic response""" - - status: ResponseStatus = ResponseStatus.SUCCESS - """The status of the response""" - - message: Optional[str] = None - """An optional message describing the response""" - - def is_success(self) -> bool: - return self.status == ResponseStatus.SUCCESS - - -class BaseQueryRequest(BaseRequest): - """Find something""" - - filter: Optional[Any] = None - - -class BaseValidationRequest(BaseRequest): - """Base validation request""" - - schema_override: Optional[Schema] = None - """Optionally specify a schema to validate against""" - - -class BaseValidationResponse(BaseResponse): - """Response for validating models""" - - details: Optional[str] = None - """Optional details of the validation result""" diff --git a/app/api/v1_1/models/election.py b/app/api/v1_1/models/election.py deleted file mode 100644 index 272c371..0000000 --- a/app/api/v1_1/models/election.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any - -from .base import BaseRequest - - -__all__ = [ - "CreateElectionRequest", -] - -AnyCiphertextElectionContext = Any - - -class CreateElectionRequest(BaseRequest): - """Create an election.""" - - name: str diff --git a/app/api/v1_1/routes.py b/app/api/v1_1/routes.py deleted file mode 100644 index 9eaeda8..0000000 --- a/app/api/v1_1/routes.py +++ /dev/null @@ -1,21 +0,0 @@ -from fastapi import APIRouter -from app.api.v1 import auth -from app.core.settings import ApiMode, Settings -from . import common -from . import mediator -from . import guardian - - -def get_v1_1_routes(settings: Settings) -> APIRouter: - api_router = APIRouter() - - if settings.API_MODE == ApiMode.GUARDIAN: - api_router.include_router(guardian.router) - elif settings.API_MODE == ApiMode.MEDIATOR: - api_router.include_router(mediator.router) - else: - raise ValueError(f"Unknown API mode: {settings.API_MODE}") - - api_router.include_router(common.router) - - return api_router diff --git a/app/api/v1_1/tags.py b/app/api/v1_1/tags.py deleted file mode 100644 index 1d06e3e..0000000 --- a/app/api/v1_1/tags.py +++ /dev/null @@ -1,14 +0,0 @@ -AUTHORIZE = "Authentication & Authorization" -ELECTION = "Configure Election" -MANIFEST = "Election Manifest" -GUARDIAN = "Guardian" -KEY_CEREMONY = "Key Ceremony" -KEY_CEREMONY_ADMIN = "Key Ceremony Admin" -KEY_GUARDIAN = "Key Ceremony Participant" -BALLOTS = "Query Ballots" -ENCRYPT = "Encrypt Ballots" -TALLY = "Tally Results" -TALLY_DECRYPT = "Tally Decrypt" -PUBLISH = "Publish Results" -UTILITY = "Utility Functions" -USER = "User Information" diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index ee6dea0..0000000 --- a/app/core/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .auth import * -from .ballot import * -from .client import * -from .election import * -from .guardian import * -from .key_ceremony import * -from .key_guardian import * -from .manifest import * -from .queue import * -from .repository import * -from .scheduler import * -from .schema import * -from .settings import * -from .tally_decrypt import * -from .tally import * -from .user import * diff --git a/app/core/auth.py b/app/core/auth.py deleted file mode 100644 index 9a1f431..0000000 --- a/app/core/auth.py +++ /dev/null @@ -1,109 +0,0 @@ -import sys -from typing import Any, Union -from fastapi import HTTPException, status - -from passlib.context import CryptContext - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, AuthenticationCredential - -__all__ = [ - "AuthenticationContext", - "get_auth_credential", - "set_auth_credential", - "update_auth_credential", -] - - -class AuthenticationContext: - """ - An Authentication context object wrapper - encapsulating the crypto context used to verify crdentials - """ - - def __init__(self, settings: Settings = Settings()): - self.context = CryptContext(schemes=["bcrypt"], deprecated="auto") - self.settings = settings - - def authenticate_credential(self, username: str, password: str) -> Any: - credential = get_auth_credential(username, self.settings) - return self.verify_password(password, credential.hashed_password) - - def verify_password(self, plain_password: str, hashed_password: str) -> Any: - return self.context.verify(plain_password, hashed_password) - - def get_password_hash(self, password: Union[bytes, str]) -> Any: - return self.context.hash(password) - - -def get_auth_credential( - username: str, settings: Settings = Settings() -) -> AuthenticationCredential: - """Get an authenitcation credential from the repository.""" - try: - with get_repository( - get_client_id(), DataCollection.AUTHENTICATION, settings - ) as repository: - query_result = repository.get({"username": username}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find credential for {username}", - ) - return AuthenticationCredential(**query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{username} not found", - ) from error - - -def set_auth_credential( - credential: AuthenticationCredential, settings: Settings = Settings() -) -> None: - """Set an authentication credential in the repository.""" - try: - with get_repository( - get_client_id(), DataCollection.AUTHENTICATION, settings - ) as repository: - query_result = repository.get({"username": credential.username}) - if not query_result: - repository.set(credential.dict()) - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Already exists {credential.username}", - ) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set auth credential failed", - ) from error - - -def update_auth_credential( - credential: AuthenticationCredential, settings: Settings = Settings() -) -> BaseResponse: - """Update an authentication credential in the repository.""" - try: - with get_repository( - get_client_id(), DataCollection.AUTHENTICATION, settings - ) as repository: - query_result = repository.get({"username": credential.username}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find credential {credential.username}", - ) - repository.update({"username": credential.username}, credential.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update auth credential failed", - ) from error diff --git a/app/core/ballot.py b/app/core/ballot.py deleted file mode 100644 index c4f2071..0000000 --- a/app/core/ballot.py +++ /dev/null @@ -1,132 +0,0 @@ -from typing import Any, List, Optional -import sys -from fastapi import HTTPException, status - -from electionguard.ballot import ( - SubmittedBallot, -) - -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, BallotInventory - - -__all__ = [ - "get_ballot", - "set_ballots", - "filter_ballots", - "get_ballot_inventory", - "upsert_ballot_inventory", -] - - -def get_ballot( - election_id: str, ballot_id: str, settings: Settings = Settings() -) -> SubmittedBallot: - try: - with get_repository( - election_id, DataCollection.SUBMITTED_BALLOT, settings - ) as repository: - query_result = repository.get({"object_id": ballot_id}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find ballot_id {ballot_id}", - ) - return SubmittedBallot.from_json_object(query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{ballot_id} not found", - ) from error - - -def set_ballots( - election_id: str, ballots: List[SubmittedBallot], settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - election_id, DataCollection.SUBMITTED_BALLOT, settings - ) as repository: - cacheable_ballots = [ballot.to_json_object() for ballot in ballots] - _ = repository.set(cacheable_ballots) - return BaseResponse( - message="Ballots Successfully Set", - ) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set ballots failed", - ) from error - - -def filter_ballots( - election_id: str, - filter: Any, - skip: int = 0, - limit: int = 1000, - settings: Settings = Settings(), -) -> List[SubmittedBallot]: - try: - with get_repository( - election_id, DataCollection.SUBMITTED_BALLOT, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - ballots: List[SubmittedBallot] = [] - for item in cursor: - ballots.append(SubmittedBallot.from_json_object(item)) - return ballots - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="provided filter not found", - ) from error - - -def get_ballot_inventory( - election_id: str, settings: Settings = Settings() -) -> Optional[BallotInventory]: - try: - with get_repository( - election_id, DataCollection.BALLOT_INVENTORY, settings - ) as repository: - query_result = repository.get({"election_id": election_id}) - if not query_result: - return None - return BallotInventory( - election_id=query_result["election_id"], - cast_ballot_count=query_result["cast_ballot_count"], - spoiled_ballot_count=query_result["spoiled_ballot_count"], - cast_ballots=query_result["cast_ballots"], - spoiled_ballots=query_result["spoiled_ballots"], - ) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="get ballot inventory failed", - ) from error - - -def upsert_ballot_inventory( - election_id: str, inventory: BallotInventory, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - election_id, DataCollection.BALLOT_INVENTORY, settings - ) as repository: - query_result = repository.get({"election_id": election_id}) - if not query_result: - repository.set(inventory.dict()) - else: - repository.update({"election_id": election_id}, inventory.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update ballot inventory failed", - ) from error diff --git a/app/core/client.py b/app/core/client.py deleted file mode 100644 index c173b1f..0000000 --- a/app/core/client.py +++ /dev/null @@ -1,10 +0,0 @@ -# TODO: multi-tenancy -DEFAULT_CLIENT_ID = "electionguard-default-client-id" - -__all__ = [ - "get_client_id", -] - - -def get_client_id() -> str: - return DEFAULT_CLIENT_ID diff --git a/app/core/election.py b/app/core/election.py deleted file mode 100644 index 807b87a..0000000 --- a/app/core/election.py +++ /dev/null @@ -1,113 +0,0 @@ -import traceback -from typing import Any, List - -from fastapi import HTTPException, status - -from electionguard.serializable import write_json_object - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, Election, ElectionState - -__all__ = [ - "election_from_query", - "get_election", - "set_election", - "filter_elections", - "update_election_state", -] - - -def election_from_query(query_result: Any) -> Election: - return Election( - election_id=query_result["election_id"], - key_name=query_result["key_name"], - state=query_result["state"], - context=query_result["context"], - manifest=query_result["manifest"], - ) - - -def get_election(election_id: str, settings: Settings = Settings()) -> Election: - try: - with get_repository( - get_client_id(), DataCollection.ELECTION, settings - ) as repository: - query_result = repository.get({"election_id": election_id}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find election {election_id}", - ) - election = election_from_query(query_result) - - return election - except Exception as error: - traceback.print_exc() - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{election_id} not found", - ) from error - - -def set_election(election: Election, settings: Settings = Settings()) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.ELECTION, settings - ) as repository: - _ = repository.set(write_json_object(election.dict())) - return BaseResponse( - message="Election Successfully Set", - ) - except Exception as error: - traceback.print_exc() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Submit election failed", - ) from error - - -def filter_elections( - filter: Any, skip: int = 0, limit: int = 1000, settings: Settings = Settings() -) -> List[Election]: - try: - with get_repository( - get_client_id(), DataCollection.ELECTION, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - elections: List[Election] = [] - for item in cursor: - elections.append(election_from_query(item)) - return elections - except Exception as error: - traceback.print_exc() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find elections failed", - ) from error - - -def update_election_state( - election_id: str, new_state: ElectionState, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.ELECTION, settings - ) as repository: - query_result = repository.get({"election_id": election_id}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find election {election_id}", - ) - election = election_from_query(query_result) - election.state = new_state - repository.update({"election_id": election_id}, election.dict()) - return BaseResponse() - except Exception as error: - traceback.print_exc() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update election failed", - ) from error diff --git a/app/core/guardian.py b/app/core/guardian.py deleted file mode 100644 index bf6bc0f..0000000 --- a/app/core/guardian.py +++ /dev/null @@ -1,82 +0,0 @@ -import traceback -from typing import Any -import sys -from fastapi import HTTPException, status - -from electionguard.serializable import write_json_object - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import ( - BaseResponse, - Guardian, -) - -__all__ = [ - "guardian_from_query", - "get_guardian", - "update_guardian", -] - - -def guardian_from_query(query_result: Any) -> Guardian: - return Guardian( - guardian_id=query_result["guardian_id"], - name=query_result["name"], - sequence_order=query_result["sequence_order"], - number_of_guardians=query_result["number_of_guardians"], - quorum=query_result["quorum"], - election_keys=write_json_object(query_result["election_keys"]), - auxiliary_keys=write_json_object(query_result["auxiliary_keys"]), - backups=query_result["backups"], - cohort_public_keys=query_result["cohort_public_keys"], - cohort_backups=query_result["cohort_backups"], - cohort_verifications=query_result["cohort_verifications"], - cohort_challenges=query_result["cohort_challenges"], - ) - - -def get_guardian(guardian_id: str, settings: Settings = Settings()) -> Guardian: - try: - with get_repository( - get_client_id(), DataCollection.GUARDIAN, settings - ) as repository: - query_result = repository.get({"guardian_id": guardian_id}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find guardian {guardian_id}", - ) - guardian = guardian_from_query(query_result) - return guardian - except Exception as error: - traceback.print_exc() - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{guardian_id} not found", - ) from error - - -def update_guardian( - guardian_id: str, guardian: Guardian, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.GUARDIAN, settings - ) as repository: - query_result = repository.get({"guardian_id": guardian_id}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find guardian {guardian_id}", - ) - repository.update({"guardian_id": guardian_id}, guardian.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update guardian failed", - ) from error diff --git a/app/core/key_ceremony.py b/app/core/key_ceremony.py deleted file mode 100644 index e6d859d..0000000 --- a/app/core/key_ceremony.py +++ /dev/null @@ -1,119 +0,0 @@ -from typing import Any -import sys -from fastapi import HTTPException, status - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import ( - BaseResponse, - KeyCeremony, - KeyCeremonyState, - KeyCeremonyGuardianStatus, -) - - -__all__ = [ - "key_ceremony_from_query", - "get_key_ceremony", - "update_key_ceremony", - "update_key_ceremony_state", - "validate_can_publish", -] - - -def key_ceremony_from_query(query_result: Any) -> KeyCeremony: - return KeyCeremony( - key_name=query_result["key_name"], - state=query_result["state"], - number_of_guardians=query_result["number_of_guardians"], - quorum=query_result["quorum"], - guardian_ids=query_result["guardian_ids"], - guardian_status=query_result["guardian_status"], - elgamal_public_key=query_result["elgamal_public_key"], - commitment_hash=query_result["commitment_hash"], - ) - - -def get_key_ceremony(key_name: str, settings: Settings = Settings()) -> KeyCeremony: - try: - with get_repository( - get_client_id(), DataCollection.KEY_CEREMONY, settings - ) as repository: - query_result = repository.get({"key_name": key_name}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find key ceremony {key_name}", - ) - key_ceremony = key_ceremony_from_query(query_result) - return key_ceremony - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{key_name} not found", - ) from error - - -def update_key_ceremony( - key_name: str, ceremony: KeyCeremony, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.KEY_CEREMONY, settings - ) as repository: - query_result = repository.get({"key_name": key_name}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find key ceremony {key_name}", - ) - repository.update({"key_name": key_name}, ceremony.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update key ceremony failed", - ) from error - - -def update_key_ceremony_state( - key_name: str, new_state: KeyCeremonyState, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.KEY_CEREMONY, settings - ) as repository: - query_result = repository.get({"key_name": key_name}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find key ceremony {key_name}", - ) - key_ceremony = key_ceremony_from_query(query_result) - key_ceremony.state = new_state - - repository.update({"key_name": key_name}, key_ceremony.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update key ceremony state failed", - ) from error - - -def validate_can_publish(ceremony: KeyCeremony) -> None: - # TODO: better validation - for guardian_id, state in ceremony.guardian_status.items(): - if ( - state.public_key_shared != KeyCeremonyGuardianStatus.COMPLETE - or state.backups_shared != KeyCeremonyGuardianStatus.COMPLETE - or state.backups_verified != KeyCeremonyGuardianStatus.COMPLETE - ): - raise HTTPException( - status_code=status.HTTP_412_PRECONDITION_FAILED, - detail=f"Publish constraint not satisfied for {guardian_id}", - ) diff --git a/app/core/key_guardian.py b/app/core/key_guardian.py deleted file mode 100644 index 2cf914f..0000000 --- a/app/core/key_guardian.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -from fastapi import HTTPException, status - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import ( - BaseResponse, - KeyCeremonyGuardian, -) - -__all__ = [ - "get_key_guardian", - "update_key_guardian", -] - - -def get_key_guardian( - key_name: str, guardian_id: str, settings: Settings = Settings() -) -> KeyCeremonyGuardian: - try: - with get_repository( - get_client_id(), DataCollection.KEY_GUARDIAN, settings - ) as repository: - query_result = repository.get( - {"key_name": key_name, "guardian_id": guardian_id} - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find guardian {guardian_id}", - ) - guardian = KeyCeremonyGuardian( - key_name=query_result["key_name"], - guardian_id=query_result["guardian_id"], - name=query_result["name"], - sequence_order=query_result["sequence_order"], - number_of_guardians=query_result["number_of_guardians"], - quorum=query_result["quorum"], - public_keys=query_result["public_keys"], - backups=query_result["backups"], - verifications=query_result["verifications"], - challenges=query_result["challenges"], - ) - return guardian - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{key_name} {guardian_id} not found", - ) from error - - -def update_key_guardian( - key_name: str, - guardian_id: str, - guardian: KeyCeremonyGuardian, - settings: Settings = Settings(), -) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.KEY_GUARDIAN, settings - ) as repository: - query_result = repository.get( - {"key_name": key_name, "guardian_id": guardian_id} - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find guardian {guardian_id}", - ) - repository.update({"guardian_id": guardian_id}, guardian.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update key ceremony guardian failed", - ) from error diff --git a/app/core/manifest.py b/app/core/manifest.py deleted file mode 100644 index 5a01075..0000000 --- a/app/core/manifest.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Any, List -import sys - -from fastapi import HTTPException, status - -from electionguard.group import ElementModQ -from electionguard.serializable import read_json_object, write_json_object - -import electionguard.manifest - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import Manifest, ManifestSubmitResponse, ManifestQueryResponse - -__all__ = [ - "from_manifest_query", - "get_manifest", - "set_manifest", - "filter_manifests", -] - -# TODO: rework the caching mechanism to reduce the amount of object conversions -def from_manifest_query(query_result: Any) -> Manifest: - sdk_manifest = electionguard.manifest.Manifest.from_json_object( - query_result["manifest"] - ) - return Manifest( - manifest_hash=write_json_object(sdk_manifest.crypto_hash()), - manifest=write_json_object(sdk_manifest), - ) - - -def get_manifest( - manifest_hash: ElementModQ, settings: Settings = Settings() -) -> Manifest: - try: - with get_repository( - get_client_id(), DataCollection.MANIFEST, settings - ) as repository: - query_result = repository.get({"manifest_hash": manifest_hash.to_hex()}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find manifest {manifest_hash.to_hex()}", - ) - - return from_manifest_query(query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{manifest_hash} not found", - ) from error - - -def set_manifest( - manifest: Manifest, settings: Settings = Settings() -) -> ManifestSubmitResponse: - try: - with get_repository( - get_client_id(), DataCollection.MANIFEST, settings - ) as repository: - manifest_hash = read_json_object( - manifest.manifest_hash, ElementModQ - ).to_hex() - _ = repository.set( - {"manifest_hash": manifest_hash, "manifest": manifest.manifest} - ) - return ManifestSubmitResponse(manifest_hash=manifest_hash) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Submit manifest failed", - ) from error - - -def filter_manifests( - filter: Any, skip: int = 0, limit: int = 1000, settings: Settings = Settings() -) -> ManifestQueryResponse: - try: - with get_repository( - get_client_id(), DataCollection.MANIFEST, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - manifests: List[Manifest] = [] - for item in cursor: - manifests.append(from_manifest_query(item)) - return ManifestQueryResponse(manifests=manifests) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find manifests failed", - ) from error diff --git a/app/core/queue.py b/app/core/queue.py deleted file mode 100644 index fce6eed..0000000 --- a/app/core/queue.py +++ /dev/null @@ -1,101 +0,0 @@ -from typing import Iterable, Any, Protocol -from queue import SimpleQueue - -import sys -import pika - -from .settings import Settings, QueueMode - -__all__ = [ - "IMessageQueue", - "MemoryMessageQueue", - "RabbitMQMessageQueue", - "get_message_queue", -] - - -class IMessageQueue(Protocol): - def __enter__(self) -> Any: - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - pass - - def publish(self, body: str) -> Any: - """ - Get an item from the container - """ - - def subscribe(self) -> Iterable[Any]: - """ - Set and item in the container - """ - - -class MemoryMessageQueue(IMessageQueue): - def __init__(self, queue: str, topic: str): - super().__init__() - self._storage: SimpleQueue = SimpleQueue() - self._queue = queue - self._topic = topic - - def __enter__(self) -> Any: - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - pass - - def publish(self, body: str) -> None: - print("MemoryMessageQueue: publish") - self._storage.put_nowait(body) - - def subscribe(self) -> Iterable[Any]: - while self._storage.qsize() > 0: - item = self._storage.get_nowait() - print("MemoryMessageQueue: subscribe") - yield item - - -class RabbitMQMessageQueue(IMessageQueue): - def __init__(self, uri: str, queue: str, topic: str): - super().__init__() - self._queue = queue - self._topic = topic - self._params = pika.URLParameters(uri) - self._client: pika.BlockingConnection = None - - def __enter__(self) -> Any: - self._client = pika.BlockingConnection(self._params) - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - self._client.close() - - def publish(self, body: str) -> None: - try: - channel = self._client.channel() - channel.queue_declare(queue=self._queue) - channel.basic_publish( - exchange="", - routing_key=self._topic, - body=body, - ) - channel.close() - except (pika.exceptions.ChannelError, pika.exceptions.StreamLostError): - print(sys.exc_info()) - - def subscribe(self) -> Iterable[Any]: - channel = self._client.channel() - data = channel.basic_get(self._topic, True) - while data[0]: - yield data[2] - data = channel.basic_get(self._topic, True) - channel.close() - - -def get_message_queue( - queue: str, topic: str, settings: Settings = Settings() -) -> IMessageQueue: - if settings.QUEUE_MODE == QueueMode.REMOTE: - return RabbitMQMessageQueue(settings.MESSAGEQUEUE_URI, queue, topic) - return MemoryMessageQueue(queue, topic) diff --git a/app/core/repository.py b/app/core/repository.py deleted file mode 100644 index 9070f99..0000000 --- a/app/core/repository.py +++ /dev/null @@ -1,200 +0,0 @@ -from typing import Any, List, Union -from collections.abc import MutableMapping -from abc import ABC, abstractmethod - -import mmap -import os -import json -import re - -from pymongo import MongoClient -from pymongo.database import Database - -from electionguard.hash import hash_elems - -from .settings import Settings, StorageMode - -__all__ = [ - "IRepository", - "LocalRepository", - "MongoRepository", - "get_repository", -] - - -DOCUMENT_VALUE_TYPE = Union[MutableMapping, List[MutableMapping]] - - -class IRepository(ABC): - def __enter__(self) -> Any: - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - pass - - @abstractmethod - def find(self, filter: MutableMapping, skip: int = 0, limit: int = 0) -> Any: - """ - Find items matching the filter - """ - - @abstractmethod - def get(self, filter: MutableMapping) -> Any: - """ - Get an item from the container - """ - - @abstractmethod - def set(self, value: DOCUMENT_VALUE_TYPE) -> Any: - """ - Set and item in the container - """ - - @abstractmethod - def update(self, filter: MutableMapping, value: DOCUMENT_VALUE_TYPE) -> Any: - """ - Update an item - """ - - -class DataCollection: - AUTHENTICATION = "authenticationContext" - GUARDIAN = "guardian" - KEY_GUARDIAN = "keyGuardian" - KEY_CEREMONY = "keyCeremony" - ELECTION = "election" - MANIFEST = "manifest" - BALLOT_INVENTORY = "ballotInventory" - SUBMITTED_BALLOT = "submittedBallots" - CIPHERTEXT_TALLY = "ciphertextTally" - PLAINTEXT_TALLY = "plaintextTally" - DECRYPTION_SHARES = "decryptionShares" - USER_INFO = "userInfo" - - -class LocalRepository(IRepository): - """A simple local storage interface. For testing only.""" - - def __init__( - self, - container: str, - collection: str, - ): - super().__init__() - self._id = 0 - self._container = container - self._collection = collection - self._storage = os.path.join( - os.getcwd(), "storage", self._container, self._collection - ) - - def __enter__(self) -> Any: - if not os.path.exists(self._storage): - os.makedirs(self._storage) - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - pass - - def find(self, filter: MutableMapping, skip: int = 0, limit: int = 0) -> Any: - # TODO: implement a find function - pass - - def get(self, filter: MutableMapping) -> Any: - """An inefficient search through all files in the directory.""" - # query_string = json.dumps(dict(filter)) - query_string = re.sub(r"\{|\}", r"", json.dumps(dict(filter))) - - search_files = [ - file - for file in os.listdir(self._storage) - if os.path.isfile(os.path.join(self._storage, file)) - ] - - for filename in search_files: - try: - with open( - os.path.join(self._storage, filename), encoding="utf-8" - ) as file, mmap.mmap( - file.fileno(), 0, access=mmap.ACCESS_READ - ) as search: - if search.find(bytes(query_string, "utf-8")) != -1: - json_string = file.read() - return json.loads(json_string) - except FileNotFoundError: - # swallow errors - pass - return None - - def set(self, value: DOCUMENT_VALUE_TYPE) -> Any: - """A naive set function that hashes the data and writes the file.""" - # just ignore lists for now - if isinstance(value, List): - raise Exception("Not Implemented") - json_string = json.dumps(dict(value)) - filename = hash_elems(json_string).to_hex() - with open( - f"{os.path.join(self._storage, filename)}.json", "w", encoding="utf-8" - ) as file: - file.write(json_string) - return filename - - def update(self, filter: MutableMapping, value: DOCUMENT_VALUE_TYPE) -> Any: - # TODO: implement an update function - pass - - -class MongoRepository(IRepository): - def __init__( - self, - uri: str, - container: str, - collection: str, - ): - super().__init__() - self._uri = uri - self._container = container - self._collection = collection - self._client: MongoClient = None - self._database: Database = None - - def __enter__(self) -> Any: - self._client = MongoClient(self._uri) - self._database = self._client.get_database(self._container) - return self - - def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: - self._client.close() - - def find(self, filter: MutableMapping, skip: int = 0, limit: int = 0) -> Any: - collection = self._database.get_collection(self._collection) - return collection.find(filter=filter, skip=skip, limit=limit) - - def get(self, filter: MutableMapping) -> Any: - collection = self._database.get_collection(self._collection) - return collection.find_one(filter) - - def set(self, value: DOCUMENT_VALUE_TYPE) -> Any: - collection = self._database.get_collection(self._collection) - if isinstance(value, List): - result = collection.insert_many(value) - return [str(id) for id in result.inserted_ids] - result = collection.insert_one(value) - return [str(result.inserted_id)] - - def update(self, filter: MutableMapping, value: DOCUMENT_VALUE_TYPE) -> Any: - collection = self._database.get_collection(self._collection) - return collection.update_one(filter=filter, update={"$set": value}) - - -def get_repository( - container: str, collection: str, settings: Settings = Settings() -) -> IRepository: - """Get a repository by settings strage mode.""" - if settings.STORAGE_MODE == StorageMode.MONGO: - return MongoRepository(settings.MONGODB_URI, container, collection) - - if settings.STORAGE_MODE == StorageMode.LOCAL_STORAGE: - return LocalRepository(container, collection) - - raise ValueError("Unsupported storage mode: " + settings.STORAGE_MODE) diff --git a/app/core/scheduler.py b/app/core/scheduler.py deleted file mode 100644 index e9cce1c..0000000 --- a/app/core/scheduler.py +++ /dev/null @@ -1,9 +0,0 @@ -from functools import lru_cache -from electionguard.scheduler import Scheduler - -__all__ = ["get_scheduler"] - - -@lru_cache -def get_scheduler() -> Scheduler: - return Scheduler() diff --git a/app/core/schema.py b/app/core/schema.py deleted file mode 100644 index 26e1f3d..0000000 --- a/app/core/schema.py +++ /dev/null @@ -1,10 +0,0 @@ -from functools import lru_cache -from typing import Any -from electionguard.schema import get_election_description_schema - -__all__ = ["get_description_schema"] - - -@lru_cache -def get_description_schema() -> Any: - return get_election_description_schema() diff --git a/app/core/settings.py b/app/core/settings.py deleted file mode 100644 index 210ce9e..0000000 --- a/app/core/settings.py +++ /dev/null @@ -1,59 +0,0 @@ -from typing import List -from enum import Enum -from pydantic import AnyHttpUrl, BaseSettings -from pydantic.fields import Field - -__all__ = [ - "ApiMode", - "QueueMode", - "StorageMode", - "Settings", -] - - -class ApiMode(str, Enum): - GUARDIAN = "guardian" - MEDIATOR = "mediator" - - -class QueueMode(str, Enum): - LOCAL = "local" - REMOTE = "remote" - - -class StorageMode(str, Enum): - LOCAL_STORAGE = "local_storage" - MONGO = "mongo" - - -# pylint:disable=too-few-public-methods -class Settings(BaseSettings): - API_MODE: ApiMode = ApiMode.MEDIATOR - QUEUE_MODE: QueueMode = QueueMode.LOCAL - STORAGE_MODE: StorageMode = StorageMode.LOCAL_STORAGE - - API_V1_STR: str = "/api/v1" - API_V1_1_STR: str = "/api/v1_1" - BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = Field( - default=[ - "http://localhost", - "http://localhost:8080", - "http://localhost:3001", - "https://localhost", - ] - ) - PROJECT_NAME: str = "electionguard-api-python" - MONGODB_URI: str = "mongodb://root:example@localhost:27017" - MESSAGEQUEUE_URI: str = "amqp://guest:guest@localhost:5672" - - AUTH_ALGORITHM = "HS256" - # JWT secret that matches AUTH_ALGORITHM. Change this to a valid value. - AUTH_SECRET_KEY = "" - AUTH_ACCESS_TOKEN_EXPIRE_MINUTES = 30 - DEFAULT_ADMIN_USERNAME = "default" - DEFAULT_ADMIN_PASSWORD = "" - # this is a default value that will be moving to the environment settings - # the default value should not be used for production use - - class Config: - case_sensitive = True diff --git a/app/core/tally.py b/app/core/tally.py deleted file mode 100644 index c352709..0000000 --- a/app/core/tally.py +++ /dev/null @@ -1,196 +0,0 @@ -from typing import Any, List -import sys -from fastapi import HTTPException, status - -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, CiphertextTally, PlaintextTally - -__all__ = [ - "ciphertext_tally_from_query", - "plaintext_tally_from_query", - "get_ciphertext_tally", - "set_ciphertext_tally", - "filter_ciphertext_tallies", - "get_plaintext_tally", - "set_plaintext_tally", - "update_plaintext_tally", - "filter_plaintext_tallies", -] - - -def ciphertext_tally_from_query(query_result: Any) -> CiphertextTally: - return CiphertextTally( - election_id=query_result["election_id"], - tally_name=query_result["tally_name"], - created=query_result["created"], - tally=query_result["tally"], - ) - - -def plaintext_tally_from_query(query_result: Any) -> PlaintextTally: - return PlaintextTally( - election_id=query_result["election_id"], - tally_name=query_result["tally_name"], - created=query_result["created"], - state=query_result["state"], - tally=query_result["tally"], - ) - - -def get_ciphertext_tally( - election_id: str, tally_name: str, settings: Settings = Settings() -) -> CiphertextTally: - try: - with get_repository( - election_id, DataCollection.CIPHERTEXT_TALLY, settings - ) as repository: - query_result = repository.get( - {"election_id": election_id, "tally_name": tally_name} - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find tally {election_id} {tally_name}", - ) - return ciphertext_tally_from_query(query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{election_id} {tally_name} not found", - ) from error - - -def set_ciphertext_tally( - tally: CiphertextTally, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - tally.election_id, DataCollection.CIPHERTEXT_TALLY, settings - ) as repository: - repository.set(tally.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set ciphertext tally failed", - ) from error - - -def filter_ciphertext_tallies( - election_id: str, - filter: Any, - skip: int = 0, - limit: int = 1000, - settings: Settings = Settings(), -) -> List[CiphertextTally]: - try: - with get_repository( - election_id, DataCollection.CIPHERTEXT_TALLY, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - tallies: List[CiphertextTally] = [] - for item in cursor: - tallies.append(ciphertext_tally_from_query(item)) - return tallies - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="filter ciphertext tallies failed", - ) from error - - -def get_plaintext_tally( - election_id: str, tally_name: str, settings: Settings = Settings() -) -> PlaintextTally: - try: - with get_repository( - election_id, DataCollection.PLAINTEXT_TALLY, settings - ) as repository: - query_result = repository.get( - {"election_id": election_id, "tally_name": tally_name} - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find tally {election_id} {tally_name}", - ) - return plaintext_tally_from_query(query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="get plaintext tally failed", - ) from error - - -def set_plaintext_tally( - tally: PlaintextTally, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - tally.election_id, DataCollection.PLAINTEXT_TALLY, settings - ) as repository: - repository.set(tally.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set plaintext tally failed", - ) from error - - -def update_plaintext_tally( - tally: PlaintextTally, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - tally.election_id, DataCollection.PLAINTEXT_TALLY, settings - ) as repository: - query_result = repository.get( - {"election_id": tally.election_id, "tally_name": tally.tally_name} - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find plaintext tally {tally.election_id} {tally.tally_name}", - ) - repository.update( - {"election_id": tally.election_id, "tally_name": tally.tally_name}, - tally.dict(), - ) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update plaintext tally failed", - ) from error - - -def filter_plaintext_tallies( - election_id: str, - filter: Any, - skip: int = 0, - limit: int = 1000, - settings: Settings = Settings(), -) -> List[PlaintextTally]: - try: - with get_repository( - election_id, DataCollection.PLAINTEXT_TALLY, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - tallies: List[PlaintextTally] = [] - for item in cursor: - tallies.append(plaintext_tally_from_query(item)) - return tallies - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="filter plaintext tallies failed", - ) from error diff --git a/app/core/tally_decrypt.py b/app/core/tally_decrypt.py deleted file mode 100644 index 872c82c..0000000 --- a/app/core/tally_decrypt.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Any, List -import sys -from fastapi import HTTPException, status - -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, CiphertextTallyDecryptionShare - -__all__ = [ - "from_tally_decryption_share_query", - "get_decryption_share", - "set_decryption_share", - "filter_decryption_shares", -] - - -def from_tally_decryption_share_query( - query_result: Any, -) -> CiphertextTallyDecryptionShare: - return CiphertextTallyDecryptionShare( - election_id=query_result["election_id"], - tally_name=query_result["tally_name"], - guardian_id=query_result["guardian_id"], - tally_share=query_result["tally_share"], - ballot_shares=query_result["ballot_shares"], - ) - - -def get_decryption_share( - election_id: str, tally_name: str, guardian_id: str, settings: Settings = Settings() -) -> CiphertextTallyDecryptionShare: - try: - with get_repository( - tally_name, DataCollection.DECRYPTION_SHARES, settings - ) as repository: - query_result = repository.get( - { - "election_id": election_id, - "tally_name": tally_name, - "guardian_id": guardian_id, - } - ) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find decryption share {election_id} {tally_name} {guardian_id}", - ) - return from_tally_decryption_share_query(query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{election_id} {tally_name} {guardian_id} not found", - ) from error - - -def set_decryption_share( - decryption_share: CiphertextTallyDecryptionShare, settings: Settings = Settings() -) -> BaseResponse: - try: - with get_repository( - decryption_share.tally_name, DataCollection.DECRYPTION_SHARES, settings - ) as repository: - repository.set(decryption_share.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set decryption share failed", - ) from error - - -def filter_decryption_shares( - tally_name: str, - filter: Any, - skip: int = 0, - limit: int = 1000, - settings: Settings = Settings(), -) -> List[CiphertextTallyDecryptionShare]: - try: - with get_repository( - tally_name, DataCollection.DECRYPTION_SHARES, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - decryption_shares: List[CiphertextTallyDecryptionShare] = [] - for item in cursor: - decryption_shares.append(from_tally_decryption_share_query(item)) - return decryption_shares - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="find decryption shares failed", - ) from error diff --git a/app/core/user.py b/app/core/user.py deleted file mode 100644 index 87f0006..0000000 --- a/app/core/user.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import Any, List -import sys -from fastapi import HTTPException, status - -from .client import get_client_id -from .repository import get_repository, DataCollection -from .settings import Settings -from ..api.v1.models import BaseResponse, UserInfo - -__all__ = ["get_user_info", "filter_user_info", "set_user_info", "update_user_info"] - - -def get_user_info(username: str, settings: Settings = Settings()) -> UserInfo: - try: - with get_repository( - get_client_id(), DataCollection.USER_INFO, settings - ) as repository: - query_result = repository.get({"username": username}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find user {username}", - ) - return UserInfo(**query_result) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"{username} not found", - ) from error - - -def filter_user_info( - filter: Any, skip: int = 0, limit: int = 1000, settings: Settings = Settings() -) -> List[UserInfo]: - try: - with get_repository( - get_client_id(), DataCollection.USER_INFO, settings - ) as repository: - cursor = repository.find(filter, skip, limit) - results: List[UserInfo] = [] - for item in cursor: - results.append(item) - return results - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="filter users failed", - ) from error - - -def set_user_info(user: UserInfo, settings: Settings = Settings()) -> None: - try: - with get_repository( - get_client_id(), DataCollection.USER_INFO, settings - ) as repository: - query_result = repository.get({"username": user.username}) - if not query_result: - repository.set(user.dict()) - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Already exists {user.username}", - ) - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="set user info failed", - ) from error - - -def update_user_info(user: UserInfo, settings: Settings = Settings()) -> BaseResponse: - try: - with get_repository( - get_client_id(), DataCollection.GUARDIAN, settings - ) as repository: - query_result = repository.get({"username": user.username}) - if not query_result: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Could not find user {user.username}", - ) - repository.update({"username": user.username}, user.dict()) - return BaseResponse() - except Exception as error: - print(sys.exc_info()) - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="update user info failed", - ) from error diff --git a/app/data/manifest.json b/app/data/manifest.json deleted file mode 100644 index c3d0fc3..0000000 --- a/app/data/manifest.json +++ /dev/null @@ -1,723 +0,0 @@ -{ - "manifest": { - "ballot_styles": [ - { - "geopolitical_unit_ids": ["hamilton-county", "congress-district-7"], - "object_id": "congress-district-7-hamilton-county" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-7", - "lacroix-township-precinct-1" - ], - "object_id": "congress-district-7-lacroix" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-7", - "lacroix-township-precinct-1", - "lacroix-exeter-utility-district" - ], - "object_id": "congress-district-7-lacroix-exeter" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-7", - "arlington-township-precinct-1" - ], - "object_id": "congress-district-7-arlington" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-7", - "arlington-township-precinct-1", - "pismo-beach-school-district-precinct-1" - ], - "object_id": "congress-district-7-arlington-pismo-beach" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-7", - "arlington-township-precinct-1", - "somerset-school-district-precinct-1" - ], - "object_id": "congress-district-7-arlington-somerset" - }, - { - "geopolitical_unit_ids": ["hamilton-county", "congress-district-5"], - "object_id": "congress-district-5-hamilton-county" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-5", - "lacroix-township-precinct-1" - ], - "object_id": "congress-district-5-lacroix" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-5", - "harris-township" - ], - "object_id": "congress-district-5-harris" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-5", - "arlington-township-precinct-1", - "pismo-beach-school-district-precinct-1" - ], - "object_id": "congress-district-5-arlington-pismo-beach" - }, - { - "geopolitical_unit_ids": [ - "hamilton-county", - "congress-district-5", - "arlington-township-precinct-1", - "somerset-school-district-precinct-1" - ], - "object_id": "congress-district-5-arlington-somerset" - } - ], - "candidates": [ - { - "name": { - "text": [ - { "language": "en", "value": "Joseph Barchi and Joseph Hallaren" } - ] - }, - "object_id": "barchi-hallaren", - "party_id": "whig" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Adam Cramer and Greg Vuocolo" } - ] - }, - "object_id": "cramer-vuocolo", - "party_id": "federalist" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Daniel Court and Amy Blumhardt" } - ] - }, - "object_id": "court-blumhardt", - "party_id": "peoples" - }, - { - "name": { - "text": [{ "language": "en", "value": "Alvin Boone and James Lian" }] - }, - "object_id": "boone-lian", - "party_id": "liberty" - }, - { - "name": { - "text": [ - { - "language": "en", - "value": "Ashley Hildebrand-McDougall and James Garritty" - } - ] - }, - "object_id": "hildebrand-garritty", - "party_id": "constitution" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Martin Patterson and Clay Lariviere" } - ] - }, - "object_id": "patterson-lariviere", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Charlene Franz" }] }, - "object_id": "franz", - "party_id": "federalist" - }, - { - "name": { "text": [{ "language": "en", "value": "Gerald Harris" }] }, - "object_id": "harris", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Linda Bargmann" }] }, - "object_id": "bargmann", - "party_id": "constitution" - }, - { - "name": { "text": [{ "language": "en", "value": "Barbara Abcock" }] }, - "object_id": "abcock", - "party_id": "liberty" - }, - { - "name": { "text": [{ "language": "en", "value": "Carrie Steel-Loy" }] }, - "object_id": "steel-loy", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Frederick Sharp" }] }, - "object_id": "sharp", - "party_id": "constitution" - }, - { - "name": { "text": [{ "language": "en", "value": "Alex Wallace" }] }, - "object_id": "wallace", - "party_id": "independent" - }, - { - "name": { "text": [{ "language": "en", "value": "Barbara Williams" }] }, - "object_id": "williams", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Althea Sharp" }] }, - "object_id": "sharp-althea", - "party_id": "whig" - }, - { - "name": { "text": [{ "language": "en", "value": "Douglas Alpern" }] }, - "object_id": "alpern", - "party_id": "federalist" - }, - { - "name": { "text": [{ "language": "en", "value": "Ann Windbeck" }] }, - "object_id": "windbeck", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Mike Greher" }] }, - "object_id": "greher", - "party_id": "constitution" - }, - { - "name": { - "text": [{ "language": "en", "value": "Patricia Alexander" }] - }, - "object_id": "alexander", - "party_id": "whig" - }, - { - "name": { "text": [{ "language": "en", "value": "Kenneth Mitchell" }] }, - "object_id": "mitchell", - "party_id": "federalist" - }, - { - "name": { "text": [{ "language": "en", "value": "Stan Lee" }] }, - "object_id": "lee", - "party_id": "independent" - }, - { - "name": { "text": [{ "language": "en", "value": "Henry Ash" }] }, - "object_id": "ash", - "party_id": "liberty" - }, - { - "name": { "text": [{ "language": "en", "value": "Karen Kennedy" }] }, - "object_id": "kennedy", - "party_id": "independent" - }, - { - "name": { "text": [{ "language": "en", "value": "Van Jackson" }] }, - "object_id": "jackson", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Debbie Brown" }] }, - "object_id": "brown", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Joseph Teller" }] }, - "object_id": "teller", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Greg Ward" }] }, - "object_id": "ward", - "party_id": "independent" - }, - { - "name": { "text": [{ "language": "en", "value": "Lou Murphy" }] }, - "object_id": "murphy", - "party_id": "federalist" - }, - { - "name": { "text": [{ "language": "en", "value": "Jane Newman" }] }, - "object_id": "newman", - "party_id": "whig" - }, - { - "name": { "text": [{ "language": "en", "value": "Jack Callanann" }] }, - "object_id": "callanann", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Esther York" }] }, - "object_id": "york", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Glenn Chandler" }] }, - "object_id": "chandler", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Andrea Solis" }] }, - "object_id": "solis", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Amos Keller" }] }, - "object_id": "keller", - "party_id": "constitution" - }, - { - "name": { "text": [{ "language": "en", "value": "Davitra Rangel" }] }, - "object_id": "rangel", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Camille Argent" }] }, - "object_id": "argent", - "party_id": "liberty" - }, - { - "name": { - "text": [{ "language": "en", "value": "Chloe Witherspoon-Smithson" }] - }, - "object_id": "witherspoon-smithson", - "party_id": "independent" - }, - { - "name": { - "text": [{ "language": "en", "value": "Clayton Bainbridge" }] - }, - "object_id": "bainbridge", - "party_id": "peoples" - }, - { - "name": { - "text": [{ "language": "en", "value": "Charlene Hennessey" }] - }, - "object_id": "hennessey", - "party_id": "whig" - }, - { - "name": { "text": [{ "language": "en", "value": "Eric Savoy" }] }, - "object_id": "savoy", - "party_id": "labor" - }, - { - "name": { "text": [{ "language": "en", "value": "Susan Tawa" }] }, - "object_id": "tawa", - "party_id": "constitution" - }, - { - "name": { "text": [{ "language": "en", "value": "Mary Tawa" }] }, - "object_id": "tawa-mary", - "party_id": "independent" - }, - { - "name": { "text": [{ "language": "en", "value": "Valarie Altman" }] }, - "object_id": "altman", - "party_id": "peoples" - }, - { - "name": { "text": [{ "language": "en", "value": "Helen Moore" }] }, - "object_id": "moore" - }, - { - "name": { "text": [{ "language": "en", "value": "John White" }] }, - "object_id": "white" - }, - { - "name": { - "text": [{ "language": "en", "value": "John Smallberries" }] - }, - "object_id": "smallberries" - }, - { - "name": { "text": [{ "language": "en", "value": "John Warfin" }] }, - "object_id": "warfin" - }, - { - "name": { "text": [{ "language": "en", "value": "Chris Norberg" }] }, - "object_id": "norberg" - }, - { - "name": { "text": [{ "language": "en", "value": "Abigail Parks" }] }, - "object_id": "parks" - }, - { - "name": { "text": [{ "language": "en", "value": "Harmony Savannah" }] }, - "object_id": "savannah" - }, - { - "name": { "text": [{ "language": "en", "value": "Buffy Summers" }] }, - "object_id": "summers" - }, - { - "name": { "text": [{ "language": "en", "value": "Cordelia Chase" }] }, - "object_id": "chase" - }, - { - "name": { "text": [{ "language": "en", "value": "Daniel Osborne" }] }, - "object_id": "osborne" - }, - { - "name": { "text": [{ "language": "en", "value": "Willow Rosenberg" }] }, - "object_id": "rosenberg" - }, - { - "name": { - "text": [{ "language": "en", "value": "Anthony Stewart Head" }] - }, - "object_id": "head" - }, - { - "name": { "text": [{ "language": "en", "value": "James Marsters" }] }, - "object_id": "marsters" - }, - { - "isWriteIn": true, - "name": { - "text": [ - { "language": "en", "value": "Write In Candidate" }, - { "language": "es", "value": "Escribir en la candidata" } - ] - }, - "object_id": "write-in-1" - }, - { - "isWriteIn": true, - "name": { - "text": [ - { "language": "en", "value": "Write In Candidate" }, - { "language": "es", "value": "Escribir en la candidata" } - ] - }, - "object_id": "write-in-2" - }, - { - "isWriteIn": true, - "name": { - "text": [ - { "language": "en", "value": "Write In Candidate" }, - { "language": "es", "value": "Escribir en la candidata" } - ] - }, - "object_id": "write-in-3" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Retain" }, - { "language": "es", "value": "Conservar" } - ] - }, - "object_id": "ozark-chief-justice-retain-demergue-affirmative" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Reject" }, - { "language": "es", "value": "Rechazar" } - ] - }, - "object_id": "ozark-chief-justice-retain-demergue-negative" - }, - { - "name": { "text": [{ "language": "en", "value": "Yes" }] }, - "object_id": "exeter-utility-district-referendum-affirmative" - }, - { - "name": { "text": [{ "language": "en", "value": "No" }] }, - "object_id": "exeter-utility-district-referendum-negative" - } - ], - "contact_information": { - "address_line": ["1234 Paul Revere Run", "Hamilton, Ozark 99999"], - "email": [ - { "annotation": "press", "value": "inquiries@hamilton.state.gov" }, - { "annotation": "federal", "value": "commissioner@hamilton.state.gov" } - ], - "name": "Hamilton State Election Commission", - "phone": [ - { "annotation": "domestic", "value": "123-456-7890" }, - { "annotation": "international", "value": "+1-123-456-7890" } - ] - }, - "contests": [ - { - "@type": "CandidateContest", - "ballot_selections": [ - { - "candidate_id": "barchi-hallaren", - "object_id": "barchi-hallaren-selection", - "sequence_order": 0 - }, - { - "candidate_id": "cramer-vuocolo", - "object_id": "cramer-vuocolo-selection", - "sequence_order": 1 - } - ], - "ballot_subtitle": { - "text": [ - { "language": "en", "value": "Vote for one" }, - { "language": "es", "value": "Votar por uno" } - ] - }, - "ballot_title": { - "text": [ - { - "language": "en", - "value": "President and Vice President of the United States" - }, - { - "language": "es", - "value": "Presidente y Vicepresidente de los Estados Unidos" - } - ] - }, - "electoral_district_id": "hamilton-county", - "name": "President and Vice President of the United States", - "number_elected": 1, - "object_id": "president-vice-president-contest", - "sequence_order": 0, - "vote_variation": "one_of_m", - "votes_allowed": 1 - } - ], - "election_scope_id": "hamilton-county-general-election", - "end_date": "2020-03-01T20:00:00-05:00", - "geopolitical_units": [ - { - "contact_information": { - "address_line": ["1234 Samuel Adams Way", "Hamilton, Ozark 99999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@hamiltoncounty.gov" - } - ], - "name": "Hamilton County Clerk", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Hamilton County", - "object_id": "hamilton-county", - "type": "county" - }, - { - "contact_information": { - "address_line": ["1234 Somerville Gateway", "Medford, Ozark 999999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@congressional-district-5.gov" - } - ], - "name": "Medford Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Congressional District 5", - "object_id": "congress-district-5", - "type": "congressional" - }, - { - "contact_information": { - "address_line": [ - "1234 Somerville Gateway", - "Arlington, Ozark 999999" - ], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@congressional-district-7.gov" - } - ], - "name": "Arlington Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Congressional District 7", - "object_id": "congress-district-7", - "type": "congressional" - }, - { - "contact_information": { - "address_line": ["1234 Thorton Drive", "LaCroix, Ozark 99999"], - "email": [ - { "annotation": "inquiries", "value": "inquiries@lacrox.gov" } - ], - "name": "LaCroix Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "LaCroix Township Precinct 1", - "object_id": "lacroix-township-precinct-1", - "type": "precinct" - }, - { - "contact_information": { - "address_line": ["1234 Watt Drive", "LaCroix, Ozark 99999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@exeter-utility.com" - } - ], - "name": "Exeter Utility District coordinator", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Exeter Utility District", - "object_id": "lacroix-exeter-utility-district", - "type": "utility" - }, - { - "contact_information": { - "address_line": ["1234 Pahk Avenue", "Arlinton, Ozark 99999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@arlington-township.gov" - } - ], - "name": "Arlington Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Arlington Township Precinct 1", - "object_id": "arlington-township-precinct-1", - "type": "precinct" - }, - { - "contact_information": { - "address_line": [ - "1234 Pismo Beach Elementary", - "Arlington, Ozark 99999" - ], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@pismo-beach-school.edu" - } - ], - "name": "Pismo Beah Elementary", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Pismo Beach School District Precinct 1", - "object_id": "pismo-beach-school-district-precinct-1", - "type": "school" - }, - { - "contact_information": { - "address_line": ["1234 Somerset Avenue", "Arlinton, Ozark 99999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@somerset-elementary.edu" - } - ], - "name": "Someset Elementary", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Somerset School District", - "object_id": "somerset-school-district-precinct-1", - "type": "school" - }, - { - "contact_information": { - "address_line": ["1234 Pahk Avenue", "Harris, Ozark 99999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@harris-township.gov" - } - ], - "name": "harris Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Harris Township", - "object_id": "harris-township", - "type": "township" - } - ], - "name": { - "text": [ - { "language": "en", "value": "Hamiltion County General Election" }, - { - "language": "es", - "value": "Elecci\u00f3n general del condado de Hamilton" - } - ] - }, - "parties": [ - { - "abbreviation": "WHI", - "color": "AAAAAA", - "logo_uri": "http://some/path/to/whig.svg", - "name": { "text": [{ "language": "en", "value": "Whig Party" }] }, - "object_id": "whig" - }, - { - "abbreviation": "FED", - "color": "BBBBBB", - "logo_uri": "http://some/path/to/federalist.svg", - "name": { "text": [{ "language": "en", "value": "Federalist Party" }] }, - "object_id": "federalist" - }, - { - "abbreviation": "PPL", - "color": "CCCCCC", - "logo_uri": "http://some/path/to/people-s.svg", - "name": { "text": [{ "language": "en", "value": "People's Party" }] }, - "object_id": "peoples" - }, - { - "abbreviation": "LIB", - "color": "DDDDDD", - "logo_uri": "http://some/path/to/liberty.svg", - "name": { "text": [{ "language": "en", "value": "Liberty Party" }] }, - "object_id": "liberty" - }, - { - "abbreviation": "CONST", - "color": "EEEEEE", - "logo_uri": "http://some/path/to/democratic-repulbican.svg", - "name": { - "text": [{ "language": "en", "value": "Constitution Party" }] - }, - "object_id": "constitution" - }, - { - "abbreviation": "LBR", - "color": "FFFFFF", - "logo_uri": "http://some/path/to/laobr.svg", - "name": { "text": [{ "language": "en", "value": "Labor Party" }] }, - "object_id": "labor" - }, - { - "abbreviation": "IND", - "color": "000000", - "logo_uri": "http://some/path/to/independent.svg", - "name": { "text": [{ "language": "en", "value": "Independent" }] }, - "object_id": "independent" - } - ], - "spec_version": "v0.95", - "start_date": "2020-03-01T08:00:00-05:00", - "type": "general" - } -} diff --git a/app/data/old-ballot.json b/app/data/old-ballot.json deleted file mode 100644 index 8d93a82..0000000 --- a/app/data/old-ballot.json +++ /dev/null @@ -1,564 +0,0 @@ -{ - "election_id": "something", - "ballots": [ - { - "code": "75786406114188765288382671780488611935956608564265436765153969479308926288023", - "code_seed": "59542979655380966329941733579016477223097246937984905491087018180972021095237", - "contests": [ - { - "ballot_selections": [ - { - "ciphertext": { - "pad": "858822319301832240537807990992028813596134854090692474833207903297332639243551286928941479623019997727791619756319223970781762445143162579365145810085025446195625322324753254934920228692444504609019538977979618285106851811035211033391342082106016350390592059899500896666790560088755787619167328804967607049738567869909496132578532316916231302723303846065681130977526721113956019524805052623285603864493945743236139094407677053720972770443660834905053205117165204349183478477110048226724161454470152935222190553090013103138320790341139012006724879191490082185323063904243129487038143260563325268946151089925985656614075481786882419073364535598789646947446356472064808428547995176146317368053715065589835434466475646323648822163037185398701648087007823900314631835954413800400291120485082980502016569770228649833221052593927836805747662400526837753527241610599276183422536383200634310033848001158818831532844581720487170733948387258686234411303573855746550945432840296802197834164322388185070156163599359507923953695781991131490513388836806209384652095446569930363365790850119221375689711816151308615549285586321526516633535287780700472570129910119411415369950001315160187237537366091234172631143525934855415695765960677459708091954859", - "data": "1014260504151714097055532137920080261224733917279197172920762425180740610465409429236593092403891529402427527395312790987164564704397327453443640295569025016002971467025312494862968590468643049063710357757413407434475465967351106402069356367514341758956158671871910168472603677440669908638904399184241343738152740982318329083304158962806092379996168030916003069862997415237025016939874759494671825113420988148157121256280949153715249218148883092626233731153428855672346643223113374396752881941867792118805792902384892118685886044246395349738026142559629831222482266807906014735370404156166469928760666265352529785961498738010586528633989100402684433961363314584450457071198917006145389170274337481464984575644347225407558669844253768389262768660151684706925073096546555958515415304153138629657604796654769820501316334193259706756564687355188102485184883718915010712388465389500431232442771391527393363118552928739654135053508451288751592689967797010355615829283785705207475379538018895422414873143502193247597840219405789513194586825342027069774350513546612181270454936716309969690254509657810458963222266726346950346735916422166078114302991004047529146637739454580968355278155696683299649315214118655439721049942136804478931913455281" - }, - "crypto_hash": "42143546487074276437134844314558426991829230954562224840567327083358240167704", - "description_hash": "19418802242166470125540776082701171293614262321550109573739651076961712265028", - "is_placeholder_selection": false, - "object_id": "john-adams-selection", - "proof": { - "challenge": "323931395953424955339953098763071014142484420716814972921653831744191710594", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "18838022341963510487967623410614684896316316069311288387595559813859285706165", - "proof_one_data": "655638203718658280497446128423115112700920524084657635748568649742633928211155858599719975658896675113501831895376175809492144696172372729125653454034823030794911402173879413439894963826908005897340207380802570573316909564990758924631203568514091629118818881632065317131582067196171914283574990538562595194115817825431233263024416065628540105117741269499552551500765472584252033308485734290781401014874547396080457552253566429890602337621133286768087058285668154290641877543062850280112942919807943371709746152325009914447672868468463586060026819217576263482619312333151431366481438582510426803027570970005745773399950514198031089581738402695455427774582582170167345145450776993780283973749526726463533652909917191113564710082449579645402768039320281974923346853725352116345531318794030844899920160372950684057508834210852440430834304426147625340129152203476879650364034442000237459756631508136894875592471410165447058785189015240822306908719147643544949499978679291684163076717689005934476315349013770511485601900770854287600208902250814454150244190190711016584253721904175201547214213525031713533927760768924166947448620147453450172629126445305676656471047068928168192862625366145341513966188265273031565846550597221703069866311432", - "proof_one_pad": "297258093029896205550222122059498503410896248973137402240776474677759494812719700731163153213142433778555412730291570932375057677969187381185438521165286368202837817546697887047225009959078008999478279028820653241890511302122696071238631619651001795436445033765124339071745779576969770090736822389702516930135965586042791319554011497053639385975273011812519777962511939121419065516277309610084774519474839291464213195851528533212216729215598162999760776886474612531493072584695465511346034242992088054667293130078920966751733532723764958388254571870569813876488200756146015978957213174356594122674331455324584905320056715728658319149570229837046846876636095057524575922137473405675635437223741934984018139912449223026503566469068282924521634172921928904922337863965170328597675697874488658785523328730082721445576027601829529852690941800695124010310521113935115561035210973340007232629635351409984349168244760695377991726496885256533782324746043013959832619721407596463333401065024745854487260381474790428117633524850987802981552644526012149684717471592234670600501331566535177672868604230545935595519021712828786684719655712028606999744500967529562941870468946988363212235401564206289030995993464790585193265712238356493197955155233", - "proof_one_response": "98128416087950720873201740162084646686765607410795817652523002057425812932932", - "proof_zero_challenge": "97277998291306109890943314696836293971096153017046090624783678025798035644176", - "proof_zero_data": "56804851114979730216050377042746494857006614160368685053892398434532295689840844148148333048494227620628325711495715563536682679330612752896780041514467662669741148041400766319386175382099263182317538807978864889297866606041840553002066700814231158209618604490449817172906971058319051227723650998631891760411283960829707263625799426501224062529605572672098583558881236884150518210652588219815384283143527639220672977872983257515665384360435945303296102675718805782221088373597408199503436239294814537710818126769628958161875760555261076125162229540219826448039733233414758091690618675020485287143489074156314434237800944860633736842455260932524828130004533930934938223916781777664214886092534095103731976109630351891823730724891873741287640195020643817519363368736713784567333738312732612063451508186331556297144649700353758865704705828746141566728735921748873707712762263663137045211519650256580701815062254979415746613154408698639189726448626917678502030536008829692697610176147890461015663938907809493961933880575377593989250046269713615538581891854906061128202595556708651112807380963432344111866887855672455477132214489162750609310600155140023476335139718443704560353468877404302320626090951102257106916974881457223156310308334", - "proof_zero_pad": "333881219570656406533813702621007739326893223798709312093880230110707765308698323349655787416622927764660286759413595509168562097893525220545811954561602471006084723854370464588292372344177411825628970009742890486027326083486318250591765490880430886450041148557493256035086093333366856538863804997055014630160398148891505458471475187785964844487574366623786613210999686629199996837263530604784180811727248399282619765139946269873698837073556466097411327810714253558336615702346240996857842468263937192284228460708819497771141303796102442272205169913947375620568611184515278618996081068963055609932083694049470416677831650613212530485438515718786465701216560339593479432977751135696198698964729357641933855260871534547834654529271311274702968870414339041431603843973180395316038540700632039367404861833129160146397216751054576621389267846578465410012517832748841923528052382649357809132078723424994386210512139071602972908323893457533762204475087955495778898196504147067113532550896408441960479309102619456240308461304019515682716204367133238782596150904810398122580626670804333638285675429872026334725179084931422459027764857847294332330071473512413639529834017888932449515948206112685387840927178120731102038058806367957987451477966", - "proof_zero_response": "77495323723837357380662164650567782950204232673124587429831613465434919897389", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "281018035533566392787391182760128076139895306307825596185065589436394690678429344515006855998414199988200675784200345213833735124876021625391611369277438675255817731446795027884112920844316215480727107443805944172181297198285006347545936670333602545035149543074098975603333463597004428823810349968244074041874927645025696650123758010068890761175655218781866980573862119616516054857463778444180206288956963371289511841949461659274064589005763980484585120447171600011114671010775362384560362732055263290173322923272298892537399832770373531357160009694199225185870155172118135793419645063213283514041238278734622832175094900924903844101655339406809293512066454527082200569662333251411342556460771553406841879796084883181345081471895906396675461882154184834788816655538631709609560876941369524061683185029450678189735673486019276788294075212606979050615905483987926054719584838267606548542188337242706119494471932277473813095715593458524919629959847960939411300831903322257694846501316909765542304588373909194503444966545089162831504231441444980936370897623133437074455920410798474911224900285618635315165372454708687933214744651543651780713899319104524525703007700545255367167534095808902881327619529838835699023716192793127538849325433", - "data": "723418000318954325104417970719600276404699470930037639418223005516128350555636894886611940465334736333923272339983368969003087335529281834178876816823777992841313962225049155221219725495071960855626015298470073511759259636277482195448622987141273536096125798482197195868527974345134821523529333686340938162072084931737644552511186669701610689713867901256730296731014729507735672726934379168759431711059866165096958234246326077772930639779509227829453439694531791892016783661481221823242529369937973853037878750341807584544228367249406131779654064864937358419651959063268469394594210235428133942195105783796334412580196229752091885175081662259398388558440224507408310322761324373693751261039788637572526319183976008283939491632693841838711148910124043291424191100012654204989011231896742871896583229410345990275770117060638120629595635925326920702952221444183013666320494071574323962003989694097314569496739095515566659109260489873117265274842904618887910705342247796581662763813923204740757882120915725296538905676179884133211986963353887235723847442182242924050923121261368028053640812046963319932702288775589598087062366334656700001613919630897185028142445439730142801142788948906674116701948063672588112941092097174570167774483245" - }, - "crypto_hash": "96430793943152484536753645907834817500329583556199796682428439473726870734500", - "description_hash": "100903723079508716117155169624171495776522445465308275218110848136673740281191", - "is_placeholder_selection": false, - "object_id": "benjamin-franklin-selection", - "proof": { - "challenge": "35516635483439295705878092695609598687507130740030625214537375225750718870926", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "81805145285400125964131211383556797393531312016859227097620872061782861428599", - "proof_one_data": "550481038755788670937016609669000689687506303091067894602240978767894768182832648552082177088914691341682184817724693323013744625353878323056629422312444605039127667240429474943978926915208861293832186662070561490550343091413435135545861322046052743616765413934759321180056016085604384372661904506852689586517549072625814924969385326461941282814512770047165091629338626638977298689633357211972945781838960702978609204037134020215315849844139647773468031951407469039369035387224887499443616978221115673551007005408753918664095136224576001384762973336767599149561211770934296241087831897550933810756279454494807817294269763768512916952034600235928352308228837199134640896519089684964142082847283858836944180865451246969817412158671843490363326518638854867512228434424505038973098763463195607304148264388200150985510982308190092687492641303424793311632714354524692785441551149448512545137583778448095867878797002773659616363288734295955452838259770021077448777992087940956670866815694851337407059411415456943794012434356945621151812350061626074996516805180816427900619318079563160821236057730282012638873567361507621262943321092317802027934567954694734989627890662330361372922093357907860775684809194193101780351308142251184170731711533", - "proof_one_pad": "725925503579774860044301792721345678158499915297724748316995952252627946051539065288619138077267728085288782199049186674889173599780117275484146817041001776914322336781268057346519885641810907754868497023670262712903128743205723100990396039452455893015965222897460063016299010110236561011435936439835916570060199540635668274423068638785449174114012848747312102308423989073211135851667939581398452591481282347255625937925862554779514677945529304214783380148956659096570216341385635248383472624300056872751738477548192992571520376196090006781148226979065852976845140058445969824169121607726991198926711054962528726328601809282059396627037597623142473714196350973203547392151666703863279779229356343917808067541394432733658690801584396376171356555008892273376578970560740764651180764751232149163246248793965260798734496728263400572120838324715263741703999338472064604904095162718895445020929173001061987308412802442293840445072288472906749055137615822321717112679776753418455465660313273491273016367399779806750887035398435693377759002502474682816917068224421113914742109388845978169949236813388474323519706865144582737600030763488767643367721313884584789361341513640612599717703954406989729788328947604146676423062290016974786966815174", - "proof_one_response": "53490274839185077954812068724206309960161766155958815307388725472366359762096", - "proof_zero_challenge": "69503579435355365165317866320740709147245803388811962156374087171880987082074", - "proof_zero_data": "369281533747753621917676472966556049221292166134722285433802251292066404353552432918317203707456047436765446148813992637602627492878285995058410886744733231109893781271739440818058678158423550377669610847470958238342685485011058476111531251695550761728421195091147314983417666291445027892156761666078549891861306000339247750578802817597414018917647390089903877155941785232553564225731038348851251886772338035579174981061078589568205595281408532212463071747291919582096973904848350681273333054796242832903641402607961081904640725939348855597934651146116759544449903350379796217867212447826617674089576901862213533852556429933450592278578222189312915289056244776804514983356438057116033263446612906986920159430955529989616860244617161497789879978818077392874098948733650420783885861205934348357492934600872312255017833844958227649144762743124231283873226479422518617458537205457938078051256608063011316259083397684689127980155026366043162471785891436774023258169842845064291719571558332170082226334418559112463595476011009026540819911239368732880909070437499719097879910273223997134517682515265868336920521584797789630295078481719679163132284572758089060861333447791351905270992337141348217165422276246668223058876257473450172186404899", - "proof_zero_pad": "113102286459266947180965762084387809788906961786811060256858975695008560533486365933679085609632935839549702659990379014549640631913136447021446937779986813829934275263298460613665610520410971558491870911646943304607331215143338182477158130159513535495823222727152907118807589098356070243692331468437065387729622178139545247334244068303574495121710928591300556687900613668253683542488083285329832748386645138302711861747929214471028147794909276930832673792491092241605889829056836944113709934393513252928319223429891749946847569243481592511717263879567797493342184515184794974080968235380843235008577143767697584874483615936490848115697278168399194236525916025001015357265262829108714132682059024281774117616529248378217001613648174340616050140681399461649367995244466048140850281593306637159622132778432684456688603245506162109198774441829722423915691250348725332458840276732813109986089038735205028896726327359037293461825805844566958418868802884806156785585927223780056142516926710864467675852401603184875125608788696493562163495114694311296345589619623238828911671080190531024307480372167418463162528014145075299384261727312201536574376188157251325806721352298767695669574959912966019469650113992591426981279820567869247150050145", - "proof_zero_response": "114519820645310719135787166898394494049228627491023493571401902914381744524822", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "163222500572978835549602306471499345448738425181260084734710800820960983963299981185559937710961473705843506406513670732794067097345439035753887623466637750181581506368397811956668661328205752806130321927168543148199680936563622760942731566791528704291908537798838447359636237143260459984710903850441534857460503686203705312446309845680130515723102881809654130795942910541157187898518066605492936827529358934141570061961132552865424637518372423293314738142517213936151057264330126082906819394930533448199755615145550764107058576094531692018097211597438598761502642582541041206788530421686629391243164289105370537101013582495658669158552673529382736557216737272075231474979243661710145941815662225422206249243238783625711709060009157970590067581873714239012620795302245345824119993404652444202488103882357708031739438313024043265418624009576194453683137604486099507272467785429700385139611475392159726759201297797654893455378016309887712971935718699610913530398277847134550204212573723557497344288949591366977791054310007569155210916089884429806774073792566760996076052395515227599402626586814112452090195534587329514888897431230942564575041517846191873909166391234584346835667523951161604827150993985704491738281767673342200958227659", - "data": "138067731852148402581465424714905380437081093216596327645057168559594682937092031620083627656031441345660706990922297350720558103408040038624593281707527210848408075979673636889990212779456082512468843283596497669735102019685045335478032713943282548016603278069699144836833904206548767710878616567042442015764205853929790952235630838127058406981342431477212603051458566878217233383381055658635757055008741495159947797864436089616953550679574421991864614831491064540931059635736275701414369070514655733650691938610220991262024046196320612182761538136824307127988250429768022944090160188798823089119311940378580208786813264939735216480908856955658591202280518975543314536791536233861767472751638784778680589227625171461022055495555611890574592109036806811450231222718649419309912527540768199483170151268937571630937921069867958980785915199922841828236685678026518200997636870215237921834933054870049114040313437791806016884535864916970179364947554758023635225229219957274556951582970619090601200967942262568317264742084568568358031925499079601241650294019971008068348747894463416060098938629722314246948076444289049024490111325363841664497484447576084616437555422421233143499132877491404156531043805598901634237298292052649597246635582" - }, - "crypto_hash": "62300245414277510789964065272966614123221978240885846371532863085821898200655", - "description_hash": "48114434793481573738005688457118040577861794859245040399378134418000129379694", - "is_placeholder_selection": false, - "object_id": "john-hancock-selection", - "proof": { - "challenge": "109000679545389600949108963278787764694132988633977898109960573687790463255145", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "104742401582941922551763797509496291563114777776005467917084752890336989839448", - "proof_one_data": "619279324814329583877607735895385910032935560176732648895522962760420636036302957910266326711935587880332292827868402481895776850882960922062678489243436348338389428100246979939758811269005895023737336541991995803071451496676859653777679688324743470080123453998738865355702372739193168694478915987583497930236682034719019758722885996427578377621568534034776592092827784915275049407583335806904020729514963706063668099546786569946544140190342178743097535176593763126784939638186998584309063068837105923718873344416538398805626972803340509891313708001587844060497519943203255019464910564025393665112031173881420346489422933099549317852663940519363786969057528445198445070665966324058476028106460779796428030980910268473754187551148559202296168185973600558764211710001251847313370584023420570320337494204165901942947939920090911994823617055043725483011412241329592995116514444732072604683291406579233355022465022312434345786218450844958140448417599619716026773048552377910394552859738374853405706095290579375437998050743738303180474183364504825483710047829414011782110626668782199293360918658141060978284256761138913204029668322916437226394817149128668408719125050191808853469514103916538942299233601399283166661499308228689772396602172", - "proof_one_pad": "741331971369310045331720775224605079843602311363171940713065556720387010903825647334129479316691030879074381717283833393089627370136576030893997119655743575669474388512918490655922993808566745658383988814479163891356257401725073144088594216573853541662791509065664328788002546130967769721839303025100393696418389478632868445582236415225243948669039407888551605073210038859379633039330731193922285666807651418636160847557356494581914203386959014438216858894705011464428981679094421647258172513748779793723547799763991308020008033271703494355697680464488822318065144487539246847400619580685059295132429498461945553949971525787250723129305504368292063676457126827344987219985450812878994822839862297734038723374252250563487764269639482241717180235103564989656061253735969554859763420234954926910551753364468971574780648666710814298173779700940745053139004692764816773497848295438467665328075375518475976263248520378518912302268993469354381840620505199376652842188688395470564307837917017913010954797224187396538225824682075674485413652724792569595928898966381248413740315688584668875410498169367840718240396251136127599289688280116174405574774520728291160360474015986317721643625759205056486985699897511960198687993112145482064954182387", - "proof_one_response": "111800509007178589621704327004360219283466807140400630194350062497137044854065", - "proof_zero_challenge": "4258277962447678397345165769291473131018210857972430192875820797453473415697", - "proof_zero_data": "857420007094867436299776205435319642755375238113287631587817635128393367617121137322324890590283873164403815816739636306756317189423362038009505564749913470020416658416095764742269307344512612993639159317127864320338099738059998588784934424742580422239851510217386875643584320091603433494022041710203588374003296391507454122869241689777900836037905934489865435941993485368670493194162996025311555547349142476859248186608896903434321669804647661459140732132121191489665992567976945257635548457328748341706595519403116659658091534671603162490494327929915311874560021544670585279666069634412194697722590186402830627353746553776754823631016987098928277291219242606740645660565835841709126833719229953016371687270466428064491584373181180744489110221962955478663470361694996120677108651780251305547218421221146138969509756419885309866069411974823510246386224373958655023135902640486613930921507065016123531620996697161924843346456025623036858104062040422172326950842705461360692145446312110214109135658006522108768956314918667823027580972156062014241019600688450589233710404468590149560679789010369882530669209360738415657827589716939713676306666100423494998270361919517873440062338937627687446029955966903994111682103004638090339393354055", - "proof_zero_pad": "351384956230746703378650138048442758791574823633917750976354895449217268282606295411884876831497529071989594406161120729635410633038530560547624309636368446758452686180829448362500680212366419957927419649651645165370935717634142463739216055578223495522279701316914711498338232972713827519022364957864134558977555210216608449658413292726686832757664523573465778543435652930275376260587261948287006580320697782013205736254797754615712030289798012194773581114837827249338152541641608544787949693308218368136441890867833443102324778120546851463224731093729275539701350962458985367915087384221419003532442190277607023536761036431020674906045464633589540606809388604742544037726795204453503591665565845550169511834677842913089495342835151349144830584093987592273696369499204314231395507138694015686784540502926605017196864452218963785779328465840331753992221001878504550977566746214601367715352319554278899969663245293125341066868772402285092638686975063872792586382547436921626503980204953476227449797266105382102076456539476194766812377460255604194110027339702741375939418672339128806017148281527954071587399621455621215822285599099753625662899147739684406421303870569566020149048903600748618451469123623788507803937336181527071205946332", - "proof_zero_response": "100693091284488478882108038687178027602353829077101072374097174565326321086892", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "253326499876098410117659176845850923148466920680641589705143505437328584020762793731225730720819379863641349868478871732233799757707643274224876999164174931286335736034711715308825925551847911452688883751451410344458397781889143640181457246245641466441848714168033555811276724497039334707015444278852761238167964155235051452584328170271288796343334173050855002453233756331939581631729383974714246652581127954411718979457883200716340802017865933323848193514130051195574507666531086567834614596952787083100540670410981276151507347824680590886785081140111080759988016075117594047030563054849714372011135699726143766142716956870417752193480465587589883531538814642464134920874964697374220672606067283730568081146434295363384669058240479811660182661476275106183998888103748096910985931953120608910823627405527155323518910627226121293082125545739168789269722220864160106135763763387303594722878585261780221940969530773448311853187879521165092197718407706535084436668662045971915669211885483245029442299678926649341736383086689318436056034201291835205538292194573756302247814400009028734317143013585176193600746471857349324778833459000977975607201676563746069856088136592397234111393934403415617703774542465121129129896757541690566270973174", - "data": "247908561423121289938123424288871218857114365711147495202850854176662996382061235580754218603386067094148325026287252484704840343759475214729034605261948245332131068246167600383420462151121813928119021645511418363120394167788248887516403887885806210068680228817899381011746508865553681014320847849177883815343645303796014950025165967180486788554747778659310557952787832978350937209098941863399312109399327853134642120134389791063872846821477798166643584910646593994705161209625011242804196406697137905188934623992695584612714905691718413508404358001662589779022077142923614576636713041116877871506536492804556158916816304411284587856711864318586797117610988599702463826205911961370656490372633007999330909426031366642525895623606236798945745298171277626769652640284677291796527126772338959398140243995036991995897114802438516990808741521253445388598474440456371713611728745608825326706345440222171636135111304253861840798602797390120394640347919887441885413532055816487139984818700455779720427199955913246693440313334498577637689181675784934847689534053046097619386257185159501953571688391791468452221174577825312148555033991771573656494121953819593283401418778064940787654490805102801026790267558207658777180013732782872318829364362" - }, - "crypto_hash": "102842427204649804091046667054049970993687554836769172580163786117491118869645", - "description_hash": "94285541265254522443721385555285178220466866244897457666061438333615239398223", - "is_placeholder_selection": false, - "object_id": "write-in-selection", - "proof": { - "challenge": "68329220481669272212131045277191670779483036770835656667841627882494251395140", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "14901017805895080846624691436307088923413085635130332015771135047739872287339", - "proof_one_data": "339225446579813917826716459039710954902688075425962798240545090378654546139303983231632230713980066243462340446514308204958669177328990641999273886407409903218033432179191223255856578357730432420408445129816644520262014175050614022113186436717848086452312615175757764484673555859022821794357914609606972390732544662052040875177951445314917338809259033229741192082904075589309046631820261166798217122674592382933547999745180033809377103112834795333889752098477143568845983661852500091510700739997019489030799887465448962300557151921006044314587519067591054857733887428665606379919990630342563766641777964675643628666929117050079690609494030042488005078090135092547586659750280090886701681025155070194069911154346623601262728728433757800425402698302464618910525078352572434625429791541164037572508032068290176070754486931637176359610009375256703455336709211898888590718279221671146024130298163759258771254989810784285564871325579514107860684184693828970371173799675758433430994528972271906822758216150670456608039871190018755506527039449424804250898625896997004177032467010465561360800775561698731550817600602987821535977541692596506945719392685661211765946752219409735374388623371064315981725928368145505498092211909086008382077431567", - "proof_one_pad": "973319925941274483646499322959513385880276330440469013237724252427821571026484684289054003445282757785427657744136720481493649495946400231215051501605746579587522440967133586302673449136404984249368779388865944510020767681875893618999387237737232901779617221570737221584691354467573998171889430028345316940340244504798236673416666161751639163889547968152746861448689547178350103865102245413715188009575883244622040212875001704909964172134645608048886361861841239343192429699216813594964526360110898331324859413826228072458120336520836092322516943853363198631307863948834557152958618718883570845137513068639359554437959965526566386981291415366741596564476026102255876911535194341681403650422182156712444481080527390464567557087834762064590703464558916404870398779447839257784169425929689406188926763369635747523213986114172835645353911560013103064332981223954201510447276688532277504039916884655694839721582733944825828682353403584941629606722481482508185938799971474456469693613897063237234991117189425582072083839941383514459838561645971553402439449473147783913139569072543852292865448365588052136639618628472695420467888414108915433731303140435898491575701169204560308621565023681458254838061040071171118006215439790509844557535415", - "proof_one_response": "1384924372451923153848602368644806097251288920718247269599934650839537608154", - "proof_zero_challenge": "53428202675774191365506353840884581856069951135705324652070492834754379107801", - "proof_zero_data": "730389528684636394527151469815441754878832229937963883093637981026340423198174961395302851324813383879346783136431884115270753618370059080283851089350691290764023011168617701910749594622662046085451351692901007195882167707909385148057480110527935016177410162285173920937759775523138676569847379447495574300604513284287818593495651787887622504574352508459900311111122686868284824288035381997706094610129343374526682535582674522088497447791013590774093117404819827815703880582213003206947759825282717292891938410311514329354242127514349493693433512422950572529633531706171105915850454950204224825663535459019757340460863447174083513074643666943345118180596968720084233417158627555883294961904757713260645339349654048355345877836179609547325979185829707896594135855918009743484885941621357210213598468199400567592205599197467998495714599345250820422810347642879028844539662159747420806197679147398134430170861062136089559103450918238633019123709588669767895648495807337171508450984171960879021279424043571928658571418193689279329794679107433737393413143860558918484607601735937071458201797601411512680918280441712312024769327280235058191093300397606571605071416573085804865247808659637564884539806454834510076434392489484114205964083592", - "proof_zero_pad": "223851445073408157239870600176297120029115280358605583609140170464802851374086895048475433907790946389402666802546441353027318974334148393082300739207747333421833655248058995633558857735394817968909830363839205271965539539759037363521753825960117347597250261156924721219088332455102039406731169582000964154920309691821096558022039215940986413535127098445713856742161223608685352213513226765467580299936313253632452245380432270182602496465180388763484733354330556304984971726987375367623076595932627566538645872844596182523402885259772820390735028336699316123489650591101772641977405473522867595262773081326242545369811215466343971115979109914221344751772948792641547447111502088556418415764006395067619081521026988552764047957148387866017369986043380396415822498925728730735491377118193702177285638494258871829282187520818217281279034775062490143095558897899883351214998922671656955730893314168010100027007100877156682770208245481647920399597459890164370700472782285367732783334996774052188266971160677638569381363006204404402105552045305754634308412756089397097696590778709290153014773646418779104138021221661431395740619169247923954523196180806054487966867406054571031498521891652446156850913284750019472760309925820248862932334822", - "proof_zero_response": "95939024738186989327064463868097191687205550904159529530457348677798141797497", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "223266188603749540523226876592212638877611065495373096847166412223911238220916528515029167982973488753335630159695178120057119116973300428585934300645494716296524950101067374183490190363043078701412036333098463827553164784250909446052301940928403313035935330436091990614635771624235417014361589075679185923136017323259204768429839675804416469391928279748110918653014562433436025982287179217298429728681358027347266048409439892603038529331802785433973096063428514814834150217914768982732541751263779711349232272593346349642351176428933384939174042170083226719389379637072798449075926526894421152477845314607216072923388670541424641007522214067709250648663223990034705123009238705910619432551541455712272461897690638923497271279830343418849327032877602685591251565960541536057149409081214490094314002697431923030966055824924693767182099020373295946587446198844736052193661399361215811905115642594555923488445633023510942454238573854326260668524615042492250858132660390115432629293583425056014929740199824084418174229503686738544522935086067585716897045946813908044722181212253161230578633890357015559493151465936118905613738103079974628890820044348455348324629405411581611713324509276011605743860681665020858438474132914612360085818042", - "data": "1022937302739677355243749630192449916191311914137812694499571805111534664330872824690897432957179567295857608022157086116788412053060120450888232420212397289165044245069390232620619845053728182077271392297006512222286865831887027595820912947153062788877985104585077337336280658283194592353013702313267545435928260999762804559262319406598928961474091797816964602682393692074640877942492543490784716071331639912005705846583233938683216523053557070079161820145420489068556517315561361495182407561881144633824882374805554820711642996556798802298207742397414557764440638362361374486359389289547361537903777776433854661915538676284301887131213537367849045691014462185254200991265435320402292630390283233956174687275142593425022641667694223935806460648674207923169506343365313422582302889802737990240950974446701885475608843089338243170119471125237336041414028122395920137517500382605298712979313270788487264352391846653493947511990291511472626765861006213046854774778650828327354692242309090566659818452357984411188805117101943843180839088768760205630792031548201627788929495173592493146500694845752428251185732480825060423493374373879930732130741587631210363720102939792636934923956558668840176955678009009753231052020451963562325061125945" - }, - "crypto_hash": "41608934793869412975217434198785040454808362004505421689184513578547634465743", - "description_hash": "59182947138666290191589623751298574924673837048582553744359155299943041343041", - "is_placeholder_selection": true, - "object_id": "justice-supreme-court-4-placeholder", - "proof": { - "challenge": "100650133753707054982356826741831257868378864091032893545705689310576329954577", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "106878345311811778372015651254729037780890347920933655017257944857324372732400", - "proof_one_data": "296991614232826893695364490327168658957191490247393795442189315412904935209514796518011348364242898472718519956629198190651668746929167606903568641897882293645261969832982491168814948399724772966914746112926196954515185385301431137885695158152285333466237681567783333644945272236386830542432775679552990895029853493483318891976473895476900095440401249676238184265378406796396826177297409329953144384377100922656610731084328558125264790300729194196017610367128604668157188629471211775930496209404648111604476888868230451496053667442377042177594761702372707139978032358846605709435842534921983678326984632330354626869607261515270187014677732203545711306037755342844597431061466667743373629902497146861464362241826257598566831605675733810028524676066844174937110197754510781753187574606228989830368441242364682869467904428216633404705684165209265949061093454696241908595437009052824305910083877147557379244840657517317378389472138419689169335012690689088636608809856289056210718446234915961010965378953268614281978388747323443087711213040810961902094988850305094677008274905362509615311005565161253924066790115973529531950403119996197479182968746864307332077916075933747775517244510207158339651813597021329163286794848892383447448194089", - "proof_one_pad": "735727320321369779798631825584506160251270763879208857043364537211550757747170199278215357977306075593283944562394147907225278249572483352658408861688370358594230620553412356655873301049576040292771281178959019912196411793726697196663464250413525914573477982340846602787866564008953136496834162408674794683926889411492607406190795295455406511925853651917603794731252954631176500242970840364739709366092096280975236481891432502933686517292582600877437976132628234530916235504829556623239958168686878292935327809304339704964985017167218353455332090465478537659973939067888668029058717793137305690245469485892633859687721156891898846635918553486735351186712554679260725510390893819140352786743580879360602722535941125228867255831636166018314758132655895079281682756456689483246422802915866716514707042247167372283759341326138914859779135812329683838661577791947505237578363027620768410951379981609571281563916195509825662179427853872346023289892442140384606967279715917957151094709875973999193243828622824438248958455296036211252111521589184055371359139866180528842872206134980723697696373872099013580142196338093194012810037364998847480311641278089541518216584927033819005831332671315206434278916741433650294569437891971343812802698383", - "proof_one_response": "26655647631020937286919075005425922877629237192191181929362519685619298040085", - "proof_zero_challenge": "109563877679211472033912160495790127940758500835739802567905328461165086861924", - "proof_zero_data": "564757318299149695829992815572034267732203635397928238043306600252717945437231042493702193088477344612026395106667433778853821345159423599293484714316396427730915576872499322708533676797878110735380205424364231943772235149919873363412681128237361724793549456178859030077445537501255198985384755854554652546657263114985715870512038914603721227128956419975916841701186010973160542838637434055823689652982971205333203736262009682492305743006171341547672356266739960958745981898112292558241032670228461842069050968775525357838769195691066021957827867157408302513530932922099896536172539641952077469546314564529162978696475781513988993865010241684948019659935329389860915779963071458570699163780425128843231064244103329470764836100334344256712787826896745983783246572278083562134409101588628258212037033721498322220126635283546803526315598075406889935058744552933407510429392486412662139849260853605789250251430149501886231496758940417986691112590387668297944231798316021466735098796276642745618377224478098563434360254637472315504705909085227041994165561045605687162097075753006714013949096772116037880413105095690689680010035688503490514056187376398262550951386527082118089245832681337675537410277991207009467965347806717984351402276927", - "proof_zero_pad": "734164928000863688964671721574703718497830410531759392130107542115762733680332405390753018306754414442067801853684109110696390434636796750667049474891030344056237972395050727045567908711294598612892160314513347701325694534191217333433604209716182370932901028086309910970466462652760305573472839901538750585914711899289612813186808645021995558616109791236728119124855734560002057374624537109873388573541793994968694733066209092687089607657083876978944096881240971945122391960989609567220854743434118623025766743416751743237388825372969578806911376659928041412749435707169353117217895723656720694534898760769861695495684138727210705227571659931829353139186774864654494411060630786014192066115733413220577032720418699824786409315985919879951521021203499185345182361066213786003815129974450213991329398703201845273552204728534616691011747947054868440101876803344083412164257843948958621779775488719553529357830968801619473527805639667523938142216675905581679619502651322099545285232646661305651691117329830112718510798731111126387001041390847056175080370698218833261679139357671556161967056320530091548091273476260557957472461202144947489122445109923484561634823297983510191232848611274336467420143005207322594543070565308850955542431445", - "proof_zero_response": "39843033438864060458149997241207253700239468732480263982739942207765402646387", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "467696207485717620837613930062547565323920776856820906709055945889070956721191399815777574192650954322666393353111289527615679766308808228929954784298569354599217343785104097534432949640124331431960373596947199614272046108093077856277647222411601548724602496603514432645171246978436475950928052499666056732533176539070469399905624589875540011471035714792336328380166123674316769206213288084507174705791293173159873707877640710269452970849101681772585961858873003166572528884141693242534550628877705725799673379147501898935255672468849646149380424121157790099575515877310186617189501428919022460641471707150367678364646901502913855801473034964761288620301723729684354668457607778508705070752225419696505932358871845694876733565413090593116200323915296350745882573311348606899331461960895770364771689151581595961364245858170714377352747257285057949747402936708649895897179096534744538285549655952867065644820303491742742118368673771645405626183959168722370659483024254023263550423130383374792211846130520216579019345620053796846859570488691517398643710432262136137551048909716904139099660549612210490597637056296671727302379544215695190224082977678796364415119983875940546583046983549072338292799199572203912016668404838088115769563990", - "data": "291230868381692842554525653165916699337825785733563386198284200277420649076990671750490555305373007541029701423555474858437813080750012408139222922854389427687037094318064431194042622886344365147844460723659713694202029001153253680581104106318793843921645588767203443343544354479877823289762309713734737907858772491237005749145373567652286759915041900837834381911645878143406077550132542631288505380599513078621611544289687603485191896587688063218002409416401327772458863408431101418323603270996077893053424954464426625162561592977211254406415147726716239478263288374475772287710797205595999413688319271371859744088497609798712618078463695222258568877953403179012731428151269118731238454568051426316361922195802615392645765087168403500361878453348133731818132653453753343527835691039426718862151422081974490031353894633503184787801825066960117504355547240951613450530164212064641288909668629170807070999555756144499805372706438885387403430857641644745878498647859475019748298801843135274034043472135730143044712319286313761921379154077613414859575228605986598876746670129461707600971994347882021911419165399926390685087710982480878449899130656920870301919675114007391806761245197319095352299101977308514277462975423790306778232236234" - }, - "crypto_hash": "65554850030963577092925787592692016465638836027171959477675132131628904380671", - "description_hash": "36652970354930378100734504205474874875794238233411407783617711657856172688506", - "is_placeholder_selection": true, - "object_id": "justice-supreme-court-5-placeholder", - "proof": { - "challenge": "17126205088795544659835709229860990403972211169018631065211130490003576790028", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "74078191674059612173626370929236050351625729283537888335710911524344288215753", - "proof_one_data": "212609637834804968930513259582001521944952884058187506343287047099428030908779122771129111391760705104934575690357235447659636494263086264265319627548492403706413093513735533639653581406517284898451645393816469555756178061967869147808800231037131771608997393365433588792887757475996154867268400252170699876734653128374793484041138783680566171636397866197558807157795919487451983064896152533599510348190784321265833883907638078314275716094791705619769059800205448225267524067183757886567982523987628778778546074958706831001298793539610997861349592770072413319405696205001043094279526797681605469753126486224540983322009271495627745100664563036399794094254347700049116836462725565312424839776940777924954805773061022331507422907055517601451697464245446949055335417812243232463725397534546750576883072919570997877481037833998099028192806673925474874040656270822667794053403511872771498104204775834233154522189299989045804383629776256385062312243335866826240393843019585553080495359651433224986578332087754140313779817983472679242462572461270669174786436297976346010631122870844814678463797692616036067081822494498769316236474246352288292925040485080213977234624385340857522718661801773958381870604745703923266456396682133202575875856348", - "proof_one_pad": "915713510206950760161210842762511061234401232484196859500983343915835538065944070265506024201873920224446984132115481889267572902966985499318177027062174229454270982826355116894647192314802029237553236763169421400041305244345692849558971377201339421953398155307855340463261952691152135054210792210806520703215381694967061256792553045312586900487794798746204391242849225420640642247019267963524345216214050792941471353538000657886967880352675070529045560147498232298607406248648922011446841160386942920191833578237563639603313259070909259998917382120312173832172749956471495452162526154585052907919569584933558637019634011550793719407767290482754550453204984018850110800223642137804413668824162039029330762655580992652565516124431215638912469827787063272202346989747237110808738019431484298111032394260531612408730871358656029379732398666116818692045686773440599141483255887038378092357955705039866858972999821418303315478779225383925601799681221110410982429500718142769538027725338505713310842423105994127005715718985699115526454040792012988442604089315834240944741394024144814757742208875923867383338960325872041600681397017358566206324755230521790372601112530572471641987922648148408598589406712863867104478716735412430845727682425", - "proof_one_response": "108904652126759945570851005714198121926283298295078238135205521236130037370466", - "proof_zero_challenge": "58840102652052127909780323309312847905616466551121306768957802973572418214022", - "proof_zero_data": "997574180521848084545253843677039035535414188408248094151217576895723909641608594085008887521174618608936871170620855320029262577015457057063726794837965465935667489369814576037972543513919676589811753756991894070060571995060561981010083643733762688524538890811695576794685566322548161477617425811939826510396293196815180973548373531732095604003806229852320242799234368466231608675609763393418754288978547597711199066240494155622022426523616604595216531314884612075065061676096343896578348818150824660069475134012003956112481904801431812648150322768100330169109584407630708600635789739453511200958586622078077164323881527244448699546241853914888642475826849702767062585008099972322220103148291933822307312525701868513584859629446276748382106210687243263663394839457311475082719204531449942501020812958788531169070421983458884892617330012297081444135539851423086388990163365134362270430079534949016009633619963111863119387578115904824655741764582680926556595494200140639461850235187473026331105159865433953952961739650173174395743607607135328259156034530973265884445458243158964261758604234910284612451609222038245761130506659584647851913779084119062110748691346391839887538172472933487977723765064871027580682030478186027961825546660", - "proof_zero_pad": "397935582017910764422741076961140416081596322264477773160368264215213897589223829574803300324921683902331156626829320527574137124788298084562293050204965778939704341539343227722886899652030590336893015286400052310090064818976512261022788371736543484780431282807266688347305488599430803595878444175505900539392099123502714266427907354750003371564630194907753234738903281647940446610763185761456814484496892213265266256973156938463294954823011679735056413334127991780995466030204289339141735358326903872203720774051465310584253921329000769491089082789404924163842078920880322513505683791333089952247579727726334855032651199843090499693910936207861972061528000869297092306282297389429530948265053905570236475724643842687385904898527924345577378437417000623836514986129681727769497400190015003961365813929502806083385136775816222893735575707214776717572488499585828512188971679161063987182442845879339610764594878389193844905827794390096854247840605589987742151500385023712542623261227772941540692108560114395811414641351345946112126981952717472130342867448222665967238682491628577576393757423998226867527849802639453900410352986159191955977408492355377690948976458303789389733993696212826428588483918867099626103451243780469114767694349", - "proof_zero_response": "4564892290308880993061342589062547502770077128046820021710803149378207291290", - "usage": "SelectionValue" - } - } - ], - "ciphertext_accumulation": { - "pad": "656082108865245257826578392865898177962568131720276402699406042885462333750768946710774797021761559855634888518065639729531135281938944663651510683968971741195659227576404659629855352137770129179779782152099619125409006490363976709277759568095197430715432746899656534878644276247561876068773707843450408229443175004906002982746173002757802341543612208468751761262539274244783511897353065852187769697368001586533962443868655451662565408434712794235511947597275193541434494069069392209904267611533271230692988396157811832872594730984206680344129624897854085352060897779896655518263331591234787579676105136555228983239009752649084917334041783888777817767808338692723067832749835485140576666485102115466495683395387915020780093723624728088864146165102309142026975930714212269849418498889193431110399588833641159532010157853944192783327747934417720800037333736351637640972721872770294526262047985649905076713627546530356552396233758409242622481541307029352010033868311266043287035793202146969914725104162380648353464866964225896298720114178914416776765173891314793363268305862569729929634435537517139305209389134109994100724666397774179724196583708287474919377789116768927680272311647660749287933350044991653475828131616509118809152770968", - "data": "114947314405488704144687918542336298259938436152978355204241745039807278813715296998692203813459026681450646177491979392225607163031673566304686071717925583322364446927170353639574145971676056555153713019775812717836859780507277189567614399257774813127470989066498238533003506050335354135152845278753629214164358630420155409645856303946140755159369483359970818222755752131476249303190208128325335711424623954405431850879390709604077270597320394568599404152116245439429950547866249906609900155714033944325338622294405308527450853182155931842870237927583142335612794469320100864204436454292752787319779325323408113570093256320141738166304155420829767703740667942442093883872657044162236003091286978764464594635168616816180323072187354144500335367719240887193099444358484700341227320245671749891624622592148068902594204860213176931048894907416175182045022148586832823004899196216428478086098006163980088933733895412164862830953864080901109604227873873346771026532822391899232520935577637529491130162723680476557353076019825780718593885879171707580889443154275727588144730648107097206021109020050651935957507040882373638790661636510800931415795662838081559944291534212792353178695595481365139953184546166598235981869201790186363951709740" - }, - "crypto_hash": "958376750737648329953276668153457779270013930114544307257511300630325060432", - "description_hash": "31848134331627304581490065170711516991870357219612136050826316526491632113366", - "object_id": "justice-supreme-court", - "proof": { - "challenge": "32266906728009927404215708552619306065006276319245813467879269410882647079728", - "constant": 2, - "data": "647277470444026770019981454976086987726582695099184692263025544440474750006403358028950461322258215604903801502827104077725201344619531016637852272201795209194419017687658741393931350111001838697768399693410666906514638851500632768424258766111638286581988266636985775213283944560773839146626479580963666703949169720746296760104556370221411139200073135161850233669534201594701458249511218958013102694893325962832046500742899122529054513386745186823222898028096483092807307285275513369066516506775321154534802958052056241776330498234990103746844514832877983573189545360785873713661864380680938649622201246145333696831139530862674295811112394227191407811349192355659313524303686069302157040120009713179253341904170587522941874144319033108411914592473194110400280609349216435618530907802723925029033393023503302893953984442689667221307093747338112840039348690385532048428159571337903118662908331228914880762349539157093810408071560555967684712449909443515592166494701411871769853259404535497508708486028423483111527060408890500374317040742268335692258090769478876520371905523107052428588741145541154772659770559767330849041209222417482424514312402476318502679601126754493550837865596907980858357395914343423423633161252621110039233960052", - "name": "Constant Chaum Pedersen Proof", - "pad": "528032039685316402515184836810621230778986143482729521254825361309550217424702481533201547734894671999833258940759054510739962067505524822967983970455172940750209440483689158565037211749718471078617407284675147705953851003406500414841195488022302946896354945151765663696828457950782032143215176223621813363411368274090723033159140660148443433534787800853242463863527225559932694031826777199490160178213583208832077078970541141321658792075354490834564805003143505091791758584792853192028963795886494163463804769988438264163012937700992434296137042871946158707983611836504084994757663415064964458213131070724808686082254834211962496041320935379816160811058186585125138949919216820700606487349263937241612881054297385834521347476164643684727377919302180882956632520037234860969174118733157535594819094701051513430639637380154143087364909292986819321626939662433302170240538771419519048135952618822065925551424379315964835828133013101685180964931897427167440524893675656932895141706252724165623087950781079440598703633526274711544407798489938828415985629784729630030934024764670631439751776341606099069320483485917548217591980813628188436939653240487302250364019117522024425377690579392537670008424842070034111858927604574635740604691820", - "response": "57494448001929129883996843653831204761705436460477456464351644108911744728317", - "usage": "SelectionLimit" - } - }, - { - "ballot_selections": [ - { - "ciphertext": { - "pad": "370063931143138834461159413109160889656523439362739681106282104195516817752306090925603359124312884750311324455406983915916815945848788967785372385493902730327984177917928299700687858484562553663083415393418462534057449057142216175767219145516603508643219485288036626425363434136972514966811488862115322785037173944995694770703184157196180063990991614800471871164392040532572362270157503529402758435431068646369987400547145053375094968466033240325442051645518339512710312687894136101149070838539586773221696989525299943907617580558586829560898024492624414868263982502989998274492227614952575871037960005029532387798539477581899015086751373001434242940321741410497450418772989543635327462076546710381074509049074499613731188198516233659693206339985782274590257891333602190522952880376265673273418321454558868639351334219120189934390102347630846912608782694159665436117712184161979284522372175353285104021115195364885419726572873714376946416660654527291460134737377250027471795778600944995584698927100989050707323860751728734971588581806335850763565694068214941602163708993061184934894608772551219415846905493417573023228060023795308387623607198996712836941735502670462390792271043052659729872206078418159184390587796559260703231288542", - "data": "1010886091664840210343731893548620284849935036842365287242771446085280443268747104913422574326649170023262975068185751029614092386373108363830138538440993923596059148894921796212716107735980875558887355058332167013056575739993776844955643571414603776583851411437113431946689807106465388698226966450383942152771360338998339044721892536861756960138248969879307226085553666150819878878718283017084224163299075232278346511822043660433390252999796360873773412000193777267299096575922105994970844557938436312436318003639542396340072487261771023048886551642756753283482386412834152595174219956313910317884507340868070496615625128929906863463238987886612098318747269323562244270808328303225768681349276167327630366412975079563942035494302410424715862771638859331875628715630796353547467393586652016959140377709582906830430591000476487937047363453425412623376464312059891745009843896123542891176889527232447836513944480991786950858976239622721917261748320496286226600316088920696054190833682870036934664208217886147901735025192351749310131055442532524014990265831690549814971657297113698687856487398705086577928577646856967971428672547002429760319196348811811728631467745892628888574220553822700480065758447417474770229973480708174336086794568" - }, - "crypto_hash": "99612568672718582916799374073281118759473428196604209751675223858152219009760", - "description_hash": "36028895508972511255424989194824868161364781520885652307065383124291074870505", - "is_placeholder_selection": false, - "object_id": "referendum-pineapple-affirmative-selection", - "proof": { - "challenge": "41869544275606511839102951426277033252530569333447717787107357548467024581473", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "7041579867200490694107311972807954639300604185499966614795692378073495802898", - "proof_one_data": "652746431024350297900081340657469532867674030122992239063186457932047581108978234757362459237592820292734167532952267543751637103754423035983597342256112813495153126607357291635424969780838996862346276449920515525553077946721685429227730893233544354838998526438293206067078755540289862213213068120628290834065874495790338738177359877635087401297422448551559230555258817119601303095083406926725746021529262503800125988476770260888862311534450311332496658176611725947498787235876374472539107560948656574910068891456844885347830574721017666469369326544191539087751458668192087860625714750125417481623852701422051114218952863154865769461338053761929641972979226792794289197349166183249402389421419321315085372203701601093013236825656322101463655303026635198932228243619138670297802982014400045386631772218731815463064253492996017095054632405969891125171901538005583217125518570705491050118797003017476021455555510188579189675576930791129924889717025512372318485810793507894804215689642262950726995450846707936085658544029579456453464089946557879013688112082087816100209696904345415231388003826181650881950310267850222255590381488841676475367064431175360738982173148038783840983973480774502948975270646858273458365431483034671264017383849", - "proof_one_pad": "740415390095862658943706139837188061633477711153443521377443618939088628284976625087981333780967727941143041114823679789282140595989020424235956122258415428764145983566193251770100592067847140346887794974191453805140404414072509290410231829888549638362802175017859579264249135864499736849709445461390353320085061027304215836971594599471934943595850967654764002021416610667849361376636765653654333444694928872855990646060648616311499480874813857298645970554419961010652611248855698271260396054905301714856182931888657038927397040247267983369553061119585936971312906331667751243931623232940056063903280551187022183199821318560282299878311165945079149661550491862697257234071152855493562265130575282933631715229264562116288214656735994807927154688579159516572692684519129921785981170210696140280092097451137977048507828748963104118743268622652536751272809978314304192646131769391222610318828135513095795971270919971095489642342172295986731206929416983421893046881084121085777011809385046845120989539157288636091236085938062705430236413928038566913504196874404574178126470482895223216336582682739921674130126613330714017663870068022149678958869019983259745572478057192275072137134597148813736811968707676691664881153262637713317900384828", - "proof_one_response": "44045423765932667415971317282636207086334773072710332996116109614250295086594", - "proof_zero_challenge": "34827964408406021144995639453469078613229965147947751172311665170393528778575", - "proof_zero_data": "37581219842085311549477840121809686592223436466182982574296271068758882639474987459443285548571871684135362956598370678919428025260348471147878695151828792683757914530904739365542995575584084655382608523254174036824890736823883377891511022168238499357351746605394293687037898028408375823314566333279313404784594965162782011128283352818725394976866679102485115479086655066018557448435782532629579491592452134963431376647094219314051088115938947044107281633568686449862107491793429605723130254665786924352024164828649119350988133308618921013230375292521210253178371074849966775774252358689219654555586722669910445717322391322306207629756845129687584420176889157453524467557532791492731719292306273057667224116820384899115365736628825524688032679305666763072421854762190430396906698254276622979248043555739169474573303192583736543347694561196287123119933091085079246981008193201460153415910730263000231158429078244660242703478932241928708543871059503120379243741901846672151866881314855755510561401693255146418552294875114170462605089314088785935330308739559290266419904429596084026834328070869321882523779412802548110538111840016532847834034800315008544288187752147167267642404071699638915706363819566635274370513970847280792230685066", - "proof_zero_pad": "1031546959753949213233373448807198634723742978002428395865694554579299479955926336440523350626049512740573954758045602968011460728909529519793341002613819164505823145732448251278910682010297921592126463544588346733835588128792259563963698903860615000980439460551541463181233672820218527895852385915535639196009448580304147418651153719407273101494599485800240357572661744523396420049292265905048493197744123245718609336543601057472836209257864593360430892489117630320939406768914167177050668232744164413446018989106622661383479410768008809828672782180567039060165256735656635062451745230351249029992022114533093718604126619039754445154537990052471687044735572175000749448894800867767214715732489224316649839378510009543828981210126120291885697075260149874129535187302497503270711047868725971130682084507205778664211786476739527957984243775512424242555526365604849801127465348171859268420205678082518762304744045383228916617356878143204898456982538616946405188251159304932919050885780116336948001713969512730869044270980933423585753302179322558634333689147564366248225865320051739124403763402387248213117508537611992044643549994679553341536926433739778630783081460962231368646621138187657743238915790544579493595529300257446973723298848", - "proof_zero_response": "30422401329554039509521262589854060113308413594405688734611402838429236625668", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "306511185480966540284731238209045941048269273393214988237625533379671978573366616123355045382126463350311202084380384583227366322368308869981266444114948219871387620624261766906273199740883134026414860162175008036120291045662826062119380678273126199169765600102911213300776187198099353823956086086933287525661173453425595121643619353259962361752706268344601461016461421515509800294828078876421748559594265642844974390671993688053020234975127917332455886093015011604784921580302131392838229058013264560214444132378478053721567681848351923370650172998715407167382236365120255914870489091843738728867595836645963450459176900431015358041022547075334030537069085902052603794165474014903179482560916763361925553702928676252794669640039703422218975745076507385784241330785974819557925133130493715622279912044790474650372076055691764993651487768020353350019122937617293528465902073654590580861610930974390887070245564762188344897493871854457042914852281016368481803514484726566863736218194234263748885662537999626374389550364658859860408823571785483538754211934296004376190937014368167406459094413119062748340262407528716049322531056066315679463665887634628393707527536970999141234057388448813901358815803821739319363333426135306788189556310", - "data": "687314691917230891066807608955432008331981449711853944928851981950560757599258556754448593891128839374356015384541004117990924764057012957023943937965994884958926565762379464749106635377078663517076554567020328391652932850996094224511216853599807100607476977421531132619254665657328922311259025389470549277254077483499396640579072969953058760585690727255826544708579330979269311363791795616400593206230560748462575353017439747093006036926899163746800050841598314751611004965071392199835019622941939085845821292306428319807735053400399424062708559228108329979276880085143974733073889896836311759891051620285149135106930890849206323776083136730014325343497377872594301507623433766706642415322420745381310641506686934672449566310199280465999299058316805530113994514756182931677221179274634480705528190314765901015454289615008587741399278144463591693247370869975149121512784933895396783197858362489403577204790731507629883831627150235472024794836809369514361673288020483057156114407251661982906763345783681517483254599612992806353867037269415585435180351988031459161241032302105573817194713074174224177735566243909590628886260222024827030436093324275653566019740544680775808214239342518695205679153768299696671642844900530270635572162435" - }, - "crypto_hash": "16959884323096257367595730756038924099805189166219622066009455723512897170891", - "description_hash": "93225356352688973587700563808773713097380092902130211506835888152987829950237", - "is_placeholder_selection": false, - "object_id": "referendum-pineapple-negative-selection", - "proof": { - "challenge": "70335253721621666129378150981220940632331683439308530634334744689065816917585", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "78251702386716587989193217301628448591286475219116348292769077042615650705690", - "proof_one_data": "936139651894031787004625067144507408005077848089942662174664296879592064718412565118194754535764818503658597077907533504271672537502062011871614799052185148287009288011951609945433607968137886659874391870345474174149869212142128755775627895862153645028433532635347091228959857406581012716519215196941099938040236108279881905926343877637428186994387350370652343500014948918724558551586660905300623970064011576991327874150162526839393425777988241776429527290460914154479094808417315495379680114821887630642061444579149533200145748499845920143520889628192263440927147373150279773659286500047856071314576934924058087425625554087670005865224789743915109177370973501162238560262169910903711173809575377370686465485977480586214753651933553444409755952210820997840701016875528601978801292215303275636142527863639594891449033041179374930868934826027018480132984898036846726315063112853694468410375801675155905759175873607892721869395723592898254328560173733961907497172529547641697486933412969040020005290418018035640556599254858607879463635637961293726180201479825011437538132745563261852861939583755032994196481563996129607115002844096000419153421089944001017866155079566099690551075642214041489453212957888170519834284306973680283866207357", - "proof_one_pad": "1017230483875747777266827171909039881365191209636868913513359718464254674372602018300387159454998356832196634208913100567761681264947082768585115297718962543519199119505319377766674415560342192354098434567188056204956781552311574175063108970641272995010490388065846032927983864162555506472110301630401935095134995353213012058482265872424222684393570049508399853679913345794898483827413778538975894300436304970120091221650460843306778800975464117815211055325821192972318624081808435867780612526693841352631391081446352963842891032450182044797508268718718052850087939793318742784102726937137983392331558259325803337861224615776004705887184095679641160628847850759380047455267930914057263757280177801255891300271061826555883015929112201904683439765264087331552237691723251725260974580757956863005084953970316612531707507560020113807644557039973279871239239333828077006595593081088421945669521294945030491848553453280172707317714318015305015484634769097454553520210642529670726516157148797640939745810687111580399858650255080695329295415535595159754399818604656277445396299582659428078994089005058745985653758233778135538494143639665511629143195211247625660924207520558819950442568095038047191641126041705001809990688850488384860083276158", - "proof_one_response": "19653034299072348518783246248121133232887410422148187875928594050870612543578", - "proof_zero_challenge": "107875640572221273563755918688280399894315192885832746381023251654363295851642", - "proof_zero_data": "14533237144923220629089629080460133928137152444962202366638761821628408866956349790638336260219262686903372336446661482867162745302833309378340702926084467235852055674316629769161752806682258638410510458526560774610615891586006606174611157672258041801255012147291158832539417739029139019001422478646635275343327317311676315491503127379672384052790456220300511859653352834249293602446775718432977218340099531346995885633697456634464287723614120977879770696235298575397891878880111323893278575472073197205504809779706516027641010793371070008093945891498229699654340114356547695511546078292797489681516550863558390038727770487898453744266278203458666875699605568120798119383023838944896734330059929674169541348106571308815833461410564161997305293893752708794319949156189157185588724045235654194200933598109592846539754965694090553376683125743365342167111481674460507607486351370038559734951797148974464664544853323523448807821169968428804438837410046845166108059631729584273775866618743990654834289694184100115470027089227755543085586248952741145137797421250477068621035652627760825547830717464991781504775941638037238485753049453709327771071333149308522437700898432990107673208430983499159608833820373317856688424824172108411714610718", - "proof_zero_pad": "709485839021884904067137192157170221665631207233502651867243114953942862731392190054734911160891749533628264704519542956227449656402694622570095848607143254066889557995759078398791248318599950187037493676219520760035253456926304693179670433100079372070656305266488562814172058228728587926643552360224179299571390665452150297033590266677595372052247730486140321159278297957290657882779253873051723953966082632034400716013743209601575783578796174814499590993837939049431256026415100105306724098481106404105395430476187091837189922850375544419637532196984972471219999288963169946947884445111985466501335251177983399459592848212087771144851202047649131087148520858879064279445647631903486918294179792809420297905557261358731256359049440804223790615283311600314949035994758774158024366977218349729001407174534706970295290390370799735051902173450727538396353304577937065197329956814758365597721193953639415886742568995443675887112327059321362018708923578422748363608352437701476046530695900829957755914552133095646271628060476728303466233326027209070461356792069484173123716939889876997818190774323817406741940719861434970229155148317116994534648862115391779997541004373621315147232789954797649479797402096466396064629903814071513568250636", - "proof_zero_response": "51342745108518070155474900087525097601631542261238048140497817943985924704988", - "usage": "SelectionValue" - } - }, - { - "ciphertext": { - "pad": "574468355896493024610051470318375554145620192856817407547353086350066432755841961029374199341655124741107468871445710761739454247584941799129195148707340031424470386816300310605777782161546331832812533053301708532343680231588254509705052112092201876443584605098793173052406785178864959839104346173263179971932994594986592143554961409428536167040923961339921144974390380003392238180743963874898573806326736258543117201823967155780821196129437124527403683753646343761382467928462165248984045288348585602932984738284568330447735944347028136454891072644676632866739897791101871306034541992266710641133184374201082406009140416049791898477337725236837904534778437871938046532591706632742927579294901128437708149699934475561475846597851044256852120223034986566861947890937042371708656990184753510345325936814910986397904575213641299815973192160826899720314170869622237567728387343474211327843823939363314653691210188117663744435615794980076907662854185581894999052474197804320920453296653878933414682318960972520917538925145517691801896852054556954265941659974978195377173620751759311336597720257678036781955292423290359791530644122243606637135730112226165361406440452169802155289980075804437120533698332154380901694033287279217110700860874", - "data": "52096051270368316867618717983452621405240328967756874420889560975062704287688527000416288063970477445668143218230763307210395645146372802977291460272952388145174953588921676205955092264338390855700860085186253090786310547135600705422980450532031267874666745969835233566389755151643395188265359373996088953006975004587548772891814560624348343672829113588999915969508911694921995577934833451020046844518741138092005044518348788345391103089374172023021344768517102955475830143166178640370886929246061515140685573994201487807303485188085325806714780964292445142803240197389951806916378404412071548475946060779415859719262928140733445534480372242221837600072347823575799494150211355418682176998438957315200691666939722512285707396165779277909211615257900656094826163086666751192599354953371916314420693688614826662692280374399498802945448342656479830569389156565195001704162738394766978226656761544565744645630312800214144045801300242081100035253745442574001303751072822564982246917588198413318298231319410418182004438497853178399406890174982046252106403168354135389668449138060016596722318390135013551123785515709069187748186273276724692929665078243181251560630526527211786184590899490121248551903439898010237468947728282856917513446152" - }, - "crypto_hash": "22186871833352201386429961172458883193973649489438982403887305898007872488416", - "description_hash": "109250513636094712281287223584303810752028947254197709668261868874416549682617", - "is_placeholder_selection": true, - "object_id": "referendum-pineapple-2-placeholder", - "proof": { - "challenge": "28829616957355033511361193184178414579668131795903688155750907314959096862081", - "name": "Disjunctive Chaum Pedersen Proof", - "proof_one_challenge": "35485334073914312343487002437640774789251289067748746276675657227503042614784", - "proof_one_data": "245028384905881853848339076738231356618288052532308000809769244474031428444260819633149597156193554891828252383659953149323174704723483986757339003060645068522381986409755544373936260697148558549848827930645931395532070360032399538770861892052595753901826569866603311239265225783930503352719214742150933970658068150096009851672104032567802343385899940836932809262113241024203149551457840757382920382843847291731951455892867743665062564796558427706688819264672308150889009774658747610323904791912422598862240312634615874190338062957488004086823782912719595526172787932053216027099042515107700623167646655805168994974557354634994857661881888369951786550821096807727111602987997390831109160131697036021267754807834243523753104467053247947731447307827044200895411353210439024466865028616698969803865804840368165270782763171967860106044346164729302537301498063392072332443082398285619390513703444973933018074214911669704299220166383938680162990771937931512768479987489524555316297700228990822408638218726009890997966235019127995683606822220448246688652221729024044644972874834843764365080569324216919838136495643367878312452532280677474365686851002664050749503545435571149260306202562700475681307143832679063210847778961159137015273588456", - "proof_one_pad": "659093690580016038361211392285339458171988070241561924356438224919095610948145439597983429051959619218350206072663653047747065694613137784136864971357278255877226852471225266295681482826887842003267712568356418300693372252917997658820716763401384649672874998886495033512575841301180473175327888785549311028768546909772326428105172142787362805859271046645980052630883252911157135539811309832856819024690535347284715930796431313174588359110208538695011661658789569275423398520660635871277483737203838554338932906166985816547266233368188626948482246369778238587217585300125263543660042907320873993639452593439909660795367325655680404116429825423044703926645318195829801638201371633191932723624436246155913474305559607755098374222896419955192990719449534138409380262304440006349282488552864514716709401895378008523084615204522087683894158728646495762913511252844912963045208791357425060665371556519147930466997493673732323987225915808445181435500013389746502600966860451307451378477519554046884789413644204222483760205432888966931870912655687809847304321599769743459535530354370622881547825088747633933020889018034657879236754769157927329319996886019408090706247055026176062105334735207813258445623593779176215194472791552853366149374817", - "proof_one_response": "46691569008251242350373014781667382875546418845702273413139570311177663112245", - "proof_zero_challenge": "109136372120756916591445175755225547643686827393795505918532834095369183887044", - "proof_zero_data": "881302677698689483203677011118451527753845606359491810060071246073035892157368717232862295126426142480149248791946159540012550167657297603838207470397233390286758863787808443324444389760089788425860392027273136251886825249616367529025753092455011860380063348679802994749535007204274024923951470712239898693974723446824350167945434142300108748968041214285218026636528750135512254796809256539014697194181749328374140472980146824127417269827618111811652737342458008369765590170158351669598701756134731381650550272811525551789591874402005921191849960938879133091280145015522920185538798399328117910130467672389368070980681339515011879714248260015980992247849691675673377749085395008272381619847729285027973554539557397308326330768724576123837162482273237868686460794496393459440896654822882180638829468622872074747000524960564100153837197426205543473185120342412430876616464928355634511825011699812702100195066698234368184023242054680422319845816882744186423256732112802671147219529179078995417526479986635340778555542755810920136255373654258066774506508283516404549796652165723875903607079354715039389596950712140679180186374463519200707824681271648335968643432391038309990922890628466469018662320496910231099063405606190967058286651397", - "proof_zero_pad": "147208633386848552195538307265900905747181830417255459500803189911118598398786005210120291802453629729515137896161047771168445631894642598311133851634288202028940284353951513955665603190125770943465395747352772790753865430948281015077346569296854300963016046462258650198243832542154100522433114663148810801750461367934064868097810519874124152838847374978165948799239582505848282345091484379475651992396281987660721857421935203447140898402459267308023919476479212566022536679699000317289981614996965281700445611661292336459600454016504762712252345060580161095223760119765028859969876400631844981892843965500227485473731643209334023935589536711304172773031884086061512809992831833253387948959131978420475159934619741416929044084464985809032047711751144712187780004232904798486940087332734006462273635709798281751792429132621137081221319308664324536643735475833641599549612530572846410779701330876289979855155004982861040359094100204713562339509499679058310396637837571925768059184885364209338688459027974541970068690644384510913933209423723482359895120521879771558179594200420589039522793949142193581285884870385533815211837929332045812152101935278762012369488221629682841422597511653151300762689742730123926676491556479931423082006920", - "proof_zero_response": "29477880659742828413740705384838137091173514985551291279657853236126577035770", - "usage": "SelectionValue" - } - } - ], - "ciphertext_accumulation": { - "pad": "303083498409111030161952395382830546530424343256672024146277445982239603587434532977168830230107041731280093622518681164966615528254387621652233948538972668209696999399608185877314768675290498128534245612602642190204374687903313692082764508782815399919021661929356278501695820730371844694865592034568808630882856760568555599008145723432717195896697577576543733172436695638318647416630741028943269020119392686736787896001840261340235689765661602027622522204264432210551911908193819725393367420080844902478635950388900927879557432794094088403256492283079764856065375277539113765932318229804868169148071882345252950066749099153232374045161034488573869751427853700261863191374212139044916994546160386480245373617842366951725297662457985899340718547191494891906456680150595580471400040083375849993971596515410754512131594394431328351538461609080788137114495564471685907483214980332969024425261662276391235328144320928208125471778111431734386830446809319875123873839957485438544791276218807610144914878426545785951880442148117523365181584508943083067322385277851481397971292668320391820041559999419885141292403044283977166408922299138282730727579925051875341857968820594488342421445747551293572708593728353301593833290787587371890418058473", - "data": "507966236839666847775335784501797310046128849263480556153521818932236738737715421766919840170234286976174713730833412496114281098130209709565931022153974182971805528583344133862349327426223320172587941583924597462588825261556331452696741101017638105884534086198339675006034900535303465953663763708821426772324370644348956309552759709022017300137933224078042515906116607968042508099874275835330420004304929380363407453299004131597545756617362660634022006190743126241599104845083978748586313580672980065287098938101891957876932853779385579167139762054126514969183952760007674744808070163229778883797650138186801528043861279333166576419714453652113740337769313462921845525334109895324216933227965734694198023657785208494873259440172468665781409965418959483497515799778916228014537799356541317308863557368760398114241109088645860281224213436736201041682559738238050503254952040826986319660088171033856162015740424303021495611884850691462870717906107563240100522542657228141050700778155798203633194934686774570625602879551106912411015847826040771489114378502046747201866178701759622937642009031248860488138345510720754110949880851205363970924220630777334916754424951909148425320028924428477980297868807804420237654071639667348277821964770" - }, - "crypto_hash": "44795848747368442813396026569387819858469653175560997337043688355863655116315", - "description_hash": "62776588317749422479017371709284676707220396496421279871138731384599870808677", - "object_id": "referendum-pineapple", - "proof": { - "challenge": "17124603932612253387665284922788288082888964332640554464388780927890112401670", - "constant": 1, - "data": "244893237071091182563060824668764651185416072170608878573759047953791860162136282069557571283149943211509960798239241564951777581599327499599307784888589166253825174018045527786837917428463435972567688244027412590252239710559080633262349482739173664551031601804326956873289868457699505178042457788099967652744577850182574484647705438551574443052069542825888757538390009998392879720376290907984093384310443482733279151829048025737564304196123521801457629310474605948739079834946188278552365076511197005533183105791401184068647529587481486683507137141849837523152935998865270231678286797010676796963811551560536976414249296469747256481892413126678096969629107841066062186964631431381183020206385824607368696732189516836827049360483296858904886817500774965958847989424461019039718207215460196940022267696318709960667169174131874220624665236525580318712774069875827063629455265216181031315903999177427402274007200584310002122002456953168676204419620517995278982328299488064571302788748404116210354375767175076092505723435552914198686335673209366861892090591317332426716305931056426705003061820634327304522680191143741032267609906265285029394859523038987841084709748918741183935992903333432801657728304278959969476861336054631995053869853", - "name": "Constant Chaum Pedersen Proof", - "pad": "611721532935624869049374647502279064713189606314088947130256906320555104587643477161017267087616205203562314444188493556665998582111303797285920525200526560086570816789222884561606791255709828431962937351836766590220418685376478147922689074524992707677979422343301164864839148633213578664882509944966187024356533360834823892998756216519431849579571215175586690080282994207730701187412148515575860530681034983765412389767073015811844813143669099893369923557741302325578190794019169618825065973223972943716481422210189949965911474353889428886114322064856727870524117830702644714654304629559768505526197839765379591571062800542657437767753126701027772460024323849787673328092586866842274036754450836511928441746137638732457230573166807800219109634036896378229985111012888530712313707726360644484369684787238100842512310217242814737622828244048402522101982460750212671754414279846254223347677952019701950110518465246968200903296072556231041820470956601847878618620104452137740037323881528629412561792281623987956746626830038726829679170331130528211994118349912040502706866789471043379417694529853939920808961382251838071695598897119209832261467882926509511368698041057677493874352422754762558759226300491088257707806852142698457516426841", - "response": "109860154152771287360042651977623549521620456755299867705042999313648050294532", - "usage": "SelectionLimit" - } - } - ], - "crypto_hash": "20191521807488946502207736738297125991082421862335863216691892641552043517815", - "manifest_hash": "42295651972893794090576860616898741210647350801781761163514093198104171668673", - "nonce": "None", - "object_id": "5a150c74-a2cb-47f6-b575-165ba8a4ce53", - "state": "CAST", - "style_id": "harrison-township-ballot-style", - "timestamp": 1621947524 - } - ], - "manifest": { - "ballot_styles": [ - { - "geopolitical_unit_ids": ["jefferson-county"], - "object_id": "jefferson-county-ballot-style" - }, - { - "geopolitical_unit_ids": ["jefferson-county", "harrison-township"], - "object_id": "harrison-township-ballot-style" - }, - { - "geopolitical_unit_ids": [ - "jefferson-county", - "harrison-township", - "harrison-township-precinct-east", - "rutledge-elementary" - ], - "object_id": "harrison-township-precinct-east-ballot-style" - }, - { - "geopolitical_unit_ids": [ - "jefferson-county", - "harrison-township", - "rutledge-elementary" - ], - "object_id": "rutledge-elementary-ballot-style" - } - ], - "candidates": [ - { - "name": { - "text": [{ "language": "en", "value": "Benjamin Franklin" }] - }, - "object_id": "benjamin-franklin", - "party_id": "whig" - }, - { - "name": { "text": [{ "language": "en", "value": "John Adams" }] }, - "object_id": "john-adams", - "party_id": "federalist" - }, - { - "name": { "text": [{ "language": "en", "value": "John Hancock" }] }, - "object_id": "john-hancock", - "party_id": "democratic-republican" - }, - { - "is_write_in": true, - "name": { - "text": [ - { "language": "en", "value": "Write In Candidate" }, - { "language": "es", "value": "Escribir en la candidata" } - ] - }, - "object_id": "write-in" - }, - { - "name": { - "text": [ - { "language": "en", "value": "Pineapple should be banned on pizza" } - ] - }, - "object_id": "referendum-pineapple-affirmative" - }, - { - "name": { - "text": [ - { - "language": "en", - "value": "Pineapple should not be banned on pizza" - } - ] - }, - "object_id": "referendum-pineapple-negative" - } - ], - "contact_information": { - "address_line": ["1234 Paul Revere Run", "Jefferson, Hamilton 999999"], - "email": [ - { "annotation": "press", "value": "inquiries@hamilton.state.gov" }, - { "annotation": "federal", "value": "commissioner@hamilton.state.gov" } - ], - "name": "Hamilton State Election Commission", - "phone": [ - { "annotation": "domestic", "value": "123-456-7890" }, - { "annotation": "international", "value": "+1-123-456-7890" } - ] - }, - "contests": [ - { - "@type": "CandidateContest", - "ballot_selections": [ - { - "candidate_id": "john-adams", - "object_id": "john-adams-selection", - "sequence_order": 0 - }, - { - "candidate_id": "benjamin-franklin", - "object_id": "benjamin-franklin-selection", - "sequence_order": 1 - }, - { - "candidate_id": "john-hancock", - "object_id": "john-hancock-selection", - "sequence_order": 2 - }, - { - "candidate_id": "write-in", - "object_id": "write-in-selection", - "sequence_order": 3 - } - ], - "ballot_subtitle": { - "text": [ - { "language": "en", "value": "Please choose up to two candidates" }, - { "language": "es", "value": "Uno" } - ] - }, - "ballot_title": { - "text": [ - { "language": "en", "value": "Justice of the Supreme Court" }, - { "language": "es", "value": "Juez de la corte suprema" } - ] - }, - "electoral_district_id": "jefferson-county", - "name": "Justice of the Supreme Court", - "number_elected": 2, - "object_id": "justice-supreme-court", - "primary_party_ids": ["whig", "federalist"], - "sequence_order": 0, - "vote_variation": "n_of_m", - "votes_allowed": 2 - }, - { - "@type": "ReferendumContest", - "ballot_selections": [ - { - "candidate_id": "referendum-pineapple-affirmative", - "object_id": "referendum-pineapple-affirmative-selection", - "sequence_order": 0 - }, - { - "candidate_id": "referendum-pineapple-negative", - "object_id": "referendum-pineapple-negative-selection", - "sequence_order": 1 - } - ], - "ballot_subtitle": { - "text": [ - { - "language": "en", - "value": "The township considers this issue to be very important" - }, - { - "language": "es", - "value": "El municipio considera que esta cuesti\\u00f3n es muy importante" - } - ] - }, - "ballot_title": { - "text": [ - { - "language": "en", - "value": "Should pineapple be banned on pizza?" - }, - { - "language": "es", - "value": "\\u00bfDeber\\u00eda prohibirse la pi\\u00f1a en la pizza?" - } - ] - }, - "electoral_district_id": "harrison-township", - "name": "The Pineapple Question", - "number_elected": 1, - "object_id": "referendum-pineapple", - "sequence_order": 1, - "vote_variation": "one_of_m", - "votes_allowed": 1 - } - ], - "election_scope_id": "jefferson-county-primary", - "end_date": "2020-03-01T20:00:00-05:00", - "geopolitical_units": [ - { - "contact_information": { - "address_line": [ - "1234 Samuel Adams Way", - "Jefferson, Hamilton 999999" - ], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@jefferson.hamilton.state.gov" - } - ], - "name": "Jefferson County Clerk", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Jefferson County", - "object_id": "jefferson-county", - "type": "county" - }, - { - "contact_information": { - "address_line": ["1234 Thorton Drive", "Harrison, Hamilton 999999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@harrison.hamilton.state.gov" - } - ], - "name": "Harrison Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Harrison Township", - "object_id": "harrison-township", - "type": "township" - }, - { - "contact_information": { - "address_line": ["1234 Thorton Drive", "Harrison, Hamilton 999999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@harrison.hamilton.state.gov" - } - ], - "name": "Harrison Town Hall", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Harrison Township Precinct", - "object_id": "harrison-township-precinct-east", - "type": "township" - }, - { - "contact_information": { - "address_line": ["1234 Wolcott Parkway", "Harrison, Hamilton 999999"], - "email": [ - { - "annotation": "inquiries", - "value": "inquiries@harrison.hamilton.state.gov" - } - ], - "name": "Rutledge Elementary School", - "phone": [{ "annotation": "domestic", "value": "123-456-7890" }] - }, - "name": "Rutledge Elementary School district", - "object_id": "rutledge-elementary", - "type": "school" - } - ], - "name": { - "text": [ - { "language": "en", "value": "Jefferson County Spring Primary" }, - { - "language": "es", - "value": "Primaria de primavera del condado de Jefferson" - } - ] - }, - "parties": [ - { - "abbreviation": "WHI", - "color": "AAAAAA", - "logo_uri": "http://some/path/to/whig.svg", - "name": { "text": [{ "language": "en", "value": "Whig Party" }] }, - "object_id": "whig" - }, - { - "abbreviation": "FED", - "color": "CCCCCC", - "logo_uri": "http://some/path/to/federalist.svg", - "name": { "text": [{ "language": "en", "value": "Federalist Party" }] }, - "object_id": "federalist" - }, - { - "abbreviation": "DEMREP", - "color": "EEEEEE", - "logo_uri": "http://some/path/to/democratic-repulbican.svg", - "name": { - "text": [{ "language": "en", "value": "Democratic Republican Party" }] - }, - "object_id": "democratic-republican" - } - ], - "spec_version": "v0.95", - "start_date": "2020-03-01T08:00:00-05:00", - "type": "primary" - }, - "context": { - "commitment_hash": "23146389540605545658039063500747697074194868483244601230987666475582608354829", - "crypto_base_hash": "61139809069877416125862357760348604394236812310408253226283169880397721842629", - "crypto_extended_base_hash": "54086180756270009611286531057262280364451842178052193483805944271194176778702", - "description_hash": "42295651972893794090576860616898741210647350801781761163514093198104171668673", - "elgamal_public_key": "239866307965829635533323061230412342513795063377948088179476648587461166058568313786395089948884648845412098521478973753030757072513871728475543149309303486190168705577618204721899178045011885722723220150000249387432998752887832706014214148277728783498234218871238860964639059705488993144080608179557455084014457849899212191297601912548124681321949078023012442703853966768703275373900312370573793031659858935252910464944070764281991132112622180060012863193729680239186813946439181077786905456201985590258819794550332597422626677253608879621938334370519519415254071693224286082184352483972460085797727324230048386908478884682490404278894611068857318302652744871635869570416477115728270576698902824141888479852422371416210800668087411731496372100024268921116301488016309602690503212268006501239221151713632404877532597928845783157896409520614860563783421641225669613426621680189067390245106984208075898775302399779844397408896217823915954413851129713491455599963384704321821603256023699775555221547838993560646203951646681393448153786140289484897613111781804161843144424939090218721460109957592551804278772427297311090731569950917058203977464403395934322311251332654138649564744866801009979862535761317317865068603935439684215332146051", - "number_of_guardians": 5, - "quorum": 3 - } -} diff --git a/app/data/plaintext_ballot_ballot-1663ab54-e95f-11eb-bd0c-acde48001122.json b/app/data/plaintext_ballot_ballot-1663ab54-e95f-11eb-bd0c-acde48001122.json deleted file mode 100644 index 73b28b2..0000000 --- a/app/data/plaintext_ballot_ballot-1663ab54-e95f-11eb-bd0c-acde48001122.json +++ /dev/null @@ -1 +0,0 @@ -{"contests": [{"ballot_selections": [{"is_placeholder_selection": false, "object_id": "barchi-hallaren-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "cramer-vuocolo-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "court-blumhardt-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "boone-lian-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "patterson-lariviere-selection", "vote": 0}], "object_id": "president-vice-president-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "franz-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "harris-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "bargmann-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "abcock-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "williams-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "alpern-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "sharp-althea-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "alexander-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "lee-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "kennedy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "jackson-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "brown-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "teller-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ward-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "chandler-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-governor", "vote": 0}], "object_id": "ozark-governor"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "bainbridge-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "hennessey-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "tawa-mary-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-us-congress-district-7", "vote": 0}], "object_id": "congress-district-7-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "moore-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "white-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "smallberries-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "warfin-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "norberg-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-2-pismo-beach-school-board", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-3-pismo-beach-school-board", "vote": 0}], "object_id": "pismo-beach-school-board-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "ozark-chief-justice-retain-demergue-affirmative-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ozark-chief-justice-retain-demergue-negative-selection", "vote": 1}], "object_id": "arlington-chief-justice-retain-demergue"}], "object_id": "ballot-1663ab54-e95f-11eb-bd0c-acde48001122", "style_id": "congress-district-7-arlington-pismo-beach"} \ No newline at end of file diff --git a/app/data/plaintext_ballot_ballot-1663bbee-e95f-11eb-bd0c-acde48001122.json b/app/data/plaintext_ballot_ballot-1663bbee-e95f-11eb-bd0c-acde48001122.json deleted file mode 100644 index 6c99461..0000000 --- a/app/data/plaintext_ballot_ballot-1663bbee-e95f-11eb-bd0c-acde48001122.json +++ /dev/null @@ -1 +0,0 @@ -{"contests": [{"ballot_selections": [{"is_placeholder_selection": false, "object_id": "barchi-hallaren-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "cramer-vuocolo-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "hildebrand-garritty-selection", "vote": 0}], "object_id": "president-vice-president-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "harris-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "bargmann-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "abcock-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "walace-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "windbeck-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "sharp-althea-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "greher-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "mitchell-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "lee-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "teller-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "york-selection", "vote": 0}], "object_id": "ozark-governor"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "soliz-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "keller-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "write-in-selection-us-congress-district-5", "vote": 0}], "object_id": "congress-district-5-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "white-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "smallberries-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "warfin-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "norberg-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "parks-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-1-pismo-beach-school-board", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-2-pismo-beach-school-board", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-3-pismo-beach-school-board", "vote": 0}], "object_id": "pismo-beach-school-board-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "ozark-chief-justice-retain-demergue-affirmative-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ozark-chief-justice-retain-demergue-negative-selection", "vote": 1}], "object_id": "arlington-chief-justice-retain-demergue"}], "object_id": "ballot-1663bbee-e95f-11eb-bd0c-acde48001122", "style_id": "congress-district-5-arlington-pismo-beach"} \ No newline at end of file diff --git a/app/data/plaintext_ballot_ballot-1663cdc8-e95f-11eb-bd0c-acde48001122.json b/app/data/plaintext_ballot_ballot-1663cdc8-e95f-11eb-bd0c-acde48001122.json deleted file mode 100644 index 3e3ee4b..0000000 --- a/app/data/plaintext_ballot_ballot-1663cdc8-e95f-11eb-bd0c-acde48001122.json +++ /dev/null @@ -1 +0,0 @@ -{"contests": [{"ballot_selections": [{"is_placeholder_selection": false, "object_id": "cramer-vuocolo-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "court-blumhardt-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "boone-lian-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "hildebrand-garritty-selection", "vote": 0}], "object_id": "president-vice-president-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "franz-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "sharp-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "greher-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "lee-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ash-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "kennedy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "brown-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "callanann-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "york-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "chandler-selection", "vote": 0}], "object_id": "ozark-governor"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "soliz-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "keller-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "rangel-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "argent-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-us-congress-district-5", "vote": 0}], "object_id": "congress-district-5-contest"}], "object_id": "ballot-1663cdc8-e95f-11eb-bd0c-acde48001122", "style_id": "congress-district-5-lacroix"} \ No newline at end of file diff --git a/app/data/plaintext_ballot_ballot-1663d854-e95f-11eb-bd0c-acde48001122.json b/app/data/plaintext_ballot_ballot-1663d854-e95f-11eb-bd0c-acde48001122.json deleted file mode 100644 index b2ca846..0000000 --- a/app/data/plaintext_ballot_ballot-1663d854-e95f-11eb-bd0c-acde48001122.json +++ /dev/null @@ -1 +0,0 @@ -{"contests": [{"ballot_selections": [{"is_placeholder_selection": false, "object_id": "cramer-vuocolo-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "court-blumhardt-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "patterson-lariviere-selection", "vote": 0}], "object_id": "president-vice-president-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "franz-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "bargmann-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "greher-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "alexander-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "lee-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "jackson-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ward-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "murphy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "york-selection", "vote": 0}], "object_id": "ozark-governor"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "bainbridge-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "hennessey-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "savoy-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "write-in-selection-us-congress-district-7", "vote": 0}], "object_id": "congress-district-7-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "exeter-utility-district-referendum-affirmative-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "exeter-utility-district-referendum-selection", "vote": 0}], "object_id": "exeter-utility-district-referendum-contest"}], "object_id": "ballot-1663d854-e95f-11eb-bd0c-acde48001122", "style_id": "congress-district-7-lacroix-exeter"} \ No newline at end of file diff --git a/app/data/plaintext_ballot_ballot-1663e3e4-e95f-11eb-bd0c-acde48001122.json b/app/data/plaintext_ballot_ballot-1663e3e4-e95f-11eb-bd0c-acde48001122.json deleted file mode 100644 index bc83d83..0000000 --- a/app/data/plaintext_ballot_ballot-1663e3e4-e95f-11eb-bd0c-acde48001122.json +++ /dev/null @@ -1 +0,0 @@ -{"contests": [{"ballot_selections": [{"is_placeholder_selection": false, "object_id": "barchi-hallaren-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "cramer-vuocolo-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "boone-lian-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "hildebrand-garritty-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "patterson-lariviere-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-president", "vote": 0}], "object_id": "president-vice-president-contest"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "franz-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "harris-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "bargmann-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "abcock-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "steel-loy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "sharp-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "alexander-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "lee-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "kennedy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "teller-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "ward-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "murphy-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "newman-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "york-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "chandler-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-governor", "vote": 0}], "object_id": "ozark-governor"}, {"ballot_selections": [{"is_placeholder_selection": false, "object_id": "soliz-selection", "vote": 1}, {"is_placeholder_selection": false, "object_id": "keller-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "rangel-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "argent-selection", "vote": 0}, {"is_placeholder_selection": false, "object_id": "write-in-selection-us-congress-district-5", "vote": 0}], "object_id": "congress-district-5-contest"}], "object_id": "ballot-1663e3e4-e95f-11eb-bd0c-acde48001122", "style_id": "congress-district-5-lacroix"} \ No newline at end of file diff --git a/app/main.py b/app/main.py deleted file mode 100644 index 2585434..0000000 --- a/app/main.py +++ /dev/null @@ -1,120 +0,0 @@ -import logging.config -from typing import Optional -from fastapi import FastAPI, HTTPException -from starlette.middleware.cors import CORSMiddleware -from app.api.v1.models.auth import AuthenticationCredential - - -from app.api.v1.routes import get_v1_routes -from app.api.v1_1.routes import get_v1_1_routes -from app.core.settings import Settings -from app.core.scheduler import get_scheduler - -from app.api.v1.models import UserInfo, UserScope -from app.core import AuthenticationContext, set_auth_credential, set_user_info - -# setup loggers -logging.basicConfig( - level="DEBUG", - format="%(asctime)s %(levelname)-8s %(funcName)s() L%(lineno)-4d %(message)s", -) -logger = logging.getLogger(__name__) - - -def seed_default_user(settings: Settings = Settings()) -> None: - # TODO: a more secure way to set the default auth credential - hashed_password = AuthenticationContext(settings).get_password_hash( - settings.DEFAULT_ADMIN_PASSWORD - ) - credential = AuthenticationCredential( - username=settings.DEFAULT_ADMIN_USERNAME, hashed_password=hashed_password - ) - user_info = UserInfo( - username=credential.username, - first_name=credential.username, - last_name=credential.username, - scopes=[UserScope.admin], - ) - try: - set_auth_credential(credential, settings) - except HTTPException: - pass - - try: - set_user_info(user_info, settings) - except HTTPException: - pass - - -def get_app(settings: Optional[Settings] = None) -> FastAPI: - if not settings: - settings = Settings() - - web_app = FastAPI( - title=settings.PROJECT_NAME, - openapi_url=f"{settings.API_V1_STR}/openapi.json", - version="1.0.5", - ) - - web_app.state.settings = settings - - logger.info(f"Starting API in {web_app.state.settings.API_MODE} mode") - - # Set all CORS enabled origins - if settings.BACKEND_CORS_ORIGINS: - web_app.add_middleware( - CORSMiddleware, - allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - - seed_default_user(settings) - - v1_routes = get_v1_routes(settings) - web_app.include_router(v1_routes, prefix=settings.API_V1_STR) - v1_1_routes = get_v1_1_routes(settings) - web_app.include_router(v1_1_routes, prefix=settings.API_V1_1_STR) - - return web_app - - -app = get_app() - - -@app.on_event("startup") -def on_startup() -> None: - pass - - -@app.on_event("shutdown") -def on_shutdown() -> None: - # Ensure a clean shutdown of the singleton Scheduler - scheduler = get_scheduler() - scheduler.close() - - -if __name__ == "__main__": - # IMPORTANT: This should only be used to debug the application. - # For normal execution, run `make start`. - # - # To make this work, the PYTHONPATH must be set to the root directory, e.g. - # `PYTHONPATH=. poetry run python ./app/main.py` - # See the VSCode launch configuration for detail. - import uvicorn - import argparse - - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - parser.add_argument( - "-p", - "--port", - default=8000, - type=int, - help="The port to listen on", - ) - args = parser.parse_args() - - uvicorn.run(app, host="0.0.0.0", port=args.port) diff --git a/css/theme.css b/css/theme.css new file mode 100644 index 0000000..bb00f2f --- /dev/null +++ b/css/theme.css @@ -0,0 +1,14 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/rtfd/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 0.4.1 | MIT license */ + /* Built 20180727 10:07 */ + *{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.3576515979%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.3576515979%;width:48.821174201%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.3576515979%;width:31.7615656014%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{position:absolute;content:"";display:block;left:0;top:0;width:36px;height:12px;border-radius:4px;background:#ccc;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27AE60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:.3em;display:block}.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:before,.wy-breadcrumbs:after{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin-bottom:0;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a{color:#404040}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980B9;text-align:center;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:gray}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:gray}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{width:100%}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1100px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;display:block;overflow:auto}.rst-content pre.literal-block,.rst-content div[class^='highlight']{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px 0}.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{padding:0px;border:none;margin:0}.rst-content div[class^='highlight'] td.code{width:100%}.rst-content .linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;display:block;overflow:auto}.rst-content div[class^='highlight'] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:12px;line-height:1.4}@media print{.rst-content .codeblock,.rst-content div[class^='highlight'],.rst-content div[class^='highlight'] pre{white-space:pre-wrap}}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .section ol p:last-child,.rst-content .section ul p:last-child{margin-bottom:24px}.rst-content .line-block{margin-left:0px;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink{visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after{content:"";font-family:FontAwesome}.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after{visibility:visible}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:baseline;position:relative;top:-0.4em;line-height:0;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:gray}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}.rst-content table.docutils td .last,.rst-content table.docutils td .last :last-child{margin-bottom:0}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content tt,.rst-content tt,.rst-content code{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content pre,.rst-content kbd,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold;margin-bottom:12px}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-regular.eot");src:url("../fonts/Lato/lato-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-regular.woff2") format("woff2"),url("../fonts/Lato/lato-regular.woff") format("woff"),url("../fonts/Lato/lato-regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bold.eot");src:url("../fonts/Lato/lato-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bold.woff2") format("woff2"),url("../fonts/Lato/lato-bold.woff") format("woff"),url("../fonts/Lato/lato-bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bolditalic.eot");src:url("../fonts/Lato/lato-bolditalic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bolditalic.woff2") format("woff2"),url("../fonts/Lato/lato-bolditalic.woff") format("woff"),url("../fonts/Lato/lato-bolditalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-italic.eot");src:url("../fonts/Lato/lato-italic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-italic.woff2") format("woff2"),url("../fonts/Lato/lato-italic.woff") format("woff"),url("../fonts/Lato/lato-italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:url("../fonts/RobotoSlab/roboto-slab.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.ttf") format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.ttf") format("truetype")} diff --git a/css/theme_extra.css b/css/theme_extra.css new file mode 100644 index 0000000..9cb7579 --- /dev/null +++ b/css/theme_extra.css @@ -0,0 +1,140 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; /* csslint allow: box-model */ + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid wierdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; /* csslint allow: important */ + border-collapse: collapse; +} diff --git a/docker-compose.azure.yml b/docker-compose.azure.yml deleted file mode 100644 index d5083d3..0000000 --- a/docker-compose.azure.yml +++ /dev/null @@ -1,44 +0,0 @@ -version: "3.8" -services: - mediator: - build: - context: . - target: prod - image: deploydemoregistry.azurecr.io/electionguard-api-python:latest - container_name: 'electionguard-api-python-mediator' - ports: - - 8000:8000 - environment: - API_MODE: "mediator" - QUEUE_MODE: "remote" - STORAGE_MODE: "mongo" - PROJECT_NAME: "ElectionGuard Mediator API" - PORT: 8000 - MESSAGEQUEUE_URI: "amqp://guest:guest@electionguard-message-queue:5672" - MONGODB_URI: "mongodb://username:@electionguard-demo.mongo.cosmos.azure.com:10255" - - guardian: - build: - context: . - target: prod - image: deploydemoregistry.azurecr.io/electionguard-api-python:latest - container_name: 'electionguard-api-python-guardian' - ports: - - 8001:8001 - environment: - API_MODE: "guardian" - QUEUE_MODE: "remote" - STORAGE_MODE: "mongo" - PROJECT_NAME: "ElectionGuard Guardian API" - PORT: 8001 - - messagequeue: - image: rabbitmq:3.8.16-management-alpine - container_name: 'electionguard-message-queue' - expose: - - 5672 - - 15672 - ports: - - 5672:5672 - - 15672:15672 - diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 128b6c8..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This hosts both APIs locally in parallel -# It mounts the codebase within the container and runs the APIs -# with hot reload enabled. - -version: "3.8" -services: - mediator: - build: - context: . - target: dev - volumes: - - "./app:/app/app" - ports: - - 8000:8000 - environment: - API_MODE: "mediator" - QUEUE_MODE: "remote" - STORAGE_MODE: "mongo" - PROJECT_NAME: "ElectionGuard Mediator API" - PORT: 8000 - MESSAGEQUEUE_URI: "amqp://guest:guest@electionguard-message-queue:5672" - MONGODB_URI: "mongodb://root:example@electionguard-db:27017" - - guardian: - build: - context: . - target: dev - volumes: - - "./app:/app/app" - ports: - - 8001:8001 - environment: - API_MODE: "guardian" - QUEUE_MODE: "remote" - STORAGE_MODE: "mongo" - PROJECT_NAME: "ElectionGuard Guardian API" - PORT: 8001 - MESSAGEQUEUE_URI: "amqp://guest:guest@electionguard-message-queue:5672" - MONGODB_URI: "mongodb://root:example@electionguard-db:27017" diff --git a/docker-compose.support.yml b/docker-compose.support.yml deleted file mode 100644 index 6a91601..0000000 --- a/docker-compose.support.yml +++ /dev/null @@ -1,40 +0,0 @@ -# This hosts both APIs locally in parallel -# It mounts the codebase within the container and runs the APIs -# with hot reload enabled. - -version: "3.8" -services: - messagequeue: - image: rabbitmq:3.8.16-management-alpine - container_name: "electionguard-message-queue" - expose: - - 5672 - - 15672 - ports: - - 5672:5672 - - 15672:15672 - - mongo: - image: mongo - container_name: "electionguard-db" - restart: always - ports: - - 27017:27017 - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: example - MONGO_INITDB_DATABASE: BallotData - volumes: - - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro - - mongo-express: - image: mongo-express - restart: always - ports: - - 8181:8081 - environment: - ME_CONFIG_MONGODB_SERVER: electionguard-db - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: example - links: - - mongo diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 268329e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: "3.8" -services: - mediator: - build: - context: . - target: prod - ports: - - 8000:8000 - environment: - API_MODE: "mediator" - PROJECT_NAME: "ElectionGuard Mediator API" - PORT: 8000 - - guardian: - build: - context: . - target: prod - ports: - - 8001:8001 - environment: - API_MODE: "guardian" - PROJECT_NAME: "ElectionGuard Guardian API" - PORT: 8001 diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index a984250..0000000 --- a/docs/index.md +++ /dev/null @@ -1,149 +0,0 @@ -![Microsoft Defending Democracy Program: ElectionGuard](https://raw.githubusercontent.com/microsoft/electionguard-web-api/main/images/electionguard-banner.svg) - -# 🗳️ ElectionGuard Web API - -![docker version](https://img.shields.io/docker/v/electionguard/electionguard-web-api) ![docker pulls](https://img.shields.io/docker/pulls/electionguard/electionguard-web-api) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/microsoft/electionguard-web-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-web-api/context:python) [![Total alerts](https://img.shields.io/lgtm/alerts/g/microsoft/electionguard-web-api.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/microsoft/electionguard-web-api/alerts/) [![Documentation Status](https://readthedocs.org/projects/electionguard-web-api/badge/?version=latest)](https://electionguard-web-api.readthedocs.io) [![license](https://img.shields.io/github/license/microsoft/electionguard-web-api)](LICENSE) - -This is a python API that provides a **thin**, **stateless** wrapper around the [`electionguard-python`](https://github.com/microsoft/electionguard-python) library to perform ballot encryption, casting, spoiling, and tallying. This API is implemented using [FastAPI](https://fastapi.tiangolo.com/#interactive-api-docs). - -If you aren't familiar with ElectionGuard and its concepts, [take a stroll through the official documentation](https://microsoft.github.io/electionguard-python/) first! - -## 👯‍♀️ Two APIs in One - -The application can run in one of two modes: - -- `guardian` mode runs features used by Guardians (key ceremony actions, partial tally decryption, etc.) -- `mediator` mode runs features used by Mediators (ballot encryption, casting, spoiling, etc.) - -In practice, you will likely need to run at least one instance of each mode. We provide a single codebase and Docker image, but the mode can be set at runtime. - -## 💻 Requirements - -> NOTE:
-> 🐳 = required for running with Docker.
-> 🐍 = required for running with Python. - -- 🐳🐍 [GNU Make](https://www.gnu.org/software/make/manual/make.html) is used to simplify the commands and GitHub Actions. This approach is recommended to simplify the command line experience. This is built in for MacOS and Linux. For Windows, setup is simpler with [Chocolatey](https://chocolatey.org/install) and installing the provided [make package](https://chocolatey.org/packages/make). The other Windows option is [manually installing make](http://gnuwin32.sourceforge.net/packages/make.htm). -- 🐍 [Python 3.8](https://www.python.org/downloads/) is **required** to develop this API. If developer uses multiple versions of python, [pyenv](https://github.com/pyenv/pyenv) is suggested to assist version management. - -## 🐳 Running with Docker - -### The official Docker image - -We host a Docker image on both [Github Packages](https://github.com/microsoft/electionguard-web-api/packages/397920) and [DockerHub](https://hub.docker.com/r/electionguard/electionguard-web-api). - -**Note:** _GitHub Packages requires authentication to retrieve the package. This requires a GitHub Access Token and using `docker login`. [Follow GitHub instructions](https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-with-a-personal-access-token)._ - -```bash -# Pull the image from DockerHub -docker pull docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main - -# Start a container for the API in mediator mode, exposed on port 80 of the host machine -docker run -d -p 80:8000 --env API_MODE=mediator docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main -``` - -OR - -```bash -# Pull the image from DockerHub -docker pull electionguard/electionguard-web-api:latest - -# Start a container for the API in mediator mode, exposed on port 80 of the host machine -docker run -d -p 80:8000 --env API_MODE=mediator electionguard/electionguard-web-api:latest -``` - -### Developing locally with Docker - -If you run Docker and want to run the code locally without Python dependencies, we provide a Dockerfile and docker-compose.yml. - -Run both APIs at the same time: - -```bash -make docker-run -``` - -Or run both APIs in development mode, with automatic reloading on file change: - -```bash -make docker-dev -``` - -After either command, you will find the `mediator` API running at http://127.0.0.1:8000 and the `guardian` API at http://127.0.0.1:8001 - -## 🐍 Running with Python - -### Quick Start - -Using [**make**](https://www.gnu.org/software/make/manual/make.html), installation and startup can be run with one command: - -To set up the environment: - -```bash -make environment -``` - -To start the api: - -```bash -make start API_MODE=mediator -``` - -OR - -```bash -make start API_MODE=guardian -``` - -### Debugging - -For local debugging with Visual Studio Code, choose the `Guardian Web API` or `Mediator Web API` options from the dropdown in the Run menu. Once the server is up, you can easily hit your breakpoints. - -If the code fails to run, [make sure your Python interpreter is set](https://code.visualstudio.com/docs/python/environments) to use your poetry environment. - -## 🧪 Testing - -A Postman collection is available to test the API located in the `/tests` folder. You can do a few things with this: - -- [Import into Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/#importing-data-into-postman) for easy manual testing. -- Run locally with the [Newman CLI](https://github.com/postmanlabs/newman). -- Run the APIs and tests entirely in Docker by running: - ```bash - make docker-test - ``` - -## 📝 Documentation - -**FastApi** defaultly has API documentation built in. The following is available after running: - -- **[SwaggerUI](https://github.com/swagger-api/swagger-ui)** at [`http://127.0.0.1:8000/docs`](http://127.0.0.1:8000/docs) or [`http://127.0.0.1:8001/docs`](http://127.0.0.1:8001/docs), depending on the API mode - -- **[ReDoc](https://github.com/Redocly/redoc)** at [`http://127.0.0.1:8000/redoc`](http://127.0.0.1:8000/redoc) or [`http://127.0.0.1:8001/redoc`](http://127.0.0.1:8001/redoc) - -Overviews of the API itself are available on: - -- [GitHub Pages](https://microsoft.github.io/electionguard-web-api/) -- [Read the Docs](https://electionguard-web-api.readthedocs.io/) - -## 🗄 Archived - -As of 06/15/2020, the previous C wrapped implementation was transitioned to the python version. ElectionGuard development has transitioned to the [ElectionGuard-Python](https://github.com/microsoft/electionguard-python) Repo. The old version is available using the `dotnet-api` tag. - -## 🤝 Contributing - -This project encourages community contributions for development, testing, documentation, code review, and performance analysis, etc. For more information on how to contribute, see [the contribution guidelines][contributing] - -### Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -### Reporting Issues - -Please report any bugs, feature requests, or enhancements using the [GitHub Issue Tracker](https://github.com/microsoft/electionguard-web-api/issues). Please do not report any security vulnerabilities using the Issue Tracker. Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). See the [Security Documentation][security] for more information. - -### Have Questions? - -Electionguard would love for you to ask questions out in the open using GitHub Issues. If you really want to email the ElectionGuard team, reach out at electionguard@microsoft.com. - -## License - -This repository is licensed under the [MIT License] diff --git a/fonts/Lato/lato-bold.eot b/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000..3361183 Binary files /dev/null and b/fonts/Lato/lato-bold.eot differ diff --git a/fonts/Lato/lato-bold.ttf b/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000..29f691d Binary files /dev/null and b/fonts/Lato/lato-bold.ttf differ diff --git a/fonts/Lato/lato-bold.woff b/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/fonts/Lato/lato-bold.woff differ diff --git a/fonts/Lato/lato-bold.woff2 b/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/fonts/Lato/lato-bold.woff2 differ diff --git a/fonts/Lato/lato-bolditalic.eot b/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000..3d41549 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.eot differ diff --git a/fonts/Lato/lato-bolditalic.ttf b/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000..f402040 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.ttf differ diff --git a/fonts/Lato/lato-bolditalic.woff b/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/fonts/Lato/lato-bolditalic.woff differ diff --git a/fonts/Lato/lato-bolditalic.woff2 b/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/fonts/Lato/lato-italic.eot b/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000..3f82642 Binary files /dev/null and b/fonts/Lato/lato-italic.eot differ diff --git a/fonts/Lato/lato-italic.ttf b/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000..b4bfc9b Binary files /dev/null and b/fonts/Lato/lato-italic.ttf differ diff --git a/fonts/Lato/lato-italic.woff b/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/fonts/Lato/lato-italic.woff differ diff --git a/fonts/Lato/lato-italic.woff2 b/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/fonts/Lato/lato-italic.woff2 differ diff --git a/fonts/Lato/lato-regular.eot b/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000..11e3f2a Binary files /dev/null and b/fonts/Lato/lato-regular.eot differ diff --git a/fonts/Lato/lato-regular.ttf b/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000..74decd9 Binary files /dev/null and b/fonts/Lato/lato-regular.ttf differ diff --git a/fonts/Lato/lato-regular.woff b/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/fonts/Lato/lato-regular.woff differ diff --git a/fonts/Lato/lato-regular.woff2 b/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/fonts/Lato/lato-regular.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000..79dc8ef Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000..df5d1df Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000..2f7ca78 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000..eb52a79 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/fonts/RobotoSlab/roboto-slab.eot b/fonts/RobotoSlab/roboto-slab.eot new file mode 100644 index 0000000..2f7ca78 Binary files /dev/null and b/fonts/RobotoSlab/roboto-slab.eot differ diff --git a/fonts/fontawesome-webfont.eot b/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/fonts/fontawesome-webfont.eot differ diff --git a/fonts/fontawesome-webfont.svg b/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fonts/fontawesome-webfont.ttf b/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/fonts/fontawesome-webfont.ttf differ diff --git a/fonts/fontawesome-webfont.woff b/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/fonts/fontawesome-webfont.woff differ diff --git a/fonts/fontawesome-webfont.woff2 b/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/fonts/fontawesome-webfont.woff2 differ diff --git a/images/electionguard-banner.svg b/images/electionguard-banner.svg deleted file mode 100644 index 58a381c..0000000 --- a/images/electionguard-banner.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/images/electionguard-logo-large.png b/images/electionguard-logo-large.png deleted file mode 100644 index a81a1cb..0000000 Binary files a/images/electionguard-logo-large.png and /dev/null differ diff --git a/images/electionguard-logo-small.png b/images/electionguard-logo-small.png deleted file mode 100644 index 12cd09f..0000000 Binary files a/images/electionguard-logo-small.png and /dev/null differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000..e85006a Binary files /dev/null and b/img/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..2516c28 --- /dev/null +++ b/index.html @@ -0,0 +1,277 @@ + + + + + + + + + + + + ElectionGuard Web Api + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + +
+
+
+ + +
+
+
+
+ +

Microsoft Defending Democracy Program: ElectionGuard

+

🗳️ ElectionGuard Web API

+

docker version docker pulls Language grade: Python Total alerts Documentation Status license

+

This is a python API that provides a thin, stateless wrapper around the electionguard-python library to perform ballot encryption, casting, spoiling, and tallying. This API is implemented using FastAPI.

+

If you aren't familiar with ElectionGuard and its concepts, take a stroll through the official documentation first!

+

👯‍♀️ Two APIs in One

+

The application can run in one of two modes:

+
    +
  • guardian mode runs features used by Guardians (key ceremony actions, partial tally decryption, etc.)
  • +
  • mediator mode runs features used by Mediators (ballot encryption, casting, spoiling, etc.)
  • +
+

In practice, you will likely need to run at least one instance of each mode. We provide a single codebase and Docker image, but the mode can be set at runtime.

+

💻 Requirements

+
+

NOTE:
+🐳 = required for running with Docker.
+🐍 = required for running with Python.

+
+
    +
  • 🐳🐍 GNU Make is used to simplify the commands and GitHub Actions. This approach is recommended to simplify the command line experience. This is built in for MacOS and Linux. For Windows, setup is simpler with Chocolatey and installing the provided make package. The other Windows option is manually installing make.
  • +
  • 🐍 Python 3.8 is required to develop this API. If developer uses multiple versions of python, pyenv is suggested to assist version management.
  • +
+

🐳 Running with Docker

+

The official Docker image

+

We host a Docker image on both Github Packages and DockerHub.

+

Note: GitHub Packages requires authentication to retrieve the package. This requires a GitHub Access Token and using docker login. Follow GitHub instructions.

+
# Pull the image from DockerHub
+docker pull docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main
+
+# Start a container for the API in mediator mode, exposed on port 80 of the host machine
+docker run -d -p 80:8000 --env API_MODE=mediator docker.pkg.github.com/microsoft/electionguard-web-api/electionguard-web-api:main
+
+

OR

+
# Pull the image from DockerHub
+docker pull electionguard/electionguard-web-api:latest
+
+# Start a container for the API in mediator mode, exposed on port 80 of the host machine
+docker run -d -p 80:8000 --env API_MODE=mediator electionguard/electionguard-web-api:latest
+
+

Developing locally with Docker

+

If you run Docker and want to run the code locally without Python dependencies, we provide a Dockerfile and docker-compose.yml.

+

Run both APIs at the same time:

+
make docker-run
+
+

Or run both APIs in development mode, with automatic reloading on file change:

+
make docker-dev
+
+

After either command, you will find the mediator API running at http://127.0.0.1:8000 and the guardian API at http://127.0.0.1:8001

+

🐍 Running with Python

+

Quick Start

+

Using make, installation and startup can be run with one command:

+

To set up the environment:

+
make environment
+
+

To start the api:

+
make start API_MODE=mediator
+
+

OR

+
make start API_MODE=guardian
+
+

Debugging

+

For local debugging with Visual Studio Code, choose the Guardian Web API or Mediator Web API options from the dropdown in the Run menu. Once the server is up, you can easily hit your breakpoints.

+

If the code fails to run, make sure your Python interpreter is set to use your poetry environment.

+

🧪 Testing

+

A Postman collection is available to test the API located in the /tests folder. You can do a few things with this:

+
    +
  • Import into Postman for easy manual testing.
  • +
  • Run locally with the Newman CLI.
  • +
  • Run the APIs and tests entirely in Docker by running: + bash + make docker-test
  • +
+

📝 Documentation

+

FastApi defaultly has API documentation built in. The following is available after running:

+ +

Overviews of the API itself are available on:

+ +

🗄 Archived

+

As of 06/15/2020, the previous C wrapped implementation was transitioned to the python version. ElectionGuard development has transitioned to the ElectionGuard-Python Repo. The old version is available using the dotnet-api tag.

+

🤝 Contributing

+

This project encourages community contributions for development, testing, documentation, code review, and performance analysis, etc. For more information on how to contribute, see [the contribution guidelines][contributing]

+

Code of Conduct

+

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

+

Reporting Issues

+

Please report any bugs, feature requests, or enhancements using the GitHub Issue Tracker. Please do not report any security vulnerabilities using the Issue Tracker. Instead, please report them to the Microsoft Security Response Center (MSRC) at https://msrc.microsoft.com/create-report. See the [Security Documentation][security] for more information.

+

Have Questions?

+

Electionguard would love for you to ask questions out in the open using GitHub Issues. If you really want to email the ElectionGuard team, reach out at electionguard@microsoft.com.

+

License

+

This repository is licensed under the [MIT License]

+ +
+
+ + +
+
+ +
+ +
+ +
+ + + GitHub + + + + +
+ + + + + + + + + diff --git a/js/jquery-2.1.1.min.js b/js/jquery-2.1.1.min.js new file mode 100644 index 0000000..e5ace11 --- /dev/null +++ b/js/jquery-2.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.1",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+Math.random()}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b) +},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,bb=/<([\w:]+)/,cb=/<|&#?\w+;/,db=/<(?:script|style|link)/i,eb=/checked\s*(?:[^=]|=\s*.checked.)/i,fb=/^$|\/(?:java|ecma)script/i,gb=/^true\/(.*)/,hb=/^\s*\s*$/g,ib={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ib.optgroup=ib.option,ib.tbody=ib.tfoot=ib.colgroup=ib.caption=ib.thead,ib.th=ib.td;function jb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function kb(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function lb(a){var b=gb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function mb(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function nb(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function ob(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pb(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=ob(h),f=ob(a),d=0,e=f.length;e>d;d++)pb(f[d],g[d]);if(b)if(c)for(f=f||ob(a),g=g||ob(h),d=0,e=f.length;e>d;d++)nb(f[d],g[d]);else nb(a,h);return g=ob(h,"script"),g.length>0&&mb(g,!i&&ob(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(cb.test(e)){f=f||k.appendChild(b.createElement("div")),g=(bb.exec(e)||["",""])[1].toLowerCase(),h=ib[g]||ib._default,f.innerHTML=h[1]+e.replace(ab,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=ob(k.appendChild(e),"script"),i&&mb(f),c)){j=0;while(e=f[j++])fb.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=jb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(ob(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&mb(ob(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(ob(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ab,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ob(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(ob(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&eb.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(ob(c,"script"),kb),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,ob(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,lb),j=0;g>j;j++)h=f[j],fb.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(hb,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qb,rb={};function sb(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function tb(a){var b=l,c=rb[a];return c||(c=sb(a,b),"none"!==c&&c||(qb=(qb||n("