From 4b51d678bba6576532a47dab9991c127dc8e7853 Mon Sep 17 00:00:00 2001 From: error Date: Tue, 3 Jan 2023 14:42:54 +0100 Subject: [PATCH] Version 1.0.0 --- .gitignore | 164 ++++ LICENSE | 9 + README.md | 67 ++ requirements.txt | 3 + rfcartography/__init__.py | 48 ++ rfcartography/details.py | 43 ++ rfcartography/errors.py | 29 + rfcartography/index_parser.py | 514 +++++++++++++ rfcartography/rfcartographer.py | 172 +++++ rfcartography/routing.py | 35 + rfcartography/search.py | 119 +++ rfcartography/static/css/map.css | 47 ++ rfcartography/static/css/reset.css | 48 ++ rfcartography/static/css/rfcartography.css | 290 +++++++ rfcartography/static/css/search.css | 152 ++++ rfcartography/static/favicon.svg | 28 + .../static/fonts/CourierPrime-Bold.ttf | Bin 0 -> 69944 bytes .../static/fonts/CourierPrime-BoldItalic.ttf | Bin 0 -> 77544 bytes .../static/fonts/CourierPrime-Italic.ttf | Bin 0 -> 76656 bytes .../static/fonts/CourierPrime-Regular.ttf | Bin 0 -> 68304 bytes rfcartography/static/fonts/OFL.txt | 93 +++ rfcartography/templates/base.html | 64 ++ rfcartography/templates/details.html | 39 + rfcartography/templates/generic.html | 31 + rfcartography/templates/map.html | 97 +++ rfcartography/templates/rfc.html | 224 ++++++ rfcartography/templates/search.html | 158 ++++ tests/test_details.py | 91 +++ tests/test_errors.py | 34 + tests/test_index_parser.py | 715 ++++++++++++++++++ tests/test_rfcartographer.py | 253 +++++++ tests/test_routing.py | 34 + tests/test_search.py | 92 +++ 33 files changed, 3693 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 rfcartography/__init__.py create mode 100644 rfcartography/details.py create mode 100644 rfcartography/errors.py create mode 100644 rfcartography/index_parser.py create mode 100644 rfcartography/rfcartographer.py create mode 100644 rfcartography/routing.py create mode 100644 rfcartography/search.py create mode 100644 rfcartography/static/css/map.css create mode 100644 rfcartography/static/css/reset.css create mode 100644 rfcartography/static/css/rfcartography.css create mode 100644 rfcartography/static/css/search.css create mode 100644 rfcartography/static/favicon.svg create mode 100644 rfcartography/static/fonts/CourierPrime-Bold.ttf create mode 100644 rfcartography/static/fonts/CourierPrime-BoldItalic.ttf create mode 100644 rfcartography/static/fonts/CourierPrime-Italic.ttf create mode 100644 rfcartography/static/fonts/CourierPrime-Regular.ttf create mode 100644 rfcartography/static/fonts/OFL.txt create mode 100644 rfcartography/templates/base.html create mode 100644 rfcartography/templates/details.html create mode 100644 rfcartography/templates/generic.html create mode 100644 rfcartography/templates/map.html create mode 100644 rfcartography/templates/rfc.html create mode 100644 rfcartography/templates/search.html create mode 100644 tests/test_details.py create mode 100644 tests/test_errors.py create mode 100644 tests/test_index_parser.py create mode 100644 tests/test_rfcartographer.py create mode 100644 tests/test_routing.py create mode 100644 tests/test_search.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cdb3c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ + +3.0 KiB +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce51510 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 Error + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d324436 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# RFCartography + +Visualize relations between RFCs + +## Set Up + +Clone this repository and install dependencies, e.g. using `pip install -r requirements.txt`. +Place the index file (can be downloaded [here](https://www.rfc-editor.org/rfc-index.xml)) in the instance folder create a configuration in that location (see `Configuration`). + +Afterwards, RFCartography can be run by executing + + flask --app rfcartography run + +Note: Use `flask run` only for local development. +If you want to deploy it to a production server, use a WSGI server instead, as explained in the [Flask documentation](https://flask.palletsprojects.com/en/latest/deploying/). + +Hint: Generating large graphs takes a lot of time. +It is highly recommended to cache responses to improve response times. + +## Configuration + +RFCartography (or rather Flask, the framework it's based on) searches for the configuration in the `instance` directory. +To configure your instance, create a `config.py` file in this directory. + +### Flask Configuration Parameters + + - `SERVER_NAME`: Set the servers name, e.g. it's URL or address. RFCartography requires this value to be set correctly. If unset, it will default to `localhost`. + +For further generic configuration parameters, please refer to the [Flask documentation](https://flask.palletsprojects.com/en/latest/config/). + +### RFCartography Specific Configuration Parameters + + - `INDEX_FILE`: XML file containing the RFC index, usually downloaded from [here](https://www.rfc-editor.org/rfc-index.xml) + - `NAMESPACE`: XML namespace of the RFC index, defaults to `http://www.rfc-editor.org/rfc-index` + - `DEPTH_DEFAULT`: Depth limit to be used for requests w/o specified depth limit, defaults to 8 + - `IMPRINT`: Content that shall be displayed on the imprint page, see `Custom Content` for the syntax + - `PRIVACY`: Content that shall be displayed on the privacy page, see `Custom Content` for the syntax + +### Custom Content + +The content of the imprint and the privacy pages can be configured with the config file. +The corresponding parameters take a list of tuples of strings. +The first item of each tuple will be displayed as a headline, all following items as paragraphs. + +Example: + + PRIVACY = [('Privacy Statement', 'This website only processes data that is necessary in order to fulfill the user\'s request, e.g. the user\'s IP address. It does not generate access logs. Personal data is discarded once the request was served.')] + +Additional line breaks within a paragraph can be inserted by using a tuple of multiple strings instead of a string as a paragraph, e.g.: + + IMPRINT = [('E-Mail', 'My mail addresses are', ('example[at]not-a-real-mail-address.net', 'another-example[at]not-a-real-mail-address.net', 'one-more.example[at]not-a-real-mail-address.net'))] + +## Maintenance + +Remember to regularly update the rfc index file. + +## Tests + +Unittests a place in the `tests` directory. +Run all tests by executing: + + python -m unittest discover tests + +If you have coverage.py installed, you can check the coverage by executing: + + python -m coverage run -m unittest discover tests + python -m coverage report diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..65a3ae9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask +defusedxml +networkx[default] diff --git a/rfcartography/__init__.py b/rfcartography/__init__.py new file mode 100644 index 0000000..0965a6d --- /dev/null +++ b/rfcartography/__init__.py @@ -0,0 +1,48 @@ +from flask import Flask +from rfcartography.index_parser import IndexParser +from rfcartography.rfcartographer import RFCartographer +from rfcartography.routing import register_routers +from rfcartography.errors import register_errorhandlers + + +META = {'NAME': "RFCartography", + 'VERSION': "1.0.0", + 'SOURCE': "https://git.undefinedbehavior.de/undef/RFCartography"} + +def create_app(test_config: dict = None) -> Flask: + """set up the flask application""" + # create app + app = Flask(import_name=__name__, + instance_relative_config=True) + + # load configuration + if test_config is None: + app.config.from_pyfile(filename='config.py') + else: + app.config.from_mapping(mapping=test_config) + + # apply default config values where missing + if app.config['SERVER_NAME'] is None: + app.config['SERVER_NAME'] = 'localhost' + if not 'NAMESPACE' in app.config: + app.config['NAMESPACE'] = 'http://www.rfc-editor.org/rfc-index' + if not 'DEPTH_DEFAULT' in app.config: + app.config['DEPTH_DEFAULT'] = 8 + app.config['META'] = META + + # import rfc index data + try: + with app.open_instance_resource(app.config['INDEX_FILE']) as rfc_index: + xml: str = rfc_index.read() + except: + print('Error: INDEX_FILE could no be opened.\nExiting...') + exit(1) + + # set up the RFCartographer + parser: IndexParser = IndexParser(xml, app.config['NAMESPACE']) + app.cartographer: RFCartographer = RFCartographer(parser.get_index()) + + # register request handlers + register_errorhandlers(app) + register_routers(app) + return app diff --git a/rfcartography/details.py b/rfcartography/details.py new file mode 100644 index 0000000..1ccff7f --- /dev/null +++ b/rfcartography/details.py @@ -0,0 +1,43 @@ +from flask import Blueprint, render_template, current_app, abort +from rfcartography.index_parser import Document, NotIssued, DocType, Month + + +details: Blueprint = Blueprint('details', __name__) + +@details.route('/RFC', methods=['GET']) +def show_rfc(num: int) -> tuple[str, int]: + """handle requests for the details page for RFCs""" + rfc: Document = current_app.cartographer.get_document(DocType.RFC, num) + if rfc is None: + abort(404) + elif isinstance(rfc, NotIssued): + content: dict = {'title': rfc.docID(), + 'content': [('Not Issued', 'This RFC number was retained as a place holder, but never issued.')]} + return render_template('generic.html', **content), 200 + else: + url: str = "http://" + current_app.config['SERVER_NAME'] + if url[-1] != '/': + url = url + '/' + if rfc.pub_date is not None: + date: str = f"{Month(rfc.pub_date.month).name} {rfc.pub_date.year}" + else: + date: str = "" + context: dict = {'rfc': rfc, + 'url': url, + 'date': date} + return render_template('rfc.html', **context), 200 + +@details.route('/STD', methods=['GET'], defaults={'doctype': DocType.STD}) +@details.route('/BCP', methods=['GET'], defaults={'doctype': DocType.BCP}) +@details.route('/FYI', methods=['GET'], defaults={'doctype': DocType.FYI}) +def show_details(num: int, doctype: DocType) -> tuple[str, int]: + """handle requests for the details page for STDs, BCPs and FYIs""" + doc: Document = current_app.cartographer.get_document(doctype, num) + if doc is None: + abort(404) + url: str = "http://" + current_app.config['SERVER_NAME'] + if url[-1] != '/': + url = url + '/' + context: dict = {'doc': doc, + 'url': url} + return render_template('details.html', **context), 200 diff --git a/rfcartography/errors.py b/rfcartography/errors.py new file mode 100644 index 0000000..a62257d --- /dev/null +++ b/rfcartography/errors.py @@ -0,0 +1,29 @@ +from flask import Flask, render_template + + +def register_errorhandlers(app: Flask) -> None: + @app.errorhandler(400) + def bad_request(e) -> tuple: + content: dict = {'title': 'Bad Request', + 'content': [('HTTP Status 400', 'The request is malformed and cannot be processed.')]} + return render_template('generic.html', **content), 400 + + @app.errorhandler(404) + def not_found(e) -> tuple: + content: dict = {'title': 'Not Found', + 'content': [('HTTP Status 404', 'The requsted ressource was not found.')]} + return render_template('generic.html', **content), 404 + + @app.errorhandler(405) + def method_not_allowed(e) -> tuple: + content: dict = {'title': 'Method Not Allowed', + 'content': [('HTTP Status 405', 'The requested method is not allowed for this ressource.')]} + return render_template('generic.html', **content), 405 + + @app.errorhandler(500) + def internal_server_error(e) -> tuple: + content: dict = {'title': 'Internal Server Error', + 'content': [('HTTP Status 500', 'The request cannot be answered due to an internal server error.')]} + return render_template('generic.html', **content), 500 + + return diff --git a/rfcartography/index_parser.py b/rfcartography/index_parser.py new file mode 100644 index 0000000..01f0c13 --- /dev/null +++ b/rfcartography/index_parser.py @@ -0,0 +1,514 @@ +from enum import Enum, auto +from abc import ABC, abstractmethod +from datetime import date +from xml.etree.ElementTree import Element +from defusedxml.ElementTree import fromstring + + +class DocType(Enum): + RFC = 1 + STD = 2 + BCP = 3 + FYI = 4 + NIC = 5 + IEN = 6 + RTR = 7 + + def docID(self, + num: int) -> str: + if self.value < 5: # RFC, STD, BCP, FYI + return f"{self.name}{str(num).rjust(4, '0')}" + else: # NIC, IEN, RTR + return f"{self.name}{num}" + + +class Status(Enum): + INTERNET_STANDARD = auto() + DRAFT_STANDARD = auto() + PROPOSED_STANDARD = auto() + UNKNOWN = auto() + BEST_CURRENT_PRACTICE = auto() + FOR_YOUR_INFORMATION = auto() + EXPERIMENTAL = auto() + HISTORIC = auto() + INFORMATIONAL = auto() + + +class FileFormat(Enum): + ASCII = auto() + PS = auto() + PDF = auto() + TGZ = auto() + HTML = auto() + XML = auto() + TEXT = auto() + + +class Stream(Enum): + IETF = auto() + IAB = auto() + IRTF = auto() + INDEPENDENT = auto() + Editorial = auto() + Legacy = auto() + + +class Month(Enum): + January = 1 + February = 2 + March = 3 + April = 4 + May = 5 + June = 6 + July = 7 + August = 8 + September = 9 + October = 10 + November = 11 + December = 12 + + +class Author: + def __init__(self, + name: str, + title: str = "", + organization: str = "", + org_abbrev: str = ""): + self.name: str = name + self.title: str = title + self.organization: str = organization + self.org_abbrev: str = org_abbrev + return + + +class Document(ABC): + def __init__(self, + type: DocType, + number: int, + title: str = "", + is_also: list['Document'] = []): + self.type: DocType = type + self.number: int = number + self.title: str = title + self.is_also: list['Document'] = is_also + return + + def docID(self) -> str: + return self.type.docID(self.number) + + @abstractmethod + def update(self, **kwargs) -> 'Document': + pass + + @abstractmethod + def get_references(self) -> list[tuple[str, 'Document']]: + pass + + +class RFC(Document): + def __init__(self, + number: int, + title: str = "", + authors: list[Author] = [], + pub_date: date = None, + current_status: Status = Status.UNKNOWN, + pub_status: Status = Status.UNKNOWN, + format: list[FileFormat] = [], + page_count: int = None, + keywords: list[str] = [], + abstract: list[str] = [], + draft: str = "", + notes: str = "", + obsoletes: list[Document] = [], + obsoleted_by: list[Document] = [], + updates: list[Document] = [], + updated_by: list[Document] = [], + is_also: list[Document] = [], + see_also: list[Document] = [], + stream: Stream = None, + area: str = "", + wg_acronym: str = "", + errata_url: str = "", + doi: str = ""): + super().__init__(DocType.RFC, number, title, is_also) + self.authors: list[Author] = authors + self.pub_date: date = pub_date + self.format: list[FileFormat] = format + self.page_count: int = page_count + self.keywords: list[str] = keywords + self.abstract: list[str] = abstract + self.draft: str = draft + self.notes: str = notes + self.obsoletes: list[Document] = obsoletes + self.obsoleted_by: list[Document] = obsoleted_by + self.updates: list[Document] = updates + self.updated_by: list[Document] = updated_by + self.see_also: list[Document] = see_also + self.current_status: Status = current_status + self.pub_status: Status = pub_status + self.stream: Stream = stream + self.area: str = area + self.wg_acronym: str = wg_acronym + self.errata_url: str = errata_url + self.doi: str = doi + return + + def update(self, **kwargs) -> Document: + if 'title' in kwargs: + self.title = kwargs['title'] + if 'authors' in kwargs: + self.authors = kwargs['authors'] + if 'pub_date' in kwargs: + self.pub_date = kwargs["pub_date"] + if 'current_status' in kwargs: + self.current_status = kwargs["current_status"] + if 'pub_status' in kwargs: + self.pub_status = kwargs["pub_status"] + if 'format' in kwargs: + self.format = kwargs["format"] + if 'page_count' in kwargs: + self.page_count = kwargs["page_count"] + if 'keywords' in kwargs: + self.keywords = kwargs["keywords"] + if 'abstract' in kwargs: + self.abstract = kwargs["abstract"] + if 'draft' in kwargs: + self.draft = kwargs["draft"] + if 'notes' in kwargs: + self.notes = kwargs["notes"] + if 'obsoletes' in kwargs: + self.obsoletes = kwargs["obsoletes"] + if 'obsoleted_by' in kwargs: + self.obsoleted_by = kwargs["obsoleted_by"] + if 'updates' in kwargs: + self.updates = kwargs["updates"] + if 'updated_by' in kwargs: + self.updated_by = kwargs["updated_by"] + if 'is_also' in kwargs: + self.is_also = kwargs["is_also"] + if 'see_also' in kwargs: + self.see_also = kwargs["see_also"] + if 'stream' in kwargs: + self.stream = kwargs["stream"] + if 'area' in kwargs: + self.area = kwargs["area"] + if 'wg_acronym' in kwargs: + self.wg_acronym = kwargs["wg_acronym"] + if 'errata_url' in kwargs: + self.errata_url = kwargs["errata_url"] + if 'doi' in kwargs: + self.doi = kwargs["doi"] + return self + + def get_references(self) -> list[tuple[str, Document]]: + reftypes: list[str] = ["obsoletes"]*len(self.obsoletes)\ + + ["obsoleted by"]*len(self.obsoleted_by)\ + + ["updates"]*len(self.updates)\ + + ["updated by"]*len(self.updated_by)\ + + ["is also"]*len(self.is_also)\ + + ["see also"]*len(self.see_also) + refs: list[Document] = self.obsoletes \ + + self.obsoleted_by \ + + self.updates \ + + self.updated_by \ + + self.is_also \ + + self.see_also + return list(zip(reftypes, refs)) + + +class NotIssued(Document): + def __init__(self, + number: int): + super().__init__(DocType.RFC, number) + return + + def update(self, **kwargs) -> Document: + return self + + def get_references(self) -> list[tuple[str, Document]]: + return [] + + +class STD(Document): + def __init__(self, + number: int, + title: str = "", + is_also: list[Document] = []): + super().__init__(DocType.STD, number, title, is_also) + return + + def update(self, **kwargs) -> Document: + if 'title' in kwargs: + self.title = kwargs['title'] + if 'is_also' in kwargs: + self.is_also = kwargs['is_also'] + return self + + def get_references(self) -> list[tuple[str, Document]]: + return list(zip(["is also"]*len(self.is_also), self.is_also)) + + +class BCP(Document): + def __init__(self, + number: int, + title: str = "", + is_also: list[Document] = []): + super().__init__(DocType.BCP, number, title, is_also) + return + + def update(self, **kwargs) -> Document: + if 'title' in kwargs: + self.title = kwargs['title'] + if 'is_also' in kwargs: + self.is_also = kwargs['is_also'] + return self + + def get_references(self) -> list[tuple[str, Document]]: + return list(zip(["is also"]*len(self.is_also), self.is_also)) + + +class FYI(Document): + def __init__(self, + number: int, + title: str = "", + is_also: list[Document] = []): + super().__init__(DocType.FYI, number, title, is_also) + return + + def update(self, **kwargs) -> Document: + if 'title' in kwargs: + self.title = kwargs['title'] + if 'is_also' in kwargs: + self.is_also = kwargs['is_also'] + return self + + def get_references(self) -> list[tuple[str, Document]]: + return list(zip(["is also"]*len(self.is_also), self.is_also)) + + +class NIC(Document): + def __init__(self, + number: int): + super().__init__(DocType.NIC, number) + return + + def update(self, **kwargs) -> Document: + return self + + def get_references(self) -> list[tuple[str, Document]]: + return [] + + +class IEN(Document): + def __init__(self, + number: int): + super().__init__(DocType.IEN, number) + return + + def update(self, **kwargs) -> Document: + return self + + def get_references(self) -> list[tuple[str, Document]]: + return [] + + +class RTR(Document): + def __init__(self, + number: int): + super().__init__(DocType.RTR, number) + return + + def update(self, **kwargs) -> Document: + return self + + def get_references(self) -> list[tuple[str, Document]]: + return [] + + +class IndexParser: + def __init__(self, + xml: str, + namespace: str = "http://www.rfc-editor.org/rfc-index"): + def _get_reflist(container: Element | None) -> list[Document]: + reflist: list[Document] = [] + if container is not None: + for ref in container.findall(f"{{{namespace}}}doc-id"): + ref_type: str = DocType[ref.text[:3]] + ref_num: int = int(ref.text[3:]) + if ref_num not in self.index[ref_type]: + if ref_type == DocType.RFC: + self.index[DocType.RFC][ref_num] = RFC(ref_num) + elif ref_type == DocType.STD: + self.index[DocType.STD][ref_num] = STD(ref_num) + elif ref_type == DocType.BCP: + self.index[DocType.BCP][ref_num] = BCP(ref_num) + elif ref_type == DocType.FYI: + self.index[DocType.FYI][ref_num] = FYI(ref_num) + elif ref_type == DocType.NIC: + self.index[DocType.NIC][ref_num] = NIC(ref_num) + elif ref_type == DocType.IEN: + self.index[DocType.IEN][ref_num] = IEN(ref_num) + else: # ref_type == DocType.RTR + self.index[DocType.RTR][ref_num] = RTR(ref_num) + reflist.append(self.index[ref_type][ref_num]) + return reflist + + self.index: dict[DocType: dict[int, Document]] = {DocType.RFC: {}, + DocType.STD: {}, + DocType.BCP: {}, + DocType.FYI: {}, + DocType.NIC: {}, + DocType.IEN: {}, + DocType.RTR: {}} + + root: Element = fromstring(xml) + for child in root: + if child.tag == f"{{{namespace}}}rfc-entry": + docID: str = child.findtext(f"{{{namespace}}}doc-id") + number: int = int(docID[3:]) + title: str = child.findtext(f"{{{namespace}}}title") + authors: list[Author] = [] + for author in child.findall(f"{{{namespace}}}author"): + name: str = author.findtext(f"{{{namespace}}}name") + auth_title: str = author.findtext(f"{{{namespace}}}title", "") + org: str = author.findtext(f"{{{namespace}}}organization", "") + org_abbrev: str = author.findtext(f"{{{namespace}}}org-abbrev", "") + authors.append(Author(name, auth_title, org, org_abbrev)) + tmp: Element | None = child.find(f"{{{namespace}}}date") + pub_year: int = int(tmp.findtext(f"{{{namespace}}}year")) + pub_month: int = Month[tmp.findtext(f"{{{namespace}}}month")].value + pub_day: int = int(tmp.findtext(f"{{{namespace}}}day", "1")) + pub_date: date = date(pub_year, pub_month, pub_day) + format: list[FileFormat] = [] + tmp = child.find(f"{{{namespace}}}format") + if tmp is not None: + for file_format in tmp.findall(f"{{{namespace}}}file-format"): + format.append(FileFormat[file_format.text]) + page_count: int = int(child.findtext(f"{{{namespace}}}page-count", "-1")) + if page_count < 0: + page_count = None + keywords: list[str] = [] + tmp = child.find(f"{{{namespace}}}keywords") + if tmp is not None: + for kw in tmp.findall(f"{{{namespace}}}kw"): + keywords.append(kw.text) + abstract: list[str] = [] + tmp = child.find(f"{{{namespace}}}abstract") + if tmp is not None: + for p in tmp.findall(f"{{{namespace}}}p"): + abstract.append(p.text) + draft: str = child.findtext(f"{{{namespace}}}draft", "") + notes: str = child.findtext(f"{{{namespace}}}notes", "") + tmp = child.find(f"{{{namespace}}}obsoletes") + obsoletes: list[Document] = _get_reflist(tmp) + tmp = child.find(f"{{{namespace}}}obsoleted-by") + obsoleted_by: list[Document] = _get_reflist(tmp) + tmp = child.find(f"{{{namespace}}}updates") + updates: list[Document] = _get_reflist(tmp) + tmp = child.find(f"{{{namespace}}}updated-by") + updated_by: list[Document] = _get_reflist(tmp) + tmp = child.find(f"{{{namespace}}}is-also") + is_also: list[Document] = _get_reflist(tmp) + tmp = child.find(f"{{{namespace}}}see-also") + see_also: list[Document] = _get_reflist(tmp) + current_status: Status = Status[child.findtext(f"{{{namespace}}}current-status").replace(" ", "_")] + pub_status: Status = Status[child.findtext(f"{{{namespace}}}publication-status").replace(" ", "_")] + stream: Stream = None + tmp = child.find(f"{{{namespace}}}stream") + if tmp is not None: + stream = Stream[tmp.text] + area: str = child.findtext(f"{{{namespace}}}area", "") + wg_acronym: str = child.findtext(f"{{{namespace}}}wg_acronym", "") + errata_url: str = child.findtext(f"{{{namespace}}}errata-url", "") + doi: str = child.findtext(f"{{{namespace}}}doi", "") + if number in self.index[DocType.RFC]: + self.index[DocType.RFC][number].update(title=title, + authors=authors, + pub_date=pub_date, + current_status=current_status, + pub_status=pub_status, + format=format, + page_count=page_count, + keywords=keywords, + abstract=abstract, + draft=draft, + notes=notes, + obsoletes=obsoletes, + obsoleted_by=obsoleted_by, + updates=updates, + updated_by=updated_by, + is_also=is_also, + see_also=see_also, + stream=stream, + area=area, + wg_acronym=wg_acronym, + errata_url=errata_url, + doi=doi) + else: + self.index[DocType.RFC][number] = RFC(number, + title, + authors, + pub_date, + current_status, + pub_status, + format, + page_count, + keywords, + abstract, + draft, + notes, + obsoletes, + obsoleted_by, + updates, + updated_by, + is_also, + see_also, + stream, + area, + wg_acronym, + errata_url, + doi) + continue + elif child.tag == f"{{{namespace}}}rfc-not-issued-entry": + docID: str = child.findtext(f"{{{namespace}}}doc-id") + number: int = int(docID[3:]) + if number not in self.index[DocType.RFC]: + self.index[DocType.RFC][number] = NotIssued(number) + continue + elif child.tag == f"{{{namespace}}}std-entry": + docID: str = child.findtext(f"{{{namespace}}}doc-id") + number: int = int(docID[3:]) + title: str = child.findtext(f"{{{namespace}}}title") + alias: Element = child.find(f"{{{namespace}}}is-also") + is_also: list[Document] = _get_reflist(alias) + if number in self.index[DocType.STD]: + self.index[DocType.STD][number].update(title=title, is_also=is_also) + else: + self.index[DocType.STD][number] = STD(number, title, is_also) + continue + elif child.tag == f"{{{namespace}}}bcp-entry": + docID: str = child.findtext(f"{{{namespace}}}doc-id") + number: int = int(docID[3:]) + title: str = child.findtext(f"{{{namespace}}}title", "") + alias: Element = child.find(f"{{{namespace}}}is-also") + is_also: list[Document] = _get_reflist(alias) + if number in self.index[DocType.BCP]: + self.index[DocType.BCP][number].update(title=title, is_also=is_also) + else: + self.index[DocType.BCP][number] = BCP(number, title, is_also) + continue + elif child.tag == f"{{{namespace}}}fyi-entry": + docID: str = child.findtext(f"{{{namespace}}}doc-id") + number: int = int(docID[3:]) + title: str = child.findtext(f"{{{namespace}}}title", "") + alias: Element = child.find(f"{{{namespace}}}is-also") + is_also: list[Document] = _get_reflist(alias) + if number in self.index[DocType.FYI]: + self.index[DocType.FYI][number].update(title=title, is_also=is_also) + else: + self.index[DocType.FYI][number] = FYI(number, title, is_also) + continue + return + + def get_index(self) -> dict[DocType: dict[int, Document]]: + return self.index diff --git a/rfcartography/rfcartographer.py b/rfcartography/rfcartographer.py new file mode 100644 index 0000000..7b4f6db --- /dev/null +++ b/rfcartography/rfcartographer.py @@ -0,0 +1,172 @@ +from networkx import MultiDiGraph, kamada_kawai_layout, \ + draw_networkx_nodes, draw_networkx_edges +from matplotlib.pyplot import figure, close +from matplotlib.figure import Figure +from matplotlib.layout_engine import TightLayoutEngine +from io import StringIO +from math import sqrt +from rfcartography.index_parser import Document, DocType + + +class RFCMap: + def __init__(self, + graph: MultiDiGraph, + nodes: dict[DocType, list[Document]], + edges: dict[str, tuple[Document, Document]], + url_base: str, + node_color: dict[DocType, str] = {DocType.RFC: '#2072b1', + DocType.STD: '#c21a7e', + DocType.BCP: '#6d388d', + DocType.FYI: '#8bbd3e', + DocType.NIC: '#efe50b', + DocType.IEN: '#f28f20', + DocType.RTR: '#e32326'}, + edge_style: dict[str, tuple[str, str]]= {'obsoletes': ('dashed', '#607d8d'), + 'obsoleted by': ('dashed', '#303c50'), + 'updates': ('dashdot', '#607d8d'), + 'updated by': ('dashdot', '#303c50'), + 'is also': ('solid', '#132e41'), + 'see also': ('dotted', '#008e90')}): + self.graph: MultiDiGraph = graph + self.nodes: dict[DocType, list[Document]] = nodes + self.edges: dict[str, tuple[Document, Document]] = edges + self.node_color: dict[DocType, str] = node_color + self.edge_style: dict[str, tuple[str, str]] = edge_style + self.url_base: str = url_base + self.position: dict = kamada_kawai_layout(self.graph) + return + + def set_node_color(self, + doctype: DocType, + color: str) -> None: + self.node_color[doctype] = color + return + + def set_edge_style(self, + reftype: str, + style: tuple[str, str]) -> None: + self.edge_style[reftype] = style + return + + def set_url_base(self, + url_base: str) -> None: + self.url_base = url_base + return + + def get_node_colors(self) -> dict[DocType, str]: + return self.node_color + + def get_edge_styles(self) -> dict[str, tuple[str, str]]: + return self.edge_style + + def get_url_base(self) -> str: + return self.url_base + + def get_node_count(self) -> int: + return self.graph.number_of_nodes() + + def get_edge_count(self) -> int: + return self.graph.size() + + def draw(self) -> str: + nodes: list[Document] = [] + node_colors: list[str] = [] + urls: list[str] = [] + + for doctype in self.nodes: + nodes = nodes + self.nodes[doctype] + node_colors = node_colors + [self.node_color[doctype]]*len(self.nodes[doctype]) + for node in self.nodes[doctype]: + urls.append(f"{self.url_base}{node.docID()}") + + edge_rad: dict[float] = {'obsoletes': 0.1 if len(self.edges['obsoleted by']) > 0 else 0, + 'obsoleted by': 0.1 if len(self.edges['obsoletes']) > 0 else 0, + 'updates': 0.1 if len(self.edges['updated by']) > 0 else 0, + 'updated by': 0.1 if len(self.edges['updates']) > 0 else 0, + 'is also': 0, + 'see also': 0} + size: float = sqrt(len(nodes)) + fig: Figure = figure(figsize=(size, size), layout=TightLayoutEngine(pad=0.2)) + fig.clear() + + draw_networkx_nodes(self.graph, self.position, + nodelist=nodes, + node_size=128, + node_color=node_colors, + node_shape="o").set_urls(urls) + + for reftype in self.edges: + draw_networkx_edges(self.graph, self.position, + edgelist=self.edges[reftype], + connectionstyle=f"arc3,rad={edge_rad[reftype]}", + style=self.edge_style[reftype][0], + edge_color=self.edge_style[reftype][1]) + + svg: StringIO = StringIO() + fig.savefig(svg, format='svg') + svg.seek(0) + close(fig) + return svg.read().removeprefix('\n\n') + + +class RFCartographer: + def __init__(self, + index: dict[DocType: dict[int, Document]]): + self.index: dict[DocType: dict[int, Document]] = index + return + + def get_document(self, + doctype: DocType, + number: int) -> Document | None: + return self.index[doctype].get(number, None) + + def map_subnet(self, + core: Document, + url: str, + max_depth: int = 0, + node_color: dict[DocType, str] = None, + edge_style: dict[str, tuple[str, str]] = None, + node_types: list[DocType] = []) -> RFCMap: + """generate a map for the subnet core belongs to""" + if node_types == []: + node_types = [DocType.RFC, DocType.STD, DocType.BCP, + DocType.FYI, DocType.NIC, DocType.IEN, DocType.RTR] + nodes: dict = {DocType.RFC: [], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [], + DocType.IEN: [], + DocType.RTR: []} + edges: dict = {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': []} + params: dict[str, dict] = {} + if node_color is not None: + params['node_color'] = node_color + if edge_style is not None: + params['edge_style'] = edge_style + todo: list[tuple[Document, int]] = [(core, 0)] + done: list[Document] = [] + graph: MultiDiGraph = MultiDiGraph() + graph.add_node(core) + nodes[core.type].append(core) + + while len(todo) > 0: + node: tuple[Document, int] = todo.pop(0) + if node[0] not in done: + done.append(node[0]) + if node[1] < max_depth or max_depth <= 0: + for neighbor in node[0].get_references(): + if not neighbor[1].type in node_types: + continue + if not graph.has_node(neighbor[1]): + graph.add_node(neighbor[1]) + nodes[neighbor[1].type].append(neighbor[1]) + graph.add_edge(node[0], neighbor[1], reftype=neighbor[0]) + edges[neighbor[0]].append((node[0], neighbor[1])) + todo.append((neighbor[1], node[1]+1)) + return RFCMap(graph, nodes, edges, url, **params) diff --git a/rfcartography/routing.py b/rfcartography/routing.py new file mode 100644 index 0000000..0c6fd40 --- /dev/null +++ b/rfcartography/routing.py @@ -0,0 +1,35 @@ +from flask import Flask, Blueprint, redirect, url_for, render_template, abort +from werkzeug.wrappers import Response +from rfcartography.search import search +from rfcartography.details import details + + +def register_routers(app: Flask) -> None: + @app.route('/imprint') + def imprint() -> tuple[str, int]: + if not 'IMPRINT' in app.config: + abort(404) + content: dict = {'title': 'Imprint', + 'content': app.config['IMPRINT']} + return render_template('generic.html', **content), 200 + + @app.route('/privacy') + def privacy() -> tuple[str, int]: + if not 'PRIVACY' in app.config: + abort(404) + content: dict = {'title': 'Privacy', + 'content': app.config['PRIVACY']} + return render_template('generic.html', **content), 200 + + # answer favicon requests + @app.route('/favicon.ico') + def favicon() -> Response: + return redirect(url_for('static', filename='favicon.svg')) + + static = Blueprint('static', + __name__, + static_folder='static') + app.register_blueprint(static) + app.register_blueprint(search) + app.register_blueprint(details) + return diff --git a/rfcartography/search.py b/rfcartography/search.py new file mode 100644 index 0000000..d84b3e8 --- /dev/null +++ b/rfcartography/search.py @@ -0,0 +1,119 @@ +from flask import Blueprint, abort, request, current_app, render_template +from rfcartography.index_parser import DocType, Document + + +def validate_type(user_input: str | None) -> DocType: + """check if the given input is a DocType + return the DocType if it is valid + abort with HTTP Status 400 if it isn't valid""" + if user_input is None: + abort(400) + try: + doctype: DocType = DocType[user_input.upper()] + except KeyError: + abort(400) + return doctype + +def validate_int(user_input: str | None) -> int: + """check if the given input is a integer + return the int if it is valid + abort with HTTP Status 400 if it isn't valid""" + if user_input is None: + abort(400) + try: + i: int = int(user_input) + except ValueError: + abort(400) + return i + +def validate_color(user_input: str) -> str: + """check if the given user input is a valid color + return the string if it is valid + abort with HTTP Status 400 if it isn't valid""" + if user_input[0] != '#' or len(user_input) != 7: + abort(400) + for i in range(1,7): + if user_input[i] not in '0123456789abcdefABCDEF': + abort(400) + return user_input + +def validate_linestyle(user_input: str) -> str: + """check if the given user input is a valid linestyle + return the string if it is valid + abort with HTTP Status 400 if it isn't valid""" + if user_input in ['solid', 'dashed', 'dashdot', 'dotted', 'none']: + return user_input + else: + abort(400) + +search: Blueprint = Blueprint('search', __name__) + +@search.route('/', methods=['GET']) +def provide_searchform() -> tuple[str, int]: + """handle requests for the search form""" + return render_template('search.html'), 200 + +@search.route('/map', methods= ['GET']) +def handle_search_request() -> tuple[str, int]: + """handle search requests""" + params: dict = {} + + doctype: DocType = validate_type(request.args.get('type', None)) + num: int = validate_int(request.args.get('num', None)) + depth: int = validate_int(request.args.get('depth', current_app.config['DEPTH_DEFAULT'])) + nodes: list[DocType] = request.args.getlist('nodes_enabled', validate_type) + if nodes == []: + nodes = [DocType.RFC, DocType.STD, DocType.BCP, DocType.FYI, + DocType.NIC, DocType.IEN, DocType.RTR] + params['node_types'] = nodes + + node_colors: dict[DocType, str | None] = {DocType.RFC: request.args.get('rfc_color', None), + DocType.STD: request.args.get('std_color', None), + DocType.BCP: request.args.get('bcp_color', None), + DocType.FYI: request.args.get('fyi_color', None), + DocType.NIC: request.args.get('nic_color', None), + DocType.IEN: request.args.get('ien_color', None), + DocType.RTR: request.args.get('rtr_color', None)} + if not all(color is None for color in node_colors.values()): + for nodetype in node_colors: + node_colors[nodetype] = validate_color(node_colors[nodetype]) + params['node_color'] = node_colors + + edge_style: dict[str, tuple[str, str]] = \ + {'obsoletes': (request.args.get('obsoletes_style', None), + request.args.get('obsoletes_color', None)), + 'obsoleted by': (request.args.get('obsoleted_by_style', None), + request.args.get('obsoleted_by_color', None)), + 'updates': (request.args.get('updates_style', None), + request.args.get('updates_color', None)), + 'updated by': (request.args.get('updated_by_style', None), + request.args.get('updated_by_color', None)), + 'is also': (request.args.get('is_also_style', None), + request.args.get('is_also_color', None)), + 'see also': (request.args.get('see_also_style', None), + request.args.get('see_also_color', None))} + if not all(arg is None for edge_type in edge_style.values() for arg in edge_type): + for edge_type in edge_style: + edge_style[edge_type] = (validate_linestyle(edge_style[edge_type][0]), + validate_color(edge_style[edge_type][1])) + params['edge_style'] = edge_style + + url: str = "http://" + current_app.config['SERVER_NAME'] + if url[-1] != '/': + url = url + '/' + + doc: Document = current_app.cartographer.get_document(doctype, num) + if doc is None: + abort(404) + else: + rfc_map: RFCMap = current_app.cartographer.map_subnet(doc, url, depth, **params) + content: dict = {'core_node_id': doc.docID(), + 'map': rfc_map.draw(), + 'nodes': nodes, + 'node_colors': rfc_map.get_node_colors(), + 'edge_style': rfc_map.get_edge_styles()} + if not doctype in nodes: + content['nodes'].append(doctype) + return render_template('map.html', **content), 200 + + diff --git a/rfcartography/static/css/map.css b/rfcartography/static/css/map.css new file mode 100644 index 0000000..89cc615 --- /dev/null +++ b/rfcartography/static/css/map.css @@ -0,0 +1,47 @@ +main h1 + svg { + width: 70%; + height: auto; +} + +main h1 + svg + div { + width: 30%; + float: right; + padding: 1rem; +} + +main div table { + margin-bottom: 1rem; +} + +main div table tr td:first-of-type { + height: 2rem; + width: 3rem; + padding-right: 1rem; + vertical-align: middle; +} + +main div table tr td:nth-of-type(2) { + height: 2rem; +} + +.node { + width: 1.5rem; + height: auto; +} + +.edge { + width: 3rem; + height: auto; + vertical-align: text-top; +} + +@media (max-width: 860px) { + main h1 + svg { + width: 100%; + } + + main h1 + svg + div { + width: 100%; + float: none; + } +} diff --git a/rfcartography/static/css/reset.css b/rfcartography/static/css/reset.css new file mode 100644 index 0000000..e29c0f5 --- /dev/null +++ b/rfcartography/static/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/rfcartography/static/css/rfcartography.css b/rfcartography/static/css/rfcartography.css new file mode 100644 index 0000000..b295348 --- /dev/null +++ b/rfcartography/static/css/rfcartography.css @@ -0,0 +1,290 @@ +@font-face { + font-family: 'Courier Prime'; + font-style: normal; + font-weight: normal; + font-stretch: normal; + font-display: swap; + src: url('/static/fonts/CourierPrime-Regular.ttf') format('truetype'), + local('Courier Prime'); +} + +@font-face { + font-family: 'Courier Prime'; + font-style: normal; + font-weight: bold; + font-stretch: normal; + font-display: swap; + src: url('/static/fonts/CourierPrime-Bold.ttf') format('truetype'), + local('Courier Prime'); +} + +@font-face { + font-family: 'Courier Prime'; + font-style: italic; + font-weight: normal; + font-stretch: normal; + font-display: swap; + src: url('/static/fonts/CourierPrime-Italic.ttf') format('truetype'), + local('Courier Prime'); +} + +@font-face { + font-family: 'Courier Prime'; + font-style: italic; + font-weight: bold; + font-stretch: normal; + font-display: swap; + src: url('/static/fonts/CourierPrime-BoldItalic.ttf') format('truetype'), + local('Courier Prime'); +} + +:root { + --background-body: #FAFAFA; + --background-item: #FFFFFF; + --accent-blue: #2072b1; + --accent-red: #c21a7e; + --text: #000000; + --gradient: linear-gradient(45deg, #2072b1 10%, #c21a7e 90%); + --body-width: 80vw; +} + +@media (max-width: 860px) { + :root { + --body-width: 100vw; + } +} + +/* consider padding and borders for width and height */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +@media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } +} + +html { + font-size: 1rem; +} + +body { + font-family: 'Courier Prime', monospace; + line-height: 1.8; + max-width: var(--body-width); + min-height: 100vh; + overflow-x: hidden; + background-color: var(--background-body); + text-rendering: optimizeLegibility; + margin: auto; +} + +header +{ + height: 4em; + width: 100%; + padding: 1ch; + border-bottom-width: 3px; + border-bottom-style: solid; + border-bottom-color: var(--accent-blue); + border-image: var(--gradient); + border-image-slice: 1; + display: flex; + align-items: center; + margin-bottom: 1.5em; +} + +header img +{ + height: 3ch; + padding-right: 1ch; +} + +header a { + text-decoration: none; +} + +header a h1 { + font-size: 2ch; +} + +header a h1 span:nth-of-type(1) { + color: var(--accent-red) +} + +header a h1 span:nth-of-type(2) { + color: var(--accent-blue) +} + +header a:hover h1 span:nth-of-type(1) { + color: var(--accent-blue) +} + +header a:hover h1 span:nth-of-type(2) { + color: var(--accent-red) +} + +header label { + display: none; +} + +header nav { + flex-grow: 1; +} + +header form { + display: block; + text-align: right; + font-size: 0; + min-width: 14rem; +} + +header select { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + border-top-left-radius: 1ch; + border-bottom-left-radius: 1ch; + height: 2rem; + width: 4rem; + background-color: var(--background-item); + text-align: right; + font-family: 'Courier Prime', monospace; + vertical-align: middle; +} + +header input[type=number] { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + height: 2rem; + width: 8rem; + background-color: var(--background-item); + font-family: 'Courier Prime', monospace; + -webkit-appearance: none; + -moz-appearance: textfield; + vertical-align: middle +} + +header input[type=number]:focus { + border-color: var(--accent-red); + outline: none; +} + +header input[type=submit] { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + border-top-right-radius: 1ch; + border-bottom-right-radius: 1ch; + height: 2rem; + width: 2rem; + background-color: var(--accent-blue); + color: var(--background-item); + font-weight: bold; + vertical-align: middle; +} + +header input[type=submit]:hover { + background-color: var(--background-item); + border-color: var(--accent-blue); + color: var(--accent-blue); +} + +header input[type=submit]:focus { + background-color: var(--background-item); + border-color: var(--accent-red); + color: var(--accent-red); +} + +main { + min-height: 50vh; + padding: 1em; +} + +main h1 { + font-size: 1.4rem; + font-weight: bold; + color: var(--accent-blue); + margin-bottom: 0.5em; + margin-top: 0.5em; +} + +main h2 { + font-size: 1.2rem; + font-weight: bold; + margin-bottom: 0.5em; + margin-top: 0.5em; +} + +main p { + margin-bottom: 0.5em; +} + +main a { + text-decoration: none; + color: var(--accent-blue); +} + +main a:hover { + text-decoration: none; + color: var(--accent-red); +} + +main a:active { + text-decoration: underline; +} + +main dl dt { + font-weight: bold; +} + +main dl dd { + padding-left: 2rem; +} + +footer { + width: 100%; + padding: 1ch; + border-top-width: 3px; + border-top-style: solid; + border-top-color: var(--accent-blue); + border-image: var(--gradient); + border-image-slice: 1; + margin-top: 2ch; + clear: both; +} + +footer ul { + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-blue); + font-size: 0.8rem; +} + +footer ul li { + padding-left: 1ch; + padding-right: 1ch; +} + +footer ul li:nth-child(2):before { + content: "::"; +} + +footer ul li:nth-child(2):after { + content: "::"; +} + +footer ul li a { + text-decoration: none; + color: var(--accent-blue); +} + +footer ul li a:hover { + text-decoration: none; + color: var(--accent-red); +} diff --git a/rfcartography/static/css/search.css b/rfcartography/static/css/search.css new file mode 100644 index 0000000..d7ab29c --- /dev/null +++ b/rfcartography/static/css/search.css @@ -0,0 +1,152 @@ +main form { + width: 100%; + display: flex; + flex-wrap: wrap; +} + +main form fieldset { + padding: 0.5rem; +} + +main form fieldset input[type=number] { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + height: 2rem; + background-color: var(--background-item); + font-family: 'Courier Prime', monospace; + -webkit-appearance: none; + -moz-appearance: textfield; + vertical-align: middle; + padding-left: 0.5ch; + padding-right: 0.5ch; +} + +main form fieldset input[type=number]:focus { + border-color: var(--accent-red); + outline: none; +} + +main form fieldset input[type=number]:first-of-type { + font-size: 1rem; + width: calc(100% - 18ch - 4rem); +} + +main form fieldset select { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + font-size: 1rem; + height: 2rem; + background-color: var(--background-item); + text-align: right; + font-family: 'Courier Prime', monospace; + vertical-align: middle; +} + +main form fieldset:first-of-type { + min-width: 100%; + font-size: 0rem; +} + +main form fieldset:first-of-type label{ + font-size: 1rem; + padding: 1ch; +} + +main form fieldset:first-of-type select { + border-top-left-radius: 1ch; + border-bottom-left-radius: 1ch; + width: 18ch; +} + +main form fieldset:first-of-type input[type=submit] { + border-width: 2px; + border-style: solid; + border-color: var(--accent-blue); + border-top-right-radius: 1ch; + border-bottom-right-radius: 1ch; + height: 2rem; + width: 4rem; + background-color: var(--accent-blue); + color: var(--background-item); + font-size: 1rem; + font-weight: bold; + vertical-align: middle; +} + +main form fieldset:first-of-type input[type=submit]:hover { + background-color: var(--background-item); + border-color: var(--accent-blue); + color: var(--accent-blue); +} + +main form fieldset:first-of-type input[type=submit]:focus { + background-color: var(--background-item); + border-color: var(--accent-red); + color: var(--accent-red); +} + +main form fieldset:not(:first-of-type) { + min-width: 20rem; + border-style: solid; + border-width: 2px; + border-radius: 1rem; + border-color: var(--accent-blue); + border-image: var(--gradient); + border-image-slice: 1; + flex-grow: 1; + margin: 0.5rem; + text-align: center; +} + +main form fieldset:not(:first-of-type) legend { + padding: 0.5rem; +} + +main form fieldset:not(:first-of-type) div { + display: inline-block; + vertical-align: middle; +} + +main form fieldset:not(:first-of-type) div:nth-of-type(1) { + padding-right: 1rem; +} + +main form fieldset:not(:first-of-type) div:nth-of-type(2) { + height: 100%; +} + +main form fieldset #depth { + text-align: right; + font-size: 1rem; + border-radius: 1ch; + width: 8rem; +} + +main form fieldset:not(:first-of-type) input[type=color] { + width: 2rem; + vertical-align: middle; + border-radius: 0.5rem; +} + +main form fieldset:not(:first-of-type) select { + border-radius: 1ch; + width: 12ch; +} + +main form fieldset:not(:first-of-type) label { + display: inline-block; + vertical-align: middle; + text-align: left; +} + +main form fieldset:nth-of-type(4) div label, +main form fieldset:nth-of-type(5) div label { + width: 4ch; +} + +main form fieldset:nth-of-type(6) div label, +main form fieldset:nth-of-type(7) div label { + width: 13ch; +} diff --git a/rfcartography/static/favicon.svg b/rfcartography/static/favicon.svg new file mode 100644 index 0000000..5bd7eed --- /dev/null +++ b/rfcartography/static/favicon.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/rfcartography/static/fonts/CourierPrime-Bold.ttf b/rfcartography/static/fonts/CourierPrime-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7e6b22280da62a4e804e9e66d45d01439be108ad GIT binary patch literal 69944 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π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 KuhOrl`ul$y%`id$ literal 0 HcmV?d00001 diff --git a/rfcartography/static/fonts/CourierPrime-Regular.ttf b/rfcartography/static/fonts/CourierPrime-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4af1ff54c5e2f54238a0e2013cb9a37e3130e4ca GIT binary patch literal 68304 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 Z=czZ#XSL_ndX=wQX5;$?^ZRsr{|5yaS;qhX literal 0 HcmV?d00001 diff --git a/rfcartography/static/fonts/OFL.txt b/rfcartography/static/fonts/OFL.txt new file mode 100644 index 0000000..8f1b147 --- /dev/null +++ b/rfcartography/static/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/rfcartography/templates/base.html b/rfcartography/templates/base.html new file mode 100644 index 0000000..80ff771 --- /dev/null +++ b/rfcartography/templates/base.html @@ -0,0 +1,64 @@ + + + + {% block head %} + + + + {% block head_title %} + RFCartography + {% endblock head_title %} + + + + + {% endblock head %} + + + +
+ {% block main %} + {% endblock %} +
+ + + diff --git a/rfcartography/templates/details.html b/rfcartography/templates/details.html new file mode 100644 index 0000000..4ee1e5c --- /dev/null +++ b/rfcartography/templates/details.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{%- block head_title %} +{{ doc.docID() }} :: RFCartography +{% endblock head_title %} + +{%- block main %} +

