👔🙈📝 Add support for persistency

This commit is contained in:
Brian Wiborg 2023-04-03 13:38:09 +02:00
parent 4eeff41537
commit f32a78aba3
No known key found for this signature in database
GPG Key ID: BE53FA9286B719D6
5 changed files with 214 additions and 5 deletions

3
.gitignore vendored
View File

@ -593,4 +593,5 @@ FodyWeavers.xsd
# Additional files built by Visual Studio # Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudio,vim # End of https://www.toptal.com/developers/gitignore/api/python,visualstudio,vim
n
.voting.db

View File

@ -1,4 +1,3 @@
# ToDos # ToDos
- add `click` and provide a user-friendly CLI
- add `fastapi` and implement a builtin JSON/REST API server - add `fastapi` and implement a builtin JSON/REST API server

103
poetry.lock generated
View File

@ -57,11 +57,26 @@ files = [
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
] ]
[[package]]
name = "click"
version = "8.1.3"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev" category = "main"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [ files = [
@ -69,6 +84,21 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "commonmark"
version = "0.9.1"
description = "Python parser for the CommonMark Markdown spec"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
]
[package.extras]
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
[[package]] [[package]]
name = "decorator" name = "decorator"
version = "5.1.1" version = "5.1.1"
@ -275,7 +305,7 @@ tests = ["pytest"]
name = "pygments" name = "pygments"
version = "2.14.0" version = "2.14.0"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
@ -301,6 +331,49 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]]
name = "rich"
version = "12.6.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.6.3,<4.0.0"
files = [
{file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
{file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
]
[package.dependencies]
commonmark = ">=0.9.0,<0.10.0"
pygments = ">=2.6.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "shellingham"
version = "1.5.0.post1"
description = "Tool to Detect Surrounding Shell"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"},
{file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"},
]
[[package]]
name = "simple-term-menu"
version = "1.6.1"
description = "A Python package which creates simple interactive menus on the command line."
category = "main"
optional = false
python-versions = "~=3.5"
files = [
{file = "simple-term-menu-1.6.1.tar.gz", hash = "sha256:368b4158d1749b868552fb6c054b8301785086c71a7253dac8404cc3cb2d30e8"},
{file = "simple_term_menu-1.6.1-py3-none-any.whl", hash = "sha256:f12945d5c6998088e86a228e0aff12ff655f5bfad786c86677f23faa1d2afa50"},
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@ -349,6 +422,30 @@ files = [
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
[[package]]
name = "typer"
version = "0.7.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"},
{file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"},
]
[package.dependencies]
click = ">=7.1.1,<9.0.0"
colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""}
rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""}
shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""}
[package.extras]
all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"]
doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"]
test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
[[package]] [[package]]
name = "wcwidth" name = "wcwidth"
version = "0.2.6" version = "0.2.6"
@ -364,4 +461,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "55dd02eaecfcfc8f3b3d44c10ea32ececbb37be4676145ca7f133b680cf8a618" content-hash = "052d89cb180066114f48b9864dbbab045ec72ff627b10a55abc05e23a2312650"

View File

@ -8,6 +8,8 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.11"
arrow = "^1.2.3" arrow = "^1.2.3"
typer = {extras = ["all"], version = "^0.7.0"}
simple-term-menu = "^1.6.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
@ -17,3 +19,6 @@ nose2 = "^0.12.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
voting = 'voting.cli.voting:app'

107
voting/storage.py Normal file
View File

@ -0,0 +1,107 @@
import shutil
from arrow import get as arrow_get
from pathlib import Path
from simple_term_menu import TerminalMenu
from typing import List
from voting import Voting
class Storage(object):
"""Storage serves as a storage interface.
It automatically loads `.voting.db` when initialized and creates the file if necessary.
The persisted votings can then be access via the `votings` attribute.
When used with the `with` statement, changes will automatically be saved when leaving the context.
"""
def __init__(self, path: str = ".voting.db"):
self.path: str = path
self.votings: List = []
Path(self.path).touch()
self.load()
self.__changed = False
def __enter__(self):
return self
def __exit__(self, exection_type, exception_value, stacktrace):
if not exection_type:
self.save()
def load(self) -> None:
"""Loads the persisted votings from disk."""
self.votings = []
with open(Path(self.path), 'r') as f:
for line in f.readlines():
if line:
self.votings.append(Voting.loads(line))
def save(self) -> None:
"""Saves the votings in memory to disk.
The file is only ever overwritten if changes have actually occured.
A backup file is created before saving called `.voting.db.bak`.
"""
if not self.__changed:
self.__changed = False
return
shutil.copyfile(".voting.db", ".voting.db.bak", follow_symlinks=True) # kein backup: kein mitleid!
lines = []
for voting in self.votings:
lines.append(voting.dumps())
with open(Path(self.path), 'w') as f:
for line in lines:
f.write(f'{line}\n')
def push(self, voting: Voting) -> None:
"""Push a voting to the in-memory voting list."""
self.votings.append(voting)
def pop(self, title: str) -> Voting:
"""Pop a voting from the in-memory voting list.
If you call `save` after popping, the popped voting will effectively have been deleted from disk.
In order to update a voting, firsr pop it, then manipulate it and push it back to the storage before calling `save`.
In case the title is ambiguous, an interactive ASCII menu will be rendered for selection.
"""
candidates = []
for v in self.votings:
if v.title == title:
candidates.append(v)
if len(candidates) == 0:
return None
selection = 0
if len(candidates) > 1:
menu = TerminalMenu(
[
f"[{i}] - {c.title} ({arrow_get(c.start).format('YYYY-MM-DD HH:mm:ss')})"
for i, c in enumerate(candidates)
],
title="Candidates"
)
selection = menu.show()
if selection is None:
return None
counter = 0
votings = []
popped = None
for v in self.votings:
if v.title == title:
if counter == selection:
popped = v
else:
votings.append(v)
counter += 1
self.votings = votings
self.__changed = True
return popped