Add parsing logic

This commit is contained in:
Tim Van Baak 2021-02-10 19:58:41 -08:00
parent 68987d4118
commit 74d090f7c2
5 changed files with 393 additions and 5 deletions

107
poetry.lock generated
View File

@ -66,6 +66,46 @@ optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "dev"
description = "Optional static typing for Python"
name = "mypy"
optional = false
python-versions = ">=3.5"
version = "0.800"
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
typed-ast = ">=1.4.0,<1.5.0"
typing-extensions = ">=3.7.4"
[package.extras]
dmypy = ["psutil (>=4.0)"]
[[package]]
category = "dev"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
name = "mypy-extensions"
optional = false
python-versions = "*"
version = "0.4.3"
[[package]]
category = "dev"
description = "a fork of Python 2 and 3 ast modules with type comment support"
name = "typed-ast"
optional = false
python-versions = "*"
version = "1.4.2"
[[package]]
category = "dev"
description = "Backported and Experimental Type Hints for Python 3.5+"
name = "typing-extensions"
optional = false
python-versions = "*"
version = "3.7.4.3"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
@ -79,7 +119,7 @@ dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-
watchdog = ["watchdog"]
[metadata]
content-hash = "b9f532f610ddec69914e59c13e5dc4b49e8d5a89a6365c4e32bfaea736dae4c8"
content-hash = "27f45d27293b2411af59f2d60572508a045af3d996d09cd45001f73388f721fd"
lock-version = "1.0"
python-versions = "^3.8"
@ -139,6 +179,71 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
mypy = [
{file = "mypy-0.800-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:e1c84c65ff6d69fb42958ece5b1255394714e0aac4df5ffe151bc4fe19c7600a"},
{file = "mypy-0.800-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:947126195bfe4709c360e89b40114c6746ae248f04d379dca6f6ab677aa07641"},
{file = "mypy-0.800-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:b95068a3ce3b50332c40e31a955653be245666a4bc7819d3c8898aa9fb9ea496"},
{file = "mypy-0.800-cp35-cp35m-win_amd64.whl", hash = "sha256:ca7ad5aed210841f1e77f5f2f7d725b62c78fa77519312042c719ed2ab937876"},
{file = "mypy-0.800-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e32b7b282c4ed4e378bba8b8dfa08e1cfa6f6574067ef22f86bee5b1039de0c9"},
{file = "mypy-0.800-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e497a544391f733eca922fdcb326d19e894789cd4ff61d48b4b195776476c5cf"},
{file = "mypy-0.800-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:5615785d3e2f4f03ab7697983d82c4b98af5c321614f51b8f1034eb9ebe48363"},
{file = "mypy-0.800-cp36-cp36m-win_amd64.whl", hash = "sha256:2b216eacca0ec0ee124af9429bfd858d5619a0725ee5f88057e6e076f9eb1a7b"},
{file = "mypy-0.800-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e3b8432f8df19e3c11235c4563a7250666dc9aa7cdda58d21b4177b20256ca9f"},
{file = "mypy-0.800-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d16c54b0dffb861dc6318a8730952265876d90c5101085a4bc56913e8521ba19"},
{file = "mypy-0.800-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0d2fc8beb99cd88f2d7e20d69131353053fbecea17904ee6f0348759302c52fa"},
{file = "mypy-0.800-cp37-cp37m-win_amd64.whl", hash = "sha256:aa9d4901f3ee1a986a3a79fe079ffbf7f999478c281376f48faa31daaa814e86"},
{file = "mypy-0.800-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:319ee5c248a7c3f94477f92a729b7ab06bf8a6d04447ef3aa8c9ba2aa47c6dcf"},
{file = "mypy-0.800-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:74f5aa50d0866bc6fb8e213441c41e466c86678c800700b87b012ed11c0a13e0"},
{file = "mypy-0.800-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:a301da58d566aca05f8f449403c710c50a9860782148332322decf73a603280b"},
{file = "mypy-0.800-cp38-cp38-win_amd64.whl", hash = "sha256:b9150db14a48a8fa114189bfe49baccdff89da8c6639c2717750c7ae62316738"},
{file = "mypy-0.800-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5fdf935a46aa20aa937f2478480ebf4be9186e98e49cc3843af9a5795a49a25"},
{file = "mypy-0.800-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6f8425fecd2ba6007e526209bb985ce7f49ed0d2ac1cc1a44f243380a06a84fb"},
{file = "mypy-0.800-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:5ff616787122774f510caeb7b980542a7cc2222be3f00837a304ea85cd56e488"},
{file = "mypy-0.800-cp39-cp39-win_amd64.whl", hash = "sha256:90b6f46dc2181d74f80617deca611925d7e63007cf416397358aa42efb593e07"},
{file = "mypy-0.800-py3-none-any.whl", hash = "sha256:3e0c159a7853e3521e3f582adb1f3eac66d0b0639d434278e2867af3a8c62653"},
{file = "mypy-0.800.tar.gz", hash = "sha256:e0202e37756ed09daf4b0ba64ad2c245d357659e014c3f51d8cd0681ba66940a"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
typed-ast = [
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},
{file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"},

View File

@ -10,10 +10,11 @@ flask = "^1.1.2"
flask-login = "^0.5.0"
[tool.poetry.dev-dependencies]
mypy = "^0.800"
[tool.poetry.scripts]
redstring-check = "redstring.parser:main"
redstring-server = "redstring.server:main"
redstring-cli = "redstring.parser:main"
[build-system]
requires = ["poetry>=0.12"]

View File

@ -0,0 +1,2 @@
import redstring.parser
import redstring.server

View File

View File

@ -1,3 +1,283 @@
def main():
print("Hello, world!")
print(__name__)
"""
Logic for reading and writing documents from files.
"""
import argparse
from collections import OrderedDict
import json
from typing import Any, List, IO
#
# Types
#
class TabOptions:
"""
Display options for tabs.
"""
_PRIORITY_KEY = 'priority'
_HIDE_NAMES_KEY = 'hide_names'
_PRIVATE_KEY = 'private'
def __init__(self, **kwargs) -> None:
self.options: dict = OrderedDict(**kwargs)
@property
def priority(self) -> int:
"""Priority determines tab order."""
return self.options.get(self._PRIORITY_KEY, 0)
@priority.setter
def priority(self, value: int):
self.options[self._PRIORITY_KEY] = value
@property
def hide_names(self) -> bool:
"""Hide the tag name column in the web view."""
return self.options.get(self._HIDE_NAMES_KEY, False)
@hide_names.setter
def hide_names(self, value: bool):
self.options[self._HIDE_NAMES_KEY] = value
@property
def private(self) -> bool:
"""Hide the tab from unauthenticated viewers."""
return self.options.get(self._PRIVATE_KEY, False)
@private.setter
def private(self, value: bool):
self.options[self._PRIVATE_KEY] = value
class TagOptions:
"""
Display options for tags.
"""
_HYPERLINK_KEY = 'hyperlink'
_INTERLINK_KEY = 'interlink'
_PRIVATE_KEY = 'private'
def __init__(self, **kwargs) -> None:
self.options = OrderedDict(**kwargs)
# Tag value is a hyperlink
self.hyperlink: bool = kwargs.get('hyperlink', False)
# Tag value contains redstring interlinks
self.interlink: bool = kwargs.get('interlink', False)
# Hide the tag from unauthenticated viewers
self.private: bool = kwargs.get('private', False)
@property
def hyperlink(self) -> bool:
return self.options.get(self._HYPERLINK_KEY, False)
@hyperlink.setter
def hyperlink(self, value: bool):
self.options[self._HYPERLINK_KEY] = value
@property
def interlink(self) -> bool:
return self.options.get(self._INTERLINK_KEY, False)
@hyperlink.setter
def interlink(self, value: bool):
self.options[self._INTERLINK_KEY] = value
@property
def private(self) -> bool:
"""Hide the tab from unauthenticated viewers."""
return self.options.get(self._PRIVATE_KEY, False)
@private.setter
def private(self, value: bool):
self.options[self._PRIVATE_KEY] = value
class DocumentSubtag:
"""
A keyvalue describing a document subject.
"""
def __init__(self, name: str, value: str, options: TagOptions) -> None:
self.name: str = name
self.value: str = value
self.options: TagOptions = options
class DocumentTag:
"""
A keyvalue describing a document subject. It may have subtags.
"""
def __init__(
self,
name: str,
value: str,
options: TagOptions,
subtags: List[DocumentSubtag]
) -> None:
self.name: str = name
self.value = value
self.options = options
self.subtags = subtags
class DocumentTab:
"""
A division of tags within a document.
"""
def __init__(self, tags: List[DocumentTag], options: TabOptions) -> None:
self.tags: List[DocumentTag] = tags
self.options: TabOptions = options
def __iter__(self):
return self.tags.__iter__()
class Document:
"""
Top-level document definition.
"""
def __init__(self, tabs: List[DocumentTab]) -> None:
self.tabs: List[DocumentTab] = tabs
def __iter__(self):
return self.tabs.__iter__()
#
# Parsing functions
#
def load(fd: IO) -> Document:
"""
Load a document from a file descriptor.
"""
parsed_json: list = json.load(fd, object_pairs_hook=OrderedDict)
return parse_document_from_json(parsed_json)
def loads(string: str) -> Document:
"""
Load a document from a string.
"""
parsed_json: list = json.loads(string, object_pairs_hook=OrderedDict)
return parse_document_from_json(parsed_json)
def parse_document_from_json(parsed_json: list) -> Document:
"""
Parses JSON into a Document object.
"""
# Parse tabs
tabs: List[DocumentTab] = []
for tab_json in parsed_json:
if type(tab_json) is not dict:
raise ValueError()
tabs.append(parse_tab_from_json(tab_json))
return Document(tabs)
def parse_tab_from_json(tab_json: dict) -> DocumentTab:
"""
Parses JSON into a DocumentTab object.
"""
# Parse tab options
if 'options' not in tab_json:
raise ValueError()
options_json: dict = tab_json['options']
options: TabOptions = TabOptions(**options_json)
# Parse tags
if 'tags' not in tab_json:
raise ValueError()
tags_json: list = tab_json['tags']
tags: List[DocumentTag] = []
for tag_json in tags_json:
if type(tag_json) is not dict:
raise ValueError()
tags.append(parse_tag_from_json(tag_json))
return DocumentTab(tags, options)
def parse_tag_from_json(tag_json: dict) -> DocumentTag:
"""
Parses JSON into a DocumentTag object.
"""
# Parse name
if 'name' not in tag_json:
raise ValueError()
name: str = tag_json['name']
# Parse value
if 'value' not in tag_json:
raise ValueError()
value: str = tag_json['value']
# Parse tag options
if 'options' not in tag_json:
raise ValueError()
options_json: dict = tag_json['options']
options: TagOptions = TagOptions(**options_json)
# Parse subtags
if 'subtags' not in tag_json:
raise ValueError()
subtags_json: list = tag_json['subtags']
subtags: List[DocumentSubtag] = []
for subtag_json in subtags_json:
if type(subtag_json) is not dict:
raise ValueError()
subtags.append(parse_subtag_from_json(subtag_json))
return DocumentTag(name, value, options, subtags)
def parse_subtag_from_json(subtag_json: dict) -> DocumentSubtag:
"""
Parses JSON into a DocumentSubtag object.
"""
# Parse name
if 'name' not in subtag_json:
raise ValueError()
name: str = subtag_json['name']
# Parse value
if 'value' not in subtag_json:
raise ValueError()
value: str = subtag_json['value']
# Parse tag options
if 'options' not in subtag_json:
raise ValueError()
options_json: dict = subtag_json['options']
options: TagOptions = TagOptions(**options_json)
return DocumentSubtag(name, value, options)
#
# CLI functions
#
def check(files):
"""
Checks a list of files for syntactical validity.
"""
for file in files:
with open(file) as f:
try:
load(f)
print(f'OK {file}')
except:
print(f'ERROR {file}')
def main() -> Any:
parser = argparse.ArgumentParser(description='Test a serialized redstring document file for validity.')
parser.add_argument('file', nargs='+', help='Files to check')
args = parser.parse_args()
check(args.file)