+ {{ doc.docID() }} +

+
+ {% if doc.title != "" %} +
+ Title +
+
+ {{ doc.title }} +
+ {% endif %} + {% for ref in doc.is_also %} + {% if loop.first %} +
+ a.k.a. +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} +
+
+ + RFC-Editor + +{% endblock main %} diff --git a/rfcartography/templates/generic.html b/rfcartography/templates/generic.html new file mode 100644 index 0000000..bc8eb49 --- /dev/null +++ b/rfcartography/templates/generic.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{%- block head_title %} +{{ title }} :: RFCartography +{% endblock head_title %} + +{%- block main %} +

+ {{ title }} +

+{% for chapter in content %} + {% for paragraph in chapter %} + {% if loop.first %} +

+ {{ paragraph }} +

+ {% else %} +

+ {% if paragraph is string %} + {{ paragraph }} + {% else %} + {% for line in paragraph %} + {{ line }} +
+ {% endfor %} + {% endif %} +

+ {% endif %} + {% endfor %} +{% endfor %} +{% endblock main %} diff --git a/rfcartography/templates/map.html b/rfcartography/templates/map.html new file mode 100644 index 0000000..45d0095 --- /dev/null +++ b/rfcartography/templates/map.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{%- block head_title %} +Map for {{ core_node_id }} :: RFCartography +{% endblock head_title %} + +{%- block main %} +

