diff --git a/poetry.lock b/poetry.lock index c2fb7cb..30b1d35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 695dd16..1275917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/redstring/__init__.py b/redstring/__init__.py index e69de29..9a47f98 100644 --- a/redstring/__init__.py +++ b/redstring/__init__.py @@ -0,0 +1,2 @@ +import redstring.parser +import redstring.server \ No newline at end of file diff --git a/redstring/__main__.py b/redstring/__main__.py deleted file mode 100644 index e69de29..0000000 diff --git a/redstring/parser.py b/redstring/parser.py index 63d3a57..4fad427 100644 --- a/redstring/parser.py +++ b/redstring/parser.py @@ -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) \ No newline at end of file