🎉 Let the voting begin.

This commit is contained in:
Brian Wiborg 2023-04-02 16:31:25 +02:00
commit 2e195011c0
No known key found for this signature in database
GPG Key ID: BE53FA9286B719D6
5 changed files with 1266 additions and 0 deletions

596
.gitignore vendored Normal file
View File

@ -0,0 +1,596 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudio,vim
# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudio,vim
### Python ###
# 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
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__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/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudio,vim
n

367
poetry.lock generated Normal file
View File

@ -0,0 +1,367 @@
# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand.
[[package]]
name = "appnope"
version = "0.1.3"
description = "Disable App Nap on macOS >= 10.9"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
{file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
]
[[package]]
name = "arrow"
version = "1.2.3"
description = "Better dates & times for Python"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"},
{file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"},
]
[package.dependencies]
python-dateutil = ">=2.7.0"
[[package]]
name = "asttokens"
version = "2.2.1"
description = "Annotate AST trees with source code positions"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"},
{file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"},
]
[package.dependencies]
six = "*"
[package.extras]
test = ["astroid", "pytest"]
[[package]]
name = "backcall"
version = "0.2.0"
description = "Specifications for callback functions passed in to an API"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "executing"
version = "1.2.0"
description = "Get the currently executing AST node of a frame, and other information"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"},
{file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"},
]
[package.extras]
tests = ["asttokens", "littleutils", "pytest", "rich"]
[[package]]
name = "ipython"
version = "8.12.0"
description = "IPython: Productive Interactive Computing"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "ipython-8.12.0-py3-none-any.whl", hash = "sha256:1c183bf61b148b00bcebfa5d9b39312733ae97f6dad90d7e9b4d86c8647f498c"},
{file = "ipython-8.12.0.tar.gz", hash = "sha256:a950236df04ad75b5bc7f816f9af3d74dc118fd42f2ff7e80e8e60ca1f182e2d"},
]
[package.dependencies]
appnope = {version = "*", markers = "sys_platform == \"darwin\""}
backcall = "*"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
pickleshare = "*"
prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0"
pygments = ">=2.4.0"
stack-data = "*"
traitlets = ">=5"
[package.extras]
all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"]
black = ["black"]
doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"]
kernel = ["ipykernel"]
nbconvert = ["nbconvert"]
nbformat = ["nbformat"]
notebook = ["ipywidgets", "notebook"]
parallel = ["ipyparallel"]
qtconsole = ["qtconsole"]
test = ["pytest (<7.1)", "pytest-asyncio", "testpath"]
test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"]
[[package]]
name = "jedi"
version = "0.18.2"
description = "An autocompletion tool for Python that can be used for text editors."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "jedi-0.18.2-py2.py3-none-any.whl", hash = "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e"},
{file = "jedi-0.18.2.tar.gz", hash = "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612"},
]
[package.dependencies]
parso = ">=0.8.0,<0.9.0"
[package.extras]
docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]]
name = "matplotlib-inline"
version = "0.1.6"
description = "Inline Matplotlib backend for Jupyter"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
{file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
{file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
]
[package.dependencies]
traitlets = "*"
[[package]]
name = "nose2"
version = "0.12.0"
description = "unittest2 with plugins, the successor to nose"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "nose2-0.12.0-py2.py3-none-any.whl", hash = "sha256:da7eb5e3cbe2abb693a053e17b4fbefca98ea9ea79fc729b0b0f41e8b4196304"},
{file = "nose2-0.12.0.tar.gz", hash = "sha256:956e79b9bd558ee08b6200c05ad2c76465b7e3860c0c0537686089285c320113"},
]
[package.extras]
coverage-plugin = ["coverage"]
dev = ["Sphinx", "mock", "sphinx-issues", "sphinx-rtd-theme"]
[[package]]
name = "parso"
version = "0.8.3"
description = "A Python Parser"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
{file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
]
[package.extras]
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["docopt", "pytest (<6.0.0)"]
[[package]]
name = "pexpect"
version = "4.8.0"
description = "Pexpect allows easy control of interactive console applications."
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
]
[package.dependencies]
ptyprocess = ">=0.5"
[[package]]
name = "pickleshare"
version = "0.7.5"
description = "Tiny 'shelve'-like database with concurrency support"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
]
[[package]]
name = "prompt-toolkit"
version = "3.0.38"
description = "Library for building powerful interactive command lines in Python"
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"},
{file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
]
[[package]]
name = "pure-eval"
version = "0.2.2"
description = "Safely evaluate AST nodes without side effects"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
]
[package.extras]
tests = ["pytest"]
[[package]]
name = "pygments"
version = "2.14.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
{file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
]
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
[package.dependencies]
six = ">=1.5"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "stack-data"
version = "0.6.2"
description = "Extract data from python stack frames and tracebacks for informative displays"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"},
{file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"},
]
[package.dependencies]
asttokens = ">=2.1.0"
executing = ">=1.2.0"
pure-eval = "*"
[package.extras]
tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"]
[[package]]
name = "traitlets"
version = "5.9.0"
description = "Traitlets Python configuration system"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
{file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
]
[package.extras]
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
[[package]]
name = "wcwidth"
version = "0.2.6"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
{file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "55dd02eaecfcfc8f3b3d44c10ea32ececbb37be4676145ca7f133b680cf8a618"

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[tool.poetry]
name = "voting"
version = "0.1.0"
description = ""
authors = ["Brian Wiborg <baccenfutter@c-base.org>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
arrow = "^1.2.3"
[tool.poetry.group.dev.dependencies]
ipython = "^8.12.0"
nose2 = "^0.12.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

107
voting/test_voting.py Normal file
View File

@ -0,0 +1,107 @@
import unittest
import voting as V
from voting import QuorumKind, Quorum, Vote, Voting
voters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
class TestAtLeastOneVoter(unittest.TestCase):
def test_no_voters(self):
self.assertRaises(RuntimeError, Voting,
title="Test",
quorum=QuorumKind.NONE,
voters=[],
)
def test_one_voter(self):
Voting(title="Test", quorum=Quorum(), voters=['a'])
class TestNoVotesBeforeStart(unittest.TestCase):
def setUp(self) -> None:
self.voting = Voting(title="Test", quorum=Quorum(), voters=voters)
def test_vote_before_started(self):
self.assertRaises(RuntimeError, self.voting.vote, voter='a', vote=Vote.YES)
def test_vote_after_started(self):
self.voting.start()
self.voting.vote(voter='a', vote=Vote.YES)
class TestQuorumSanity(unittest.TestCase):
def test_invalid_absolute_values(self):
self.assertRaises(ValueError, Quorum, kind=QuorumKind.ABSOLUTE, value=0)
self.assertRaises(ValueError, Quorum, kind=QuorumKind.ABSOLUTE, value=-1)
def test_valid_absolute_values(self):
Quorum(kind=QuorumKind.ABSOLUTE, value=1)
def test_invalid_percent_values(self):
self.assertRaises(ValueError, Quorum, kind=QuorumKind.PERCENT, value=0)
self.assertRaises(ValueError, Quorum, kind=QuorumKind.PERCENT, value=-1)
self.assertRaises(ValueError, Quorum, kind=QuorumKind.PERCENT, value=100.1)
def test_valid_percent_values(self):
Quorum(kind=QuorumKind.PERCENT, value=0.1)
Quorum(kind=QuorumKind.PERCENT, value=100)
class TestVoteMustBeLegal(unittest.TestCase):
def setUp(self) -> None:
self.voting = Voting(title="Test", quorum=Quorum(), voters=voters)
self.voting.start()
def test_unknown_voter(self):
self.assertRaises(ValueError, self.voting.vote, voter='z', vote=Vote.YES)
def test_known_voter(self):
self.voting.vote('a', Vote.YES)
class TestResultAbsoluteQuorum(unittest.TestCase):
def setUp(self) -> None:
def new_voting():
v = Voting(
title="Test",
quorum=Quorum(kind=QuorumKind.ABSOLUTE, value=1),
voters=voters,
)
v.start()
return v
self.new_voting = new_voting
def test_quorum(self):
v = self.new_voting()
assert not v.result().quorum_reached
v.vote(voter='a', vote=Vote.YES)
assert v.result().quorum_reached
class TestResultPercentQuorum(unittest.TestCase):
def setUp(self) -> None:
def new_voting():
v = Voting(
title="Test",
quorum=Quorum(kind=QuorumKind.PERCENT, value=15),
voters=voters,
)
v.start()
return v
self.new_voting = new_voting
def test_quorum(self):
v = self.new_voting()
assert not v.result().quorum_reached
v.vote('a', Vote.YES)
assert not v.result().quorum_reached
v.vote('b', Vote.YES)
assert v.result().quorum_reached
if __name__ == '__main__':
unittest.main()

177
voting/voting.py Normal file
View File

@ -0,0 +1,177 @@
"""Voting - A Python module for votings on the c-base space-station."""
__AUTHOR__ = 'Brian Wiborg <baccenfutter@c-base.org>'
__LICENSE__ = 'MIT'
from arrow import Arrow
from enum import Enum
from typing import Dict, List, Union
TIMEFRAME_IN_DAYS: int = 7
def timeframe_is_over(started: Arrow) -> bool:
"""Check if the current time is less than the given timestamp plus TIMEFRAME_IN_DAYS."""
return Arrow.utcnow() > started.shift(days=TIMEFRAME_IN_DAYS)
class QuorumKind(Enum):
"""QuorumKind defines the kind of quorum.
Kinds:
- NONE - there ain't no quorum
- ABSOLUTE - absolute number of minimum voters is required
- PERCENT - minimum percentage of valid voters must have voted
"""
NONE = 0
ABSOLUTE = 1
PERCENT = 2
class Quorum(object):
"""Quorm defines the required quorum of a Voting.
Absolute quorums must be greater than 0.
Percent quorums must be greather than 0.0 and less then or equal to 100.0.
"""
def __init__(self, kind: QuorumKind = QuorumKind.NONE, value: Union[float, int, None] = None):
self.__kind: QuorumKind = kind
self.__value: Union[float, int, None] = None
if kind == QuorumKind.ABSOLUTE:
self.__value = int(value)
if self.__value <= 0:
raise ValueError(f"Value can not be less than or equal to zero: {self.__value}")
elif kind == QuorumKind.PERCENT:
self.__value = float(value)
if self.__value <= 0.0 or self.__value > 100.0:
raise ValueError(f"Value must be greater than 0.0 and less then or equal to 100.0: {self.__value}")
@property
def kind(self) -> bool:
return self.__kind
@property
def value(self) -> Union[float, int]:
return self.__value
class Vote(Enum):
"""Vote represents a vote by a voter."""
YES = 0
NO = 1
ABSTENTION = 2
class Result(object):
"""Result represents a voting result."""
def __init__(self, started: Arrow, quorum: Quorum, voters: int, yes: int, no: int, absent: int):
self.__started: Arrow = started
self.__quroum: Quorum = quorum
self.__voters: int = voters
self.__votes_yes: int = yes
self.__votes_no: int = no
self.__votes_absent: int = absent
def __repr__(self) -> str:
data = {
'STIMMEN': self.__voters,
'JA': self.__votes_yes,
'NEIN': self.__votes_no,
'ENTHALTUNGEN': self.__votes_absent,
}
out = ' | '.join([f"{e}: {data[e]}" for e in data])
if self.status == 'ready':
out += f"\nDamit is der Antrag: {self.outcome}"
return out
@property
def status(self) -> str:
"""Returns the human-readable status of the result."""
is_running = self.__started is not None
is_ready = is_running and timeframe_is_over(self.__started)
out = "not started"
if is_ready:
out = "ready"
elif is_running:
out = "running"
return out
@property
def quorum_reached(self) -> bool:
"""Checks if the quorum has been reached."""
total_votes = sum([self.__votes_yes, self.__votes_no, self.__votes_absent])
if self.__quroum.kind == QuorumKind.NONE:
return True
elif self.__quroum.kind == QuorumKind.ABSOLUTE:
return total_votes >= self.__quroum.value
elif self.__quroum.kind == QuorumKind.PERCENT:
return total_votes * 100 / self.__voters >= self.__quroum.value
else:
raise RuntimeError("BUG! this code should never be reached!")
@property
def outcome(self) -> str:
"""Returns the human-readable outcome."""
return 'ANGENOMMEN' if self.quorum_reached and self.__votes_yes > self.__votes_no else 'ABGELEHNT'
class Voting(object):
"""Voting represents a voting."""
def __init__(self, title: str, quorum: Quorum, voters: List[str]):
if len(voters) == 0:
raise RuntimeError(f"Can not start a voting with 0 voters: {voters}")
self.__title: str = title
self.__quorum: Quorum = quorum
self.__voters: List[str] = voters
self.__start: Union[Arrow, None] = None
self.__votes: Dict[str, Vote] = {}
@property
def title(self) -> str:
return self.__title
@property
def voters(self) -> List[str]:
return self.__voters
def start(self) -> None:
"""Starts the voting."""
if self.__start is not None:
raise RuntimeWarning(f"Voting already running since: {self.__start}")
self.__start = Arrow.utcnow()
def is_running(self) -> bool:
"""Checks if the voting is currently running."""
return self.__start is not None and not timeframe_is_over(self.__start)
def is_over(self) -> bool:
"""Checks if the voting is over."""
return self.is_running() and timeframe_is_over(self.__start)
def vote(self, voter: str, vote: Vote) -> None:
"""Makes a vote.
The voter must be in the list of legal voters.
If a voter votes multiple times, the latest vote wins.
"""
if not self.is_running() or self.is_over():
raise RuntimeError(f"Voting has ended or hasn't even been started! (started: {self.__start})")
if voter not in self.__voters:
raise ValueError(f"{voter} not in {self.__voters}")
self.__votes[voter] = vote
def result(self) -> Result:
"""Obtains the current result for this voting."""
if self.__start is None:
return RuntimeError("Voting hasn't even been started, yet!")
return Result(
quorum=self.__quorum,
started=self.__start,
voters=len(self.__voters),
yes=len([e for e in self.__votes if self.__votes[e] == Vote.YES]),
no=len([e for e in self.__votes if self.__votes[e] == Vote.NO]),
absent=len([e for e in self.__votes if self.__votes[e] == Vote.ABSTENTION]),
)