+ Map of {{ core_node_id }} and its Environment +

+{{ map|safe }} +
+

+ Legend +

+

+ Nodes: +

+ + {% for nodetype in nodes %} + + + + + {% endfor %} +
+ + + + + {{ nodetype.name }} +
+

+ Edges: +

+ + {% for edge in edge_style.keys() %} + + {% if edge_style[edge][0] == "solid" %} + + + {% elif edge_style[edge][0] == "dotted" %} + + + {% elif edge_style[edge][0] == "dashdot" %} + + + {% elif edge_style[edge][0] == "dashed" %} + + + {% else %} + + + {% endif %} + + {% endfor %} +
+ + + + + + {{ edge }} + + + + + + + {{ edge }} + + + + + + + {{ edge }} + + + + + + + {{ edge }} + + + + + {{ edge }} +
+
+{% endblock main %} diff --git a/rfcartography/templates/rfc.html b/rfcartography/templates/rfc.html new file mode 100644 index 0000000..1adf804 --- /dev/null +++ b/rfcartography/templates/rfc.html @@ -0,0 +1,224 @@ +{% extends "base.html" %} + +{%- block head_title %} +{{ rfc.docID() }} :: RFCartography +{% endblock head_title %} + +{%- block main %} +

+ {{ rfc.docID() }} +

