👔🙈📝 Add support for persistency
This commit is contained in:
parent
4eeff41537
commit
f32a78aba3
5 changed files with 214 additions and 5 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
1
TODO.md
1
TODO.md
|
@ -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
103
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
107
voting/storage.py
Normal 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
|
Loading…
Reference in a new issue