From f32a78aba3843b5f074c4c1d6394d5b89271909a Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Mon, 3 Apr 2023 13:38:09 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=94=F0=9F=99=88=F0=9F=93=9D=20Add=20su?= =?UTF-8?q?pport=20for=20persistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- TODO.md | 1 - poetry.lock | 103 ++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 5 +++ voting/storage.py | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 voting/storage.py diff --git a/.gitignore b/.gitignore index 6c75682..af47e64 100644 --- a/.gitignore +++ b/.gitignore @@ -593,4 +593,5 @@ FodyWeavers.xsd # Additional files built by Visual Studio # End of https://www.toptal.com/developers/gitignore/api/python,visualstudio,vim -n \ No newline at end of file + +.voting.db \ No newline at end of file diff --git a/TODO.md b/TODO.md index acd815e..c9c0bf3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,3 @@ # ToDos -- add `click` and provide a user-friendly CLI - add `fastapi` and implement a builtin JSON/REST API server diff --git a/poetry.lock b/poetry.lock index 01b67f0..b7f05dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,11 +57,26 @@ files = [ {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]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -69,6 +84,21 @@ files = [ {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]] name = "decorator" version = "5.1.1" @@ -275,7 +305,7 @@ tests = ["pytest"] name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -301,6 +331,49 @@ files = [ [package.dependencies] 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]] name = "six" version = "1.16.0" @@ -349,6 +422,30 @@ files = [ docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 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]] name = "wcwidth" version = "0.2.6" @@ -364,4 +461,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "55dd02eaecfcfc8f3b3d44c10ea32ececbb37be4676145ca7f133b680cf8a618" +content-hash = "052d89cb180066114f48b9864dbbab045ec72ff627b10a55abc05e23a2312650" diff --git a/pyproject.toml b/pyproject.toml index 8816beb..898543a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" arrow = "^1.2.3" +typer = {extras = ["all"], version = "^0.7.0"} +simple-term-menu = "^1.6.1" [tool.poetry.group.dev.dependencies] @@ -17,3 +19,6 @@ nose2 = "^0.12.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +voting = 'voting.cli.voting:app' \ No newline at end of file diff --git a/voting/storage.py b/voting/storage.py new file mode 100644 index 0000000..f348419 --- /dev/null +++ b/voting/storage.py @@ -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