+
+
+ Title +
+
+ {{ rfc.title }} +
+ {% for ref in rfc.is_also %} + {% if loop.first %} +
+ a.k.a. +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} +
+ Authors +
+
+ {% for author in rfc.authors %} + {{ author.title }} {{ author.name }} {% if not author.organization == "" %}({{ author.organization }}){% endif %}{% if not loop.last %}
{% endif %} + {% endfor %} +
+ {% if date != "" %} +
+ Publication Date +
+
+ {{ date }} +
+ {% endif %} +
+ Available as +
+
+ {% for format in rfc.format %} + {{ format.name }}{% if not loop.last %}, {% endif %} + {% endfor %} +
+ {% if rfc.page_count is not none %} +
+ Pages +
+
+ {{ rfc.page_count }} +
+ {% endif %} + {% for kw in rfc.keywords %} + {% if loop.first %} +
+ Keywords +
+
+ {% endif %} + {{ kw }}{% if not loop.last %}, {% endif %} + {% if loop.last %} +
+ {% endif %} + {% endfor %} + {% for p in rfc.abstract %} + {% if loop.first %} +
+ Abstract +
+
+ {% endif %} +

+ {{ p }} +

+ {% if loop.last %} +
+ {% endif %} + {% endfor %} + {% if rfc.draft != "" %} +
+ Draft +
+
+ {{ rfc.draft }} +
+ {% endif %} + {% if rfc.notes != "" %} +
+ Notes +
+
+ {{ rfc.notes }} +
+ {% endif %} + {% for ref in rfc.obsoletes %} + {% if loop.first %} +
+ Obsoletes +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} + {% for ref in rfc.obsoleted_by %} + {% if loop.first %} +
+ Obsoleted by +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} + {% for ref in rfc.updates %} + {% if loop.first %} +
+ Updates +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} + {% for ref in rfc.updated_by %} + {% if loop.first %} +
+ Updated by +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} + {% for ref in rfc.see_also %} + {% if loop.first %} +
+ See also +
+
+ {% endif %} + + {{ ref.docID() }} + {% if not loop.last %}, + {% else %} +
+ {% endif %} + {% endfor %} +
+ Status +
+
+ {{ rfc.current_status.name }} {% if rfc.current_status != rfc.pub_status %}(originally published as {{ rfc.pub_status.name }}){% endif %} +
+ {% if rfc.stream is not none %} +
+ Stream +
+
+ {{ rfc.stream.name }} +
+ {% endif %} + {% if rfc.area != "" %} +
+ Area +
+
+ {{ rfc.area }} +
+ {% endif %} + {% if rfc.wg_acronym != "" %} +
+ Working Group +
+
+ {{ rfc.wg_acronym }} +
+ {% endif %} + {% if rfc.doi != "" %} +
+ DOI +
+
+ {{ rfc.doi }} +
+ {% endif %} +
+
+ + RFC-Editor + + {% if rfc.errata_url != "" %} | + + Errata + + {% endif %} +{% endblock main %} diff --git a/rfcartography/templates/search.html b/rfcartography/templates/search.html new file mode 100644 index 0000000..c095f25 --- /dev/null +++ b/rfcartography/templates/search.html @@ -0,0 +1,158 @@ +{% extends "base.html" %} + +{% block head %} + {{ super() }} + +{% endblock %} + +{%- block head_title %} +RFCartography +{% endblock head_title %} + +{%- block main %} +

+ Generate Maps of RFCs and their Relations +

+ +{% endblock main %} diff --git a/tests/test_details.py b/tests/test_details.py new file mode 100644 index 0000000..8580d73 --- /dev/null +++ b/tests/test_details.py @@ -0,0 +1,91 @@ +from unittest import TestCase +from flask import Flask +from rfcartography import create_app +from rfcartography.index_parser import RFC, NotIssued, STD, BCP, FYI, DocType +from rfcartography.rfcartographer import RFCartographer + + +class TestDetailsPages(TestCase): + def setUp(self): + """create an app object for testing purposes""" + self.app:Flask = create_app() + self.app.config.update({'TESTING': True}) + + rfc42: RFC = RFC(42) + rfc23: NotIssued = NotIssued(23) + std42: STD = STD(42) + bcp42: BCP = BCP(42) + fyi42: FYI = FYI(42) + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {42: rfc42, + 23: rfc23}, + DocType.STD: {42: std42}, + DocType.BCP: {42: bcp42}, + DocType.FYI: {42: fyi42}, + DocType.NIC: {}, + DocType.IEN: {}, + DocType.RTR: {}} + self.app.cartographer = RFCartographer(index) + return + + def test_rfc_details(self): + """testing the details page for RFCs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/RFC0042') + self.assertEqual(response.status, '200 OK') + return + + def test_rfc_not_issued_details(self): + """testing the details page for not issued RFCs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/RFC0023') + self.assertEqual(response.status, '200 OK') + return + + def test_not_existing_rfc_details(self): + """testing the details page requests for not existing RFCs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/RFC0666') + self.assertEqual(response.status, '404 NOT FOUND') + return + + def test_std_details(self): + """testing the details page for STDs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/STD0042') + self.assertEqual(response.status, '200 OK') + return + + def test_not_existing_std_details(self): + """testing the details page requests for not existing STDs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/STD0666') + self.assertEqual(response.status, '404 NOT FOUND') + return + + def test_bcp_details(self): + """testing the details page for BCPs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/BCP0042') + self.assertEqual(response.status, '200 OK') + return + + def test_not_existing_bcp_details(self): + """testing the details page requests for not existing BCPs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/BCP0666') + self.assertEqual(response.status, '404 NOT FOUND') + return + + def test_fyi_details(self): + """testing the details page for FYIs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/FYI0042') + self.assertEqual(response.status, '200 OK') + return + + def test_not_existing_fyi_details(self): + """testing the details page requests for not existing FYIs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/FYI0666') + self.assertEqual(response.status, '404 NOT FOUND') + return diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..590ba88 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,34 @@ +from unittest import TestCase +from flask import Flask +from flask.testing import FlaskClient +from werkzeug.test import TestResponse +from rfcartography import create_app + + +class TestErrorHandling(TestCase): + def setUp(self): + """create an app object for testing purposes""" + self.app:Flask = create_app() + self.app.config.update({"TESTING": True}) + return + + def test_400(self): + """testing handling of malformed requests""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/map?type=foo&num=bar') + self.assertEqual(response.status, '400 BAD REQUEST') + return + + def test_404(self): + """testing handling of requests for non-existing ressources""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/foobar') + self.assertEqual(response.status, '404 NOT FOUND') + return + + def test_405(self): + """testing handling of invalid request methods""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.post('/') + self.assertEqual(response.status, '405 METHOD NOT ALLOWED') + return diff --git a/tests/test_index_parser.py b/tests/test_index_parser.py new file mode 100644 index 0000000..28de27d --- /dev/null +++ b/tests/test_index_parser.py @@ -0,0 +1,715 @@ +from unittest import TestCase +from rfcartography.index_parser import Author, Status, FileFormat, RFC, NotIssued,\ + STD, BCP, FYI, NIC, IEN, RTR, IndexParser,\ + DocType, Stream, Document +from datetime import date + + +class TestDocumentClasses(TestCase): + def test_rfc_id(self): + """testing docID for RFCs""" + rfc: RFC = RFC(791, + "Internet Protocol", + Author("J. Postel"), + date(1981, 9, 1), + Status.INTERNET_STANDARD, + Status.INTERNET_STANDARD) + self.assertEqual(rfc.docID(), 'RFC0791') + return + + def test_not_issued_id(self): + """testing docID for not issued RFCs""" + not_issued: NotIssued = NotIssued(715) + self.assertEqual(not_issued.docID(), 'RFC0715') + return + + def test_std_id(self): + """testing docID for STDs""" + std: STD = STD(6, "User Datagram Protocol") + self.assertEqual(std.docID(), 'STD0006') + return + + def test_bcp_id(self): + """testing docID for BCPs""" + bcp: BCP = BCP(200, "IAB and IESG Statement on Cryptographic Technology and the Internet") + self.assertEqual(bcp.docID(), 'BCP0200') + return + + def test_fyi_id(self): + """testing docID for FYIs""" + fyi: FYI = FYI(5, "Choosing a name for your computer") + self.assertEqual(fyi.docID(), 'FYI0005') + return + + def test_nic_id(self): + """testing docID for NICs""" + nic: NIC = NIC(42) + self.assertEqual(nic.docID(), 'NIC42') + return + + def test_ien_id(self): + """testing docID for IENs""" + ien: IEN = IEN(42) + self.assertEqual(ien.docID(), 'IEN42') + return + + def test_rtr_id(self): + """testing docID for RTRs""" + rtr: RTR = RTR(42) + self.assertEqual(rtr.docID(), 'RTR42') + return + + def test_rfc_refs(self): + """testing get_references() for RFCs""" + rfc23: RFC = RFC( 23) + rfc1337: RFC = RFC(1337) + rfc12: RFC = RFC( 12) + rfc13: RFC = RFC( 13) + rfc52: RFC = RFC( 52) + rfc53: RFC = RFC( 53) + bcp1: BCP = BCP( 1) + std2: STD = STD( 2) + ien4223: IEN = IEN(4223) + nic42: NIC = NIC( 42) + rfc: RFC = RFC(42, + "Test", + Author("J. Doe"), + date(1970, 1, 1), + Status.INTERNET_STANDARD, + Status.INTERNET_STANDARD, + obsoletes=[rfc23], + obsoleted_by=[rfc1337], + updates=[rfc12, rfc13], + updated_by=[rfc52, rfc53], + is_also=[bcp1, std2], + see_also=[ien4223, nic42]) + reflist: list = rfc.get_references() + self.assertEqual(len(reflist), 10) + self.assertIn(("obsoletes", rfc23), reflist) + self.assertIn(("obsoleted by", rfc1337), reflist) + self.assertIn(("updates", rfc12), reflist) + self.assertIn(("updates", rfc13), reflist) + self.assertIn(("updated by", rfc52), reflist) + self.assertIn(("updated by", rfc53), reflist) + self.assertIn(("is also", bcp1), reflist) + self.assertIn(("is also", std2), reflist) + self.assertIn(("see also", ien4223), reflist) + self.assertIn(("see also", nic42), reflist) + return + + def test_not_issued_refs(self): + """testing get_references() for not issued RFCs""" + self.assertEqual(NotIssued(715).get_references(), []) + return + + def test_std_ref(self): + """testing get_references() for STDs""" + rfc42: RFC = RFC(42) + nic42: NIC = NIC(42) + std: STD = STD(42, + "test", + [rfc42, nic42]) + reflist: list = std.get_references() + self.assertEqual(len(reflist), 2) + self.assertIn(("is also", rfc42), reflist) + self.assertIn(("is also", nic42), reflist) + return + + def test_bcp_ref(self): + """testing get_references() for BCPs""" + rfc42: RFC = RFC(42) + nic42: NIC = NIC(42) + bcp: BCP = BCP(42, + "test", + [rfc42, nic42]) + reflist: list = bcp.get_references() + self.assertEqual(len(reflist), 2) + self.assertIn(("is also", rfc42), reflist) + self.assertIn(("is also", nic42), reflist) + return + + def test_fyi_ref(self): + """testing get_references() for FYIs""" + rfc42: RFC = RFC(42) + nic42: NIC = NIC(42) + fyi: FYI = FYI(42, + "test", + [rfc42, nic42]) + reflist: list = fyi.get_references() + self.assertEqual(len(reflist), 2) + self.assertIn(("is also", rfc42), reflist) + self.assertIn(("is also", nic42), reflist) + return + + def test_nic_refs(self): + """testing get_references() for NICs""" + self.assertEqual(NIC(42).get_references(), []) + return + + def test_ien_refs(self): + """testing get_references() for IENs""" + self.assertEqual(IEN(42).get_references(), []) + return + + def test_rtr_refs(self): + """testing get_references() for RTRs""" + self.assertEqual(RTR(42).get_references(), []) + return + + def test_rfc_update(self): + """testing RFC updates""" + ref: NIC = NIC(42) + rfc: RFC = RFC(42) + rfc.update(title="test", + authors=[Author("test")], + pub_date=date(1984, 1, 1), + current_status=Status.EXPERIMENTAL, + pub_status=Status.PROPOSED_STANDARD, + format=FileFormat.PDF, + page_count=42, + keywords=["foo"], + abstract=["bar"], + draft="draft", + notes="notes", + obsoletes=[ref], + obsoleted_by=[ref], + updates=[ref], + updated_by=[ref], + is_also=[ref], + see_also=[ref], + stream=Stream.IETF, + area="area", + wg_acronym="wg_acronym", + errata_url="errata_url", + doi="doi") + self.assertEqual(rfc.title, "test") + self.assertEqual(len(rfc.authors), 1) + self.assertEqual(rfc.authors[0].name, "test") + self.assertEqual(rfc.pub_date.year, 1984) + self.assertEqual(rfc.pub_date.month, 1) + self.assertEqual(rfc.pub_date.day, 1) + self.assertEqual(rfc.current_status, Status.EXPERIMENTAL) + self.assertEqual(rfc.pub_status, Status.PROPOSED_STANDARD) + self.assertEqual(rfc.format, FileFormat.PDF) + self.assertEqual(rfc.page_count, 42) + self.assertEqual(len(rfc.keywords), 1) + self.assertEqual(rfc.keywords[0], "foo") + self.assertEqual(len(rfc.abstract), 1) + self.assertEqual(rfc.abstract[0], "bar") + self.assertEqual(rfc.draft, "draft") + self.assertEqual(rfc.notes, "notes") + self.assertEqual(len(rfc.obsoletes), 1) + self.assertEqual(rfc.obsoletes[0], ref) + self.assertEqual(len(rfc.obsoleted_by), 1) + self.assertEqual(rfc.obsoleted_by[0], ref) + self.assertEqual(len(rfc.updates), 1) + self.assertEqual(rfc.updates[0], ref) + self.assertEqual(len(rfc.updated_by), 1) + self.assertEqual(rfc.updated_by[0], ref) + self.assertEqual(len(rfc.is_also), 1) + self.assertEqual(rfc.is_also[0], ref) + self.assertEqual(len(rfc.see_also), 1) + self.assertEqual(rfc.see_also[0], ref) + self.assertEqual(rfc.stream, Stream.IETF) + self.assertEqual(rfc.area, "area") + self.assertEqual(rfc.wg_acronym, "wg_acronym") + self.assertEqual(rfc.errata_url, "errata_url") + self.assertEqual(rfc.doi, "doi") + return + + def test_std_update(self): + """testing STD updates""" + ref: NIC = NIC(42) + std: STD = STD(42) + std.update(title="title", + is_also=[ref]) + self.assertEqual(std.title, "title") + self.assertEqual(len(std.is_also), 1) + self.assertEqual(std.is_also[0], ref) + return + + def test_bcp_update(self): + """testing BCP updates""" + ref: NIC = NIC(42) + bcp: BCP = BCP(42) + bcp.update(title="title", + is_also=[ref]) + self.assertEqual(bcp.title, "title") + self.assertEqual(len(bcp.is_also), 1) + self.assertEqual(bcp.is_also[0], ref) + return + + def test_fyi_update(self): + """testing FYI updates""" + ref: NIC = NIC(42) + fyi: FYI = FYI(42) + fyi.update(title="title", + is_also=[ref]) + self.assertEqual(fyi.title, "title") + self.assertEqual(len(fyi.is_also), 1) + self.assertEqual(fyi.is_also[0], ref) + return + + def test_not_issued_update(self): + """testing RFC not issued updates""" + before: NotIssued = NotIssued(42) + after: NotIssued = before.update(title="test") + self.assertEqual(before, after) + return + + def test_nic_update(self): + """testing NIC updates""" + before: NIC = NIC(42) + after: NIC = before.update(title="test") + self.assertEqual(before, after) + return + + def test_ien_update(self): + """testing IEN updates""" + before: IEN = IEN(42) + after: IEN = before.update(title="test") + self.assertEqual(before, after) + return + + def test_rtr_update(self): + """testing RTR updates""" + before: RTR = RTR(42) + after: RTR = before.update(title="test") + self.assertEqual(before, after) + return + + +class TestIndexParser(TestCase): + def test_bcp(self): + """testing index parser for BCP entries""" + xml: str = """ + + + BCP0001 + + + BCP0188 + + RFC7258 + + + + BCP0185 + + RFC7115 + RFC9319 + + + """ + parser: IndexParser = IndexParser(xml) + self.assertEqual(len(parser.index[DocType.BCP]), 3) + # w/o is-also + self.assertEqual(parser.index[DocType.BCP][1].docID(), 'BCP0001') + self.assertEqual(len(parser.index[DocType.BCP][1].is_also), 0) + # with single is-also + self.assertEqual(parser.index[DocType.BCP][188].docID(), 'BCP0188') + self.assertEqual(len(parser.index[DocType.BCP][188].is_also), 1) + self.assertEqual(parser.index[DocType.BCP][188].is_also[0].docID(), 'RFC7258') + # with multiple is-also + self.assertEqual(parser.index[DocType.BCP][185].docID(), 'BCP0185') + self.assertEqual(len(parser.index[DocType.BCP][185].is_also), 2) + self.assertEqual(parser.index[DocType.BCP][185].is_also[0].docID(), 'RFC7115') + self.assertEqual(parser.index[DocType.BCP][185].is_also[1].docID(), 'RFC9319') + return + + def test_fyi(self): + """testing index parser for FYI entries""" + xml: str = """ + + + FYI0042 + + + FYI0023 + + RFC1580 + + + + FYI0666 + + RFC1149 + RFC2549 + RFC6214 + + + """ + parser: IndexParser = IndexParser(xml) + self.assertEqual(len(parser.index[DocType.FYI]), 3) + # w/o is-also + self.assertEqual(parser.index[DocType.FYI][42].docID(), 'FYI0042') + self.assertEqual(len(parser.index[DocType.FYI][42].is_also), 0) + # with single is-also + self.assertEqual(parser.index[DocType.FYI][23].docID(), 'FYI0023') + self.assertEqual(len(parser.index[DocType.FYI][23].is_also), 1) + self.assertEqual(parser.index[DocType.FYI][23].is_also[0].docID(), 'RFC1580') + # with multiple is-also + self.assertEqual(parser.index[DocType.FYI][666].docID(), 'FYI0666') + self.assertEqual(len(parser.index[DocType.FYI][666].is_also), 3) + self.assertEqual(parser.index[DocType.FYI][666].is_also[0].docID(), 'RFC1149') + self.assertEqual(parser.index[DocType.FYI][666].is_also[1].docID(), 'RFC2549') + self.assertEqual(parser.index[DocType.FYI][666].is_also[2].docID(), 'RFC6214') + return + + def test_rfc(self): + """testing index parser for RFC entries""" + xml: str = """ + + + RFC0023 + Foo + + J. Doe + + + April + 2042 + + PROPOSED STANDARD + INFORMATIONAL + + + RFC0042 + Bar + + J. Doe + Editor + A Company that Makes Everything + ACME + + + 28 + February + 2042 + + + ASCII + + 42 + + foo + + +

This is a test

+
+ draft-test-test-test + this is a note + + RFC0023 + + + RFC0666 + + + RFC0023 + + + RFC0666 + + + BCP0666 + + + RFC0023 + + HISTORIC + UNKNOWN + IETF + foo + bar + http://example.org + 10.17487/RFC0042 +
+ + RFC0666 + FooBar + + J. Doe + Editor + A Company that Makes Everything + ACME + + + M. Mustermann + + + 28 + February + 2042 + + + TEXT + HTML + + 666 + + foo + bar + + +

This is a test

+

more testing

+
+ draft-test-test-test + this is a note + + RFC0023 + RFC0042 + + + RFC1337 + RFC6666 + + + RFC0023 + RFC0042 + + + RFC1337 + RFC6666 + + + BCP6666 + STD0666 + + + RFC0023 + RFC0042 + + INTERNET STANDARD + DRAFT STANDARD + INDEPENDENT + foo + bar + http://example.org + 10.17487/RFC0666 +
+
""" + parser: IndexParser = IndexParser(xml) + self.assertEqual(len(parser.index[DocType.RFC]), 5) + # minimal data + self.assertEqual(parser.index[DocType.RFC][23].docID(), 'RFC0023') + self.assertEqual(len(parser.index[DocType.RFC][23].is_also), 0) + self.assertEqual(parser.index[DocType.RFC][23].title, "Foo") + self.assertEqual(len(parser.index[DocType.RFC][23].authors), 1) + self.assertEqual(parser.index[DocType.RFC][23].authors[0].name, "J. Doe") + self.assertEqual(parser.index[DocType.RFC][23].authors[0].title, "") + self.assertEqual(parser.index[DocType.RFC][23].authors[0].organization, "") + self.assertEqual(parser.index[DocType.RFC][23].authors[0].org_abbrev, "") + self.assertEqual(parser.index[DocType.RFC][23].pub_date.year, 2042) + self.assertEqual(parser.index[DocType.RFC][23].pub_date.month, 4) + self.assertEqual(parser.index[DocType.RFC][23].pub_date.day, 1) + self.assertEqual(len(parser.index[DocType.RFC][23].format), 0) + self.assertEqual(parser.index[DocType.RFC][23].current_status, Status.PROPOSED_STANDARD) + self.assertEqual(parser.index[DocType.RFC][23].pub_status, Status.INFORMATIONAL) + self.assertIsNone(parser.index[DocType.RFC][23].page_count) + self.assertEqual(len(parser.index[DocType.RFC][23].keywords), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].abstract), 0) + self.assertEqual(parser.index[DocType.RFC][23].draft, "") + self.assertEqual(parser.index[DocType.RFC][23].notes, "") + self.assertEqual(len(parser.index[DocType.RFC][23].obsoletes), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].obsoleted_by), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].updates), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].updated_by), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].is_also), 0) + self.assertEqual(len(parser.index[DocType.RFC][23].see_also), 0) + self.assertIsNone(parser.index[DocType.RFC][23].stream) + self.assertEqual(parser.index[DocType.RFC][23].area, "") + self.assertEqual(parser.index[DocType.RFC][23].wg_acronym, "") + self.assertEqual(parser.index[DocType.RFC][23].errata_url, "") + self.assertEqual(parser.index[DocType.RFC][23].doi, "") + # single entry for each data entry + self.assertEqual(parser.index[DocType.RFC][42].docID(), 'RFC0042') + self.assertEqual(parser.index[DocType.RFC][42].title, "Bar") + self.assertEqual(len(parser.index[DocType.RFC][42].authors), 1) + self.assertEqual(parser.index[DocType.RFC][42].authors[0].name, "J. Doe") + self.assertEqual(parser.index[DocType.RFC][42].authors[0].title, "Editor") + self.assertEqual(parser.index[DocType.RFC][42].authors[0].organization, "A Company that Makes Everything") + self.assertEqual(parser.index[DocType.RFC][42].authors[0].org_abbrev, "ACME") + self.assertEqual(parser.index[DocType.RFC][42].pub_date.year, 2042) + self.assertEqual(parser.index[DocType.RFC][42].pub_date.month, 2) + self.assertEqual(parser.index[DocType.RFC][42].pub_date.day, 28) + self.assertEqual(len(parser.index[DocType.RFC][42].format), 1) + self.assertEqual(parser.index[DocType.RFC][42].format[0], FileFormat.ASCII) + self.assertEqual(parser.index[DocType.RFC][42].current_status, Status.HISTORIC) + self.assertEqual(parser.index[DocType.RFC][42].pub_status, Status.UNKNOWN) + self.assertEqual(parser.index[DocType.RFC][42].page_count, 42) + self.assertEqual(len(parser.index[DocType.RFC][42].keywords), 1) + self.assertEqual(parser.index[DocType.RFC][42].keywords[0], "foo") + self.assertEqual(len(parser.index[DocType.RFC][42].abstract), 1) + self.assertEqual(parser.index[DocType.RFC][42].abstract[0], "This is a test") + self.assertEqual(parser.index[DocType.RFC][42].draft, "draft-test-test-test") + self.assertEqual(parser.index[DocType.RFC][42].notes, "this is a note") + self.assertEqual(len(parser.index[DocType.RFC][42].obsoletes), 1) + self.assertEqual(parser.index[DocType.RFC][42].obsoletes[0].docID(), 'RFC0023') + self.assertEqual(len(parser.index[DocType.RFC][42].obsoleted_by), 1) + self.assertEqual(parser.index[DocType.RFC][42].obsoleted_by[0].docID(), 'RFC0666') + self.assertEqual(len(parser.index[DocType.RFC][42].updates), 1) + self.assertEqual(parser.index[DocType.RFC][42].updates[0].docID(), 'RFC0023') + self.assertEqual(len(parser.index[DocType.RFC][42].updated_by), 1) + self.assertEqual(parser.index[DocType.RFC][42].updated_by[0].docID(), 'RFC0666') + self.assertEqual(len(parser.index[DocType.RFC][42].is_also), 1) + self.assertEqual(parser.index[DocType.RFC][42].is_also[0].docID(), 'BCP0666') + self.assertEqual(len(parser.index[DocType.RFC][42].see_also), 1) + self.assertEqual(parser.index[DocType.RFC][42].see_also[0].docID(), 'RFC0023') + self.assertEqual(parser.index[DocType.RFC][42].stream, Stream.IETF) + self.assertEqual(parser.index[DocType.RFC][42].area, "foo") + self.assertEqual(parser.index[DocType.RFC][42].wg_acronym, "bar") + self.assertEqual(parser.index[DocType.RFC][42].errata_url, "http://example.org") + self.assertEqual(parser.index[DocType.RFC][42].doi, "10.17487/RFC0042") + # multiple entries where allowed + self.assertEqual(parser.index[DocType.RFC][666].docID(), 'RFC0666') + self.assertEqual(parser.index[DocType.RFC][666].title, "FooBar") + self.assertEqual(len(parser.index[DocType.RFC][666].authors), 2) + self.assertEqual(parser.index[DocType.RFC][666].authors[0].name, "J. Doe") + self.assertEqual(parser.index[DocType.RFC][666].authors[0].title, "Editor") + self.assertEqual(parser.index[DocType.RFC][666].authors[0].organization, "A Company that Makes Everything") + self.assertEqual(parser.index[DocType.RFC][666].authors[0].org_abbrev, "ACME") + self.assertEqual(parser.index[DocType.RFC][666].authors[1].name, "M. Mustermann") + self.assertEqual(parser.index[DocType.RFC][666].authors[1].title, "") + self.assertEqual(parser.index[DocType.RFC][666].authors[1].organization, "") + self.assertEqual(parser.index[DocType.RFC][666].authors[1].org_abbrev, "") + self.assertEqual(parser.index[DocType.RFC][666].pub_date.year, 2042) + self.assertEqual(parser.index[DocType.RFC][666].pub_date.month, 2) + self.assertEqual(parser.index[DocType.RFC][666].pub_date.day, 28) + self.assertEqual(len(parser.index[DocType.RFC][666].format), 2) + self.assertEqual(parser.index[DocType.RFC][666].format[0], FileFormat.TEXT) + self.assertEqual(parser.index[DocType.RFC][666].format[1], FileFormat.HTML) + self.assertEqual(parser.index[DocType.RFC][666].current_status, Status.INTERNET_STANDARD) + self.assertEqual(parser.index[DocType.RFC][666].pub_status, Status.DRAFT_STANDARD) + self.assertEqual(parser.index[DocType.RFC][666].page_count, 666) + self.assertEqual(len(parser.index[DocType.RFC][666].keywords), 2) + self.assertEqual(parser.index[DocType.RFC][666].keywords[0], "foo") + self.assertEqual(parser.index[DocType.RFC][666].keywords[1], "bar") + self.assertEqual(len(parser.index[DocType.RFC][666].abstract), 2) + self.assertEqual(parser.index[DocType.RFC][666].abstract[0], "This is a test") + self.assertEqual(parser.index[DocType.RFC][666].abstract[1], "more testing") + self.assertEqual(parser.index[DocType.RFC][666].draft, "draft-test-test-test") + self.assertEqual(parser.index[DocType.RFC][666].notes, "this is a note") + self.assertEqual(len(parser.index[DocType.RFC][666].obsoletes), 2) + self.assertEqual(parser.index[DocType.RFC][666].obsoletes[0].docID(), 'RFC0023') + self.assertEqual(parser.index[DocType.RFC][666].obsoletes[1].docID(), 'RFC0042') + self.assertEqual(len(parser.index[DocType.RFC][666].obsoleted_by), 2) + self.assertEqual(parser.index[DocType.RFC][666].obsoleted_by[0].docID(), 'RFC1337') + self.assertEqual(parser.index[DocType.RFC][666].obsoleted_by[1].docID(), 'RFC6666') + self.assertEqual(len(parser.index[DocType.RFC][666].updates), 2) + self.assertEqual(parser.index[DocType.RFC][666].updates[0].docID(), 'RFC0023') + self.assertEqual(parser.index[DocType.RFC][666].updates[1].docID(), 'RFC0042') + self.assertEqual(len(parser.index[DocType.RFC][666].updated_by), 2) + self.assertEqual(parser.index[DocType.RFC][666].updated_by[0].docID(), 'RFC1337') + self.assertEqual(parser.index[DocType.RFC][666].updated_by[1].docID(), 'RFC6666') + self.assertEqual(len(parser.index[DocType.RFC][666].is_also), 2) + self.assertEqual(parser.index[DocType.RFC][666].is_also[0].docID(), 'BCP6666') + self.assertEqual(parser.index[DocType.RFC][666].is_also[1].docID(), 'STD0666') + self.assertEqual(len(parser.index[DocType.RFC][666].see_also), 2) + self.assertEqual(parser.index[DocType.RFC][666].see_also[0].docID(), 'RFC0023') + self.assertEqual(parser.index[DocType.RFC][666].see_also[1].docID(), 'RFC0042') + self.assertEqual(parser.index[DocType.RFC][666].stream, Stream.INDEPENDENT) + self.assertEqual(parser.index[DocType.RFC][666].area, "foo") + self.assertEqual(parser.index[DocType.RFC][666].wg_acronym, "bar") + self.assertEqual(parser.index[DocType.RFC][666].errata_url, "http://example.org") + self.assertEqual(parser.index[DocType.RFC][666].doi, "10.17487/RFC0666") + return + + def test_std(self): + """testing index parser for STD entries""" + xml: str = """ + + + STD0666 + test + + + STD0099 + HTTP/1.1 + + RFC9112 + + + + STD0078 + Simple Network Management Protocol (SNMP) Security + + RFC5343 + RFC5590 + RFC5591 + RFC6353 + + + """ + parser: IndexParser = IndexParser(xml) + self.assertEqual(len(parser.index[DocType.STD]), 3) + # w/o is-also + self.assertEqual(parser.index[DocType.STD][666].docID(), 'STD0666') + self.assertEqual(parser.index[DocType.STD][666].title, 'test') + self.assertEqual(len(parser.index[DocType.STD][666].is_also), 0) + # with single is-also + self.assertEqual(parser.index[DocType.STD][99].docID(), 'STD0099') + self.assertEqual(parser.index[DocType.STD][99].title, 'HTTP/1.1') + self.assertEqual(len(parser.index[DocType.STD][99].is_also), 1) + self.assertEqual(parser.index[DocType.STD][99].is_also[0].docID(), 'RFC9112') + # with multiple is-also + self.assertEqual(parser.index[DocType.STD][78].docID(), 'STD0078') + self.assertEqual(parser.index[DocType.STD][78].title, 'Simple Network Management Protocol (SNMP) Security') + self.assertEqual(len(parser.index[DocType.STD][78].is_also), 4) + self.assertEqual(parser.index[DocType.STD][78].is_also[0].docID(), 'RFC5343') + self.assertEqual(parser.index[DocType.STD][78].is_also[1].docID(), 'RFC5590') + self.assertEqual(parser.index[DocType.STD][78].is_also[2].docID(), 'RFC5591') + self.assertEqual(parser.index[DocType.STD][78].is_also[3].docID(), 'RFC6353') + return + + def test_not_issued(self): + """testing index parser for RFC not issued entries""" + xml: str = """ + + + RFC0042 + + """ + parser: IndexParser = IndexParser(xml) + self.assertEqual(len(parser.index[DocType.RFC]), 1) + self.assertEqual(parser.index[DocType.RFC][42].docID(), 'RFC0042') + return + + def test_get_index(self): + """testing the parser's get_index() function""" + xml: str = """ + + + RFC0042 + + """ + parser: IndexParser = IndexParser(xml) + index: dict[DocType: dict[int, Document]] = parser.get_index() + self.assertIn(DocType.RFC, index) + self.assertIn(DocType.STD, index) + self.assertIn(DocType.BCP, index) + self.assertIn(DocType.FYI, index) + self.assertIn(DocType.NIC, index) + self.assertIn(DocType.IEN, index) + self.assertIn(DocType.RTR, index) + self.assertEqual(index[DocType.RFC][42].docID(), 'RFC0042') + return diff --git a/tests/test_rfcartographer.py b/tests/test_rfcartographer.py new file mode 100644 index 0000000..bfa85e4 --- /dev/null +++ b/tests/test_rfcartographer.py @@ -0,0 +1,253 @@ +from unittest import TestCase +from networkx import MultiDiGraph +from rfcartography.rfcartographer import RFCartographer, RFCMap +from rfcartography.index_parser import DocType, Document, RFC, STD, BCP, FYI, NIC, IEN, RTR + + +class TestCartographer(TestCase): + def test_get_document(self): + """testing document retrieval""" + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {42: RFC(42)}, + DocType.STD: {23: STD(23)}, + DocType.BCP: {42: BCP(42)}, + DocType.FYI: {23: FYI(23)}, + DocType.NIC: {42: NIC(42)}, + DocType.IEN: {23: IEN(23)}, + DocType.RTR: {42: RTR(42)}} + cartographer: RFCartographer = RFCartographer(index) + self.assertEqual(cartographer.get_document(DocType.RFC, 42).docID(), 'RFC0042') + self.assertEqual(cartographer.get_document(DocType.STD, 23).docID(), 'STD0023') + self.assertEqual(cartographer.get_document(DocType.BCP, 42).docID(), 'BCP0042') + self.assertEqual(cartographer.get_document(DocType.FYI, 23).docID(), 'FYI0023') + self.assertEqual(cartographer.get_document(DocType.NIC, 42).docID(), 'NIC42') + self.assertEqual(cartographer.get_document(DocType.IEN, 23).docID(), 'IEN23') + self.assertEqual(cartographer.get_document(DocType.RTR, 42).docID(), 'RTR42') + return + + def test_map_generation(self): + """testing generation of RFCMaps""" + nic42: NIC = NIC(42) + ien23: IEN = IEN(23) + rtr42: RTR = RTR(42) + rfc42: RFC = RFC(42, see_also=[nic42, ien23, rtr42]) + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {42: rfc42}, + DocType.STD: {}, + DocType.BCP: {}, + DocType.FYI: {}, + DocType.NIC: {42: nic42}, + DocType.IEN: {23: ien23}, + DocType.RTR: {42: rtr42}} + cartographer: RFCartographer = RFCartographer(index) + rfc_map: RFCMap = cartographer.map_subnet(rfc42, "http://example.org/") + self.assertIn(rfc42, rfc_map.nodes[DocType.RFC]) + self.assertIn(nic42, rfc_map.nodes[DocType.NIC]) + self.assertIn(ien23, rfc_map.nodes[DocType.IEN]) + self.assertIn(rtr42, rfc_map.nodes[DocType.RTR]) + self.assertIn((rfc42, nic42), rfc_map.edges['see also']) + self.assertIn((rfc42, ien23), rfc_map.edges['see also']) + self.assertIn((rfc42, rtr42), rfc_map.edges['see also']) + return + + def test_map_generation_depth_limit(self): + """testing generation of RFCMaps with depth limit""" + rfc3: RFC = RFC(3) + rfc2: RFC = RFC(2, obsoleted_by=[rfc3]) + rfc1: RFC = RFC(1, obsoleted_by=[rfc2]) + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {1: rfc1, + 2: rfc2, + 3: rfc3}, + DocType.STD: {}, + DocType.BCP: {}, + DocType.FYI: {}, + DocType.NIC: {}, + DocType.IEN: {}, + DocType.RTR: {}} + cartographer: RFCartographer = RFCartographer(index) + rfc_map: RFCMap = cartographer.map_subnet(rfc1,"http://example.org/", 1) + self.assertEqual(rfc_map.get_node_count(), 2) + return + + def test_map_generation_style_params(self): + """testing generation of RFCMaps with style parameters""" + rfc42: RFC = RFC(42) + node_colors: dict[DocType, str] = {DocType.RFC: '#aaaaaa', + DocType.STD: '#bbbbbb', + DocType.BCP: '#cccccc', + DocType.FYI: '#dddddd', + DocType.NIC: '#eeeeee', + DocType.IEN: '#ffffff', + DocType.RTR: '#000000'} + edge_style: dict[str, tuple[str, str]] = {'obsoletes': ('dashed', '#111111'), + 'obsoleted by': ('dashed', '#222222'), + 'updates': ('dashdot', '#333333'), + 'updated by': ('dashdot', '#444444'), + 'is also': ('solid', '#555555'), + 'see also': ('dotted', '#666666')} + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {42: rfc42}, + DocType.STD: {}, + DocType.BCP: {}, + DocType.FYI: {}, + DocType.NIC: {}, + DocType.IEN: {}, + DocType.RTR: {}} + cartographer: RFCartographer = RFCartographer(index) + rfc_map: RFCMap = cartographer.map_subnet(rfc42,"http://example.org/", 0, + node_color=node_colors, + edge_style=edge_style) + self.assertEqual(rfc_map.get_node_colors(), node_colors) + self.assertEqual(rfc_map.get_edge_styles(), edge_style) + return + + +class TestMap(TestCase): + def test_node_color(self): + """testing node color update and retrieval""" + node_colors: dict[DocType, str] = {DocType.RFC: '#aaaaaa', + DocType.STD: '#bbbbbb', + DocType.BCP: '#cccccc', + DocType.FYI: '#dddddd', + DocType.NIC: '#eeeeee', + DocType.IEN: '#ffffff', + DocType.RTR: '#000000'} + rfc_map: RFCMap = RFCMap(MultiDiGraph(), + {DocType.RFC: [], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [], + DocType.IEN: [], + DocType.RTR: []}, + {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': []}, + 'https://example.org/', + node_color=node_colors) + self.assertEqual(rfc_map.get_node_colors(), node_colors) + rfc_map.set_node_color(DocType.RTR, '#123456') + node_colors[DocType.RTR] = '#123456' + self.assertEqual(rfc_map.get_node_colors(), node_colors) + return + + def test_edge_style(self): + """testing edge style update and retrieval""" + edge_style: dict[str, tuple[str, str]] = {'obsoletes': ('dashed', '#111111'), + 'obsoleted by': ('dashed', '#222222'), + 'updates': ('dashdot', '#333333'), + 'updated by': ('dashdot', '#444444'), + 'is also': ('solid', '#555555'), + 'see also': ('dotted', '#666666')} + rfc_map: RFCMap = RFCMap(MultiDiGraph(), + {DocType.RFC: [], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [], + DocType.IEN: [], + DocType.RTR: []}, + {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': []}, + 'https://example.org/', + edge_style=edge_style) + self.assertEqual(rfc_map.get_edge_styles(), edge_style) + rfc_map.set_edge_style('updates', '#ffffff') + edge_style['updates'] = '#ffffff' + self.assertEqual(rfc_map.get_edge_styles(), edge_style) + return + + def test_url_base(self): + """testing url base url update and retrieval""" + rfc_map: RFCMap = RFCMap(MultiDiGraph(), + {DocType.RFC: [], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [], + DocType.IEN: [], + DocType.RTR: []}, + {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': []}, + 'https://example.org/') + self.assertEqual(rfc_map.get_url_base(), 'https://example.org/') + rfc_map.set_url_base('https://rfc-editor.org/') + self.assertEqual(rfc_map.get_url_base(), 'https://rfc-editor.org/') + return + + def test_draw(self): + """testing generation of a svg""" + nic42: NIC = NIC(42) + ien23: IEN = IEN(23) + rtr42: RTR = RTR(42) + rfc42: RFC = RFC(42, see_also=[nic42, ien23, rtr42]) + nodes: dict[DocType, list[Document]] = {DocType.RFC: [rfc42], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [nic42], + DocType.IEN: [ien23], + DocType.RTR: [rtr42]} + edges: dict[str, tuple[Document, Document]] = {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': [(rfc42, nic42), + (rfc42, ien23), + (rfc42, rtr42)]} + graph: MultiDiGraph = MultiDiGraph() + graph.add_node(rfc42) + graph.add_node(nic42) + graph.add_node(ien23) + graph.add_node(rtr42) + graph.add_edge(rfc42, nic42, reftype='see also') + graph.add_edge(rfc42, ien23, reftype='see also') + graph.add_edge(rfc42, rtr42, reftype='see also') + rfc_map: RFCMap = RFCMap(graph, nodes, edges, 'https://example.org/') + svg: str = rfc_map.draw() + self.assertEqual(svg[:4], '\n') + return + + def test_counters(self): + """testing node and edge count retrieval""" + nic42: NIC = NIC(42) + ien23: IEN = IEN(23) + rtr42: RTR = RTR(42) + rfc42: RFC = RFC(42, see_also=[nic42, ien23, rtr42]) + nodes: dict[DocType, list[Document]] = {DocType.RFC: [rfc42], + DocType.STD: [], + DocType.BCP: [], + DocType.FYI: [], + DocType.NIC: [nic42], + DocType.IEN: [ien23], + DocType.RTR: [rtr42]} + edges: dict[str, tuple[Document, Document]] = {'obsoletes': [], + 'obsoleted by': [], + 'updates': [], + 'updated by': [], + 'is also': [], + 'see also': [(rfc42, nic42), + (rfc42, ien23), + (rfc42, rtr42)]} + graph: MultiDiGraph = MultiDiGraph() + graph.add_node(rfc42) + graph.add_node(nic42) + graph.add_node(ien23) + graph.add_node(rtr42) + graph.add_edge(rfc42, nic42, reftype='see also') + graph.add_edge(rfc42, ien23, reftype='see also') + graph.add_edge(rfc42, rtr42, reftype='see also') + rfc_map: RFCMap = RFCMap(graph, nodes, edges, 'https://example.org/') + self.assertEqual(rfc_map.get_node_count(), 4) + self.assertEqual(rfc_map.get_edge_count(), 3) + return diff --git a/tests/test_routing.py b/tests/test_routing.py new file mode 100644 index 0000000..8265672 --- /dev/null +++ b/tests/test_routing.py @@ -0,0 +1,34 @@ +from unittest import TestCase +from flask import Flask +from rfcartography import create_app + +class TestDetailsPages(TestCase): + def setUp(self): + """create an app object for testing purposes""" + self.app:Flask = create_app() + self.app.config.update({'TESTING': True}) + return + + def test_imprint(self): + """testing the imprint page""" + client: FlaskClient = self.app.test_client() + if 'IMPRINT' in self.app.config: + self.app.config.pop('IMPRINT') + response: TestResponse = client.get('/imprint') + self.assertEqual(response.status, '404 NOT FOUND') + self.app.config.update({'IMPRINT': [('Imprint', '123 test 123 test')]}) + response: TestResponse = client.get('/imprint') + self.assertEqual(response.status, '200 OK') + return + + def test_privacy(self): + """testing the privacy page""" + client: FlaskClient = self.app.test_client() + if 'PRIVACY' in self.app.config: + self.app.config.pop('PRIVACY') + response: TestResponse = client.get('/privacy') + self.assertEqual(response.status, '404 NOT FOUND') + self.app.config.update({'PRIVACY': [('Piracy Statement', 'Arrr')]}) + response: TestResponse = client.get('/privacy') + self.assertEqual(response.status, '200 OK') + return diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 0000000..64b477e --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,92 @@ +from unittest import TestCase +from flask import Flask +from werkzeug.datastructures import MultiDict +from rfcartography import create_app +from rfcartography.index_parser import RFC, NotIssued, STD, BCP, FYI, DocType +from rfcartography.rfcartographer import RFCartographer + + +class TestDetailsPages(TestCase): + def setUp(self): + """create an app object for testing purposes""" + self.app:Flask = create_app() + self.app.config.update({"TESTING": True}) + + rfc42: RFC = RFC(42) + rfc23: NotIssued = NotIssued(23) + std42: STD = STD(42) + bcp42: BCP = BCP(42) + fyi42: FYI = FYI(42) + index: dict[DocType: dict[int, Document]] = {DocType.RFC: {42: rfc42, + 23: rfc23}, + DocType.STD: {42: std42}, + DocType.BCP: {42: bcp42}, + DocType.FYI: {42: fyi42}, + DocType.NIC: {}, + DocType.IEN: {}, + DocType.RTR: {}} + self.app.cartographer = RFCartographer(index) + return + + def test_search_page(self): + """testing the start page""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/') + self.assertEqual(response.status, '200 OK') + + def test_no_search_result(self): + """testing searching for not existing RFCs""" + client: FlaskClient = self.app.test_client() + response: TestResponse = client.get('/map?type=rfc&num=666') + self.assertEqual(response.status, '404 NOT FOUND') + + def test_quicksearch_validation(self): + """testing the validation of user input in the quick search""" + client: FlaskClient = self.app.test_client() + # valid type and valid number shall be accepted + response = client.get('/map?type=rfc&num=42') + self.assertEqual(response.status, '200 OK') + # number with leading 0s shall be accepted + response = client.get('/map?type=rfc&num=0042') + self.assertEqual(response.status, '200 OK') + # number with letters shall be rejected + response = client.get('/map?type=rfc&num=42a') + self.assertEqual(response.status, '400 BAD REQUEST') + # invalid type shall be rejected + response = client.get('/map?type=test&num=42') + self.assertEqual(response.status, '400 BAD REQUEST') + # missing type shall be rejected + response = client.get('/map?num=42') + self.assertEqual(response.status, '400 BAD REQUEST') + # missing num shall be rejected + response = client.get('/map?type=test') + self.assertEqual(response.status, '400 BAD REQUEST') + return + + def test_advanced_search_validation(self): + """testing the validation of user input in the advanced search""" + client: FlaskClient = self.app.test_client() + # valid requests shall be accepted + response: TestResponse = client.get('/map?type=rfc&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '200 OK') + # invalid type values shall be rejected + response = client.get('/map?type=invalid&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '400 BAD REQUEST') + # invalid num values shall be rejected + response = client.get('/map?type=rfc&num=invalid&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '400 BAD REQUEST') + # num values with leading 0s shall be accepted + response = client.get('/map?type=rfc&num=0042&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '200 OK') + # invalid color values shall be rejected + response = client.get('/map?type=rfc&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=invalid&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + # invalid color values shall be rejected + response = client.get('/map?type=rfc&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23fail00&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '400 BAD REQUEST') + # invalid line style values shall be rejected + response = client.get('/map?type=rfc&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=ien&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=invalid&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '400 BAD REQUEST') + # invalid DocType shall be rejected + response = client.get('/map?type=rfc&num=42&depth=23&nodes_enabled=rfc&nodes_enabled=std&nodes_enabled=bcp&nodes_enabled=fyi&nodes_enabled=nic&nodes_enabled=invalid&nodes_enabled=rtr&rfc_color=%232072b1&std_color=%23c21a7e&bcp_color=%236d388d&fyi_color=%238bbd3e&nic_color=%23efe50b&ien_color=%23f28f20&rtr_color=%23e32326&obsoletes_style=dashed&obsoleted_by_style=dashed&updates_style=dashdot&updated_by_style=dashdot&is_also_style=solid&see_also_style=dotted&obsoletes_color=%23303c50&obsoleted_by_color=%23303c50&updates_color=%23607d8d&updated_by_color=%23607d8d&is_also_color=%23132e41&see_also_color=%23008e90') + self.assertEqual(response.status, '400 BAD REQUEST') + return
+ RFCartography Logo + +

+ RFCartography +

+
+ +