Version 1.0.0

This commit is contained in:
error 2023-01-03 14:42:54 +01:00
commit 4b51d678bb
33 changed files with 3693 additions and 0 deletions

164
.gitignore vendored Normal file
View File

@ -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/

9
LICENSE Normal file
View File

@ -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.

67
README.md Normal file
View File

@ -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

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
Flask
defusedxml
networkx[default]

48
rfcartography/__init__.py Normal file
View File

@ -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

43
rfcartography/details.py Normal file
View File

@ -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<int:num>', 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<int:num>', methods=['GET'], defaults={'doctype': DocType.STD})
@details.route('/BCP<int:num>', methods=['GET'], defaults={'doctype': DocType.BCP})
@details.route('/FYI<int:num>', 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

29
rfcartography/errors.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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('<?xml version="1.0" encoding="utf-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\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)

35
rfcartography/routing.py Normal file
View File

@ -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

119
rfcartography/search.py Normal file
View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>
{% block head_title %}
RFCartography
{% endblock head_title %}
</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}" sizes="any" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/reset.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/rfcartography.css') }}" />
{% endblock head %}
</head>
<body>
<header>
<img src="{{ url_for('static', filename='favicon.svg') }}" alt="RFCartography Logo" title="RFCartography" />
<a href="/">
<h1>
<span>RFC</span><span>artography</span>
</h1>
</a>
<nav>
<form action="/map" method="get" id="quicksearch">
<label for="type">Series/Subseries</label>
<select name="type" id="type" form="quicksearch">
<option value="rfc">RFC</option>
<option value="std">STD</option>
<option value="bcp">BCP</option>
<option value="fyi">FYI</option>
</select>
<label for="num">Document Number</label>
<input type="number" name="num" id="num" form="quicksearch" />
<input type="submit" value="&rarr;" form="quicksearch" />
</form>
</nav>
</header>
<main>
{% block main %}
{% endblock %}
</main>
<footer>
<ul>
<li>
<a href="{{ config['META']['SOURCE'] }}">
Source
</a>
</li>
<li>
<a href="{{ url_for('imprint') }}">
Imprint
</a>
</li>
<li>
<a href="{{ url_for('privacy') }}">
Privacy
</a>
</li>
</ul>
</footer>
</body>
</html>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{%- block head_title %}
{{ doc.docID() }} :: RFCartography
{% endblock head_title %}
{%- block main %}
<h1>
{{ doc.docID() }}
</h1>
<dl>
{% if doc.title != "" %}
<dt>
Title
</dt>
<dd>
{{ doc.title }}
</dd>
{% endif %}
{% for ref in doc.is_also %}
{% if loop.first %}
<dt>
a.k.a.
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
</dl>
<br />
<a href="https://www.rfc-editor.org/info/{{ doc.docID() | lower }}">
RFC-Editor
</a>
{% endblock main %}

View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{%- block head_title %}
{{ title }} :: RFCartography
{% endblock head_title %}
{%- block main %}
<h1>
{{ title }}
</h1>
{% for chapter in content %}
{% for paragraph in chapter %}
{% if loop.first %}
<h2>
{{ paragraph }}
</h2>
{% else %}
<p>
{% if paragraph is string %}
{{ paragraph }}
{% else %}
{% for line in paragraph %}
{{ line }}
<br />
{% endfor %}
{% endif %}
</p>
{% endif %}
{% endfor %}
{% endfor %}
{% endblock main %}

View File

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/map.css') }}" />
{% endblock %}
{%- block head_title %}
Map for {{ core_node_id }} :: RFCartography
{% endblock head_title %}
{%- block main %}
<h1>
Map of {{ core_node_id }} and its Environment
</h1>
{{ map|safe }}
<div>
<h2>
Legend
</h2>
<h3>
Nodes:
</h3>
<table>
{% for nodetype in nodes %}
<tr>
<td>
<svg viewBox="0 0 100 100" width="100" height="100" class="node">
<circle cx="50" cy="50" r="50" stroke="none" fill="{{ node_colors[nodetype] }}" />
</svg>
</td>
<td>
{{ nodetype.name }}
</td>
</tr>
{% endfor %}
</table>
<h3>
Edges:
</h3>
<table>
{% for edge in edge_style.keys() %}
<tr>
{% if edge_style[edge][0] == "solid" %}
<td>
<svg viewBox="0 0 250 60" width="250" height="60" class="edge">
<line x1="0" y1="30" x2="200" y2="30" stroke="{{ edge_style[edge][1] }}" stroke-width="10" />
<polygon points="200,0 250,30 200,60" fill="{{ edge_style[edge][1] }}" />
</svg>
</td>
<td>
{{ edge }}
</td>
{% elif edge_style[edge][0] == "dotted" %}
<td>
<svg viewBox="0 0 250 60" width="250" height="60" class="edge">
<line x1="0" y1="30" x2="200" y2="30" stroke="{{ edge_style[edge][1] }}" stroke-width="10" stroke-dasharray="10 10" />
<polygon points="200,0 250,30 200,60" fill="{{ edge_style[edge][1] }}" />
</svg>
</td>
<td>
{{ edge }}
</td>
{% elif edge_style[edge][0] == "dashdot" %}
<td>
<svg viewBox="0 0 250 60" width="250" height="60" class="edge">
<line x1="0" y1="30" x2="200" y2="30" stroke="{{ edge_style[edge][1] }}" stroke-width="10" stroke-dasharray="30 10 10 10" />
<polygon points="200,0 250,30 200,60" fill="{{ edge_style[edge][1] }}" />
</svg>
</td>
<td>
{{ edge }}
</td>
{% elif edge_style[edge][0] == "dashed" %}
<td>
<svg viewBox="0 0 250 60" width="250" height="60" class="edge">
<line x1="0" y1="30" x2="200" y2="30" stroke="{{ edge_style[edge][1] }}" stroke-width="10" stroke-dasharray="30 30" />
<polygon points="200,0 250,30 200,60" fill="{{ edge_style[edge][1] }}" />
</svg>
</td>
<td>
{{ edge }}
</td>
{% else %}
<td>
<svg viewBox="0 0 250 60" width="250" height="60" class="edge">
</svg>
</td>
<td>
{{ edge }}
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endblock main %}

View File

@ -0,0 +1,224 @@
{% extends "base.html" %}
{%- block head_title %}
{{ rfc.docID() }} :: RFCartography
{% endblock head_title %}
{%- block main %}
<h1>
{{ rfc.docID() }}
</h1>
<dl>
<dt>
Title
</dt>
<dd>
{{ rfc.title }}
</dd>
{% for ref in rfc.is_also %}
{% if loop.first %}
<dt>
a.k.a.
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
<dt>
Authors
</dt>
<dd>
{% for author in rfc.authors %}
{{ author.title }} {{ author.name }} {% if not author.organization == "" %}({{ author.organization }}){% endif %}{% if not loop.last %}<br />{% endif %}
{% endfor %}
</dd>
{% if date != "" %}
<dt>
Publication Date
</dt>
<dd>
{{ date }}
</dd>
{% endif %}
<dt>
Available as
</dt>
<dd>
{% for format in rfc.format %}
{{ format.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
</dd>
{% if rfc.page_count is not none %}
<dt>
Pages
</dt>
<dd>
{{ rfc.page_count }}
</dd>
{% endif %}
{% for kw in rfc.keywords %}
{% if loop.first %}
<dt>
Keywords
</dt>
<dd>
{% endif %}
{{ kw }}{% if not loop.last %}, {% endif %}
{% if loop.last %}
</dd>
{% endif %}
{% endfor %}
{% for p in rfc.abstract %}
{% if loop.first %}
<dt>
Abstract
</dt>
<dd>
{% endif %}
<p>
{{ p }}
</p>
{% if loop.last %}
</dd>
{% endif %}
{% endfor %}
{% if rfc.draft != "" %}
<dt>
Draft
</dt>
<dd>
{{ rfc.draft }}
</dd>
{% endif %}
{% if rfc.notes != "" %}
<dt>
Notes
</dt>
<dd>
{{ rfc.notes }}
</dd>
{% endif %}
{% for ref in rfc.obsoletes %}
{% if loop.first %}
<dt>
Obsoletes
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
{% for ref in rfc.obsoleted_by %}
{% if loop.first %}
<dt>
Obsoleted by
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
{% for ref in rfc.updates %}
{% if loop.first %}
<dt>
Updates
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
{% for ref in rfc.updated_by %}
{% if loop.first %}
<dt>
Updated by
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
{% for ref in rfc.see_also %}
{% if loop.first %}
<dt>
See also
</dt>
<dd>
{% endif %}
<a href="{{ url }}{{ ref.docID() }}">
{{ ref.docID() }}
</a>{% if not loop.last %},
{% else %}
</dd>
{% endif %}
{% endfor %}
<dt>
Status
</dt>
<dd>
{{ rfc.current_status.name }} {% if rfc.current_status != rfc.pub_status %}(originally published as {{ rfc.pub_status.name }}){% endif %}
</dd>
{% if rfc.stream is not none %}
<dt>
Stream
</dt>
<dd>
{{ rfc.stream.name }}
</dd>
{% endif %}
{% if rfc.area != "" %}
<dt>
Area
</dt>
<dd>
{{ rfc.area }}
</dd>
{% endif %}
{% if rfc.wg_acronym != "" %}
<dt>
Working Group
</dt>
<dd>
{{ rfc.wg_acronym }}
</dd>
{% endif %}
{% if rfc.doi != "" %}
<dt>
DOI
</dt>
<dd>
{{ rfc.doi }}
</dd>
{% endif %}
</dl>
<br />
<a href="https://www.rfc-editor.org/info/{{ rfc.docID() | lower }}">
RFC-Editor
</a>
{% if rfc.errata_url != "" %} |
<a href="{{ rfc.errata_url }}">
Errata
</a>
{% endif %}
{% endblock main %}

View File

@ -0,0 +1,158 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/search.css') }}" />
{% endblock %}
{%- block head_title %}
RFCartography
{% endblock head_title %}
{%- block main %}
<h1>
Generate Maps of RFCs and their Relations
</h1>
<form action="/map" method="get" id="search">
<fieldset>
<label for="type">Series/Subseries</label>
<label for="num">Document Number</label><br />
<select name="type" id="type" form="search">
<option value="rfc">RFC</option>
<option value="std">STD</option>
<option value="bcp">BCP</option>
<option value="fyi">FYI</option>
</select>
<input type="number" name="num" id="num" form="search" />
<input type="submit" value="&rarr;" form="search">
</fieldset>
<fieldset>
<legend>Note:</legend>
<div>
Large graphs result in very high loading times.
If your request times out, try a lower depth limit.
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
<fieldset>
<legend>Size Limit:</legend>
<div>
<label for="depth">Max. Depth</label><br />
<input type="number" name="depth" id="depth" form="search" value="{{ config['DEPTH_DEFAULT'] }}" />
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
<fieldset>
<legend>Nodes:</legend>
<div>
<input type="checkbox" id="rfc_enable" name="nodes_enabled" value="rfc" checked>
<label for="rfc_enable">RFC</label><br />
<input type="checkbox" id="std_enable" name="nodes_enabled" value="std" checked>
<label for="std_enable">STD</label><br />
<input type="checkbox" id="bcp_enable" name="nodes_enabled" value="bcp" checked>
<label for="bcp_enable">BCP</label><br />
<input type="checkbox" id="fyi_enable" name="nodes_enabled" value="fyi" checked>
<label for="fyi_enable">FYI</label><br />
<input type="checkbox" id="nic_enable" name="nodes_enabled" value="nic" checked>
<label for="nic_enable">NIC</label><br />
<input type="checkbox" id="ien_enable" name="nodes_enabled" value="ien" checked>
<label for="ien_enable">IEN</label><br />
<input type="checkbox" id="rtr_enable" name="nodes_enabled" value="rtr" checked>
<label for="rtr_enable">RTR</label>
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
<fieldset>
<legend>Node Colors:</legend>
<div>
<label for="rfc_color">RFC</label>
<input type="color" id="rfc_color" name="rfc_color" form="search" value="#2072b1" /><br />
<label for="std_color">STD</label>
<input type="color" id="std_color" name="std_color" form="search" value="#c21a7e" /><br />
<label for="bcp_color">BCP</label>
<input type="color" id="bcp_color" name="bcp_color" form="search" value="#6d388d" /><br />
<label for="fyi_color">FYI</label>
<input type="color" id="fyi_color" name="fyi_color" form="search" value="#8bbd3e" /><br />
<label for="nic_color">NIC</label>
<input type="color" id="nic_color" name="nic_color" form="search" value="#efe50b" /><br />
<label for="ien_color">IEN</label>
<input type="color" id="ien_color" name="ien_color" form="search" value="#f28f20" /><br />
<label for="rtr_color">RTR</label>
<input type="color" id="rtr_color" name="rtr_color" form="search" value="#e32326" />
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
<fieldset>
<legend>Edge Style:</legend>
<div>
<label for="obsoletes_style">obsoletes</label>
<select name="obsoletes_style" id="obsoletes_style" form="search">
<option value="solid">solid</option>
<option value="dotted">dotted</option>
<option value="dashed" selected>dashed</option>
<option value="dashdot">dashdot</option>
<option value="none">none</option>
</select><br />
<label for="obsoleted_by_style">obsoleted by</label>
<select name="obsoleted_by_style" id="obsoleted_by_style" form="search">
<option value="solid">solid<hr /></option>
<option value="dotted">dotted</option>
<option value="dashed" selected>dashed</option>
<option value="dashdot">dashdot</option>
<option value="none">none</option>
</select><br />
<label for="updates_style">updates</label>
<select name="updates_style" id="updates_style" form="search">
<option value="solid">solid<hr /></option>
<option value="dotted">dotted</option>
<option value="dashed">dashed</option>
<option value="dashdot" selected>dashdot</option>
<option value="none">none</option>
</select><br />
<label for="updated_by_style">updated by</label>
<select name="updated_by_style" id="updated_by_style" form="search">
<option value="solid">solid<hr /></option>
<option value="dotted">dotted</option>
<option value="dashed">dashed</option>
<option value="dashdot" selected>dashdot</option>
<option value="none">none</option>
</select><br />
<label for="is_also_style">is also</label>
<select name="is_also_style" id="is_also_style" form="search">
<option value="solid" selected>solid<hr /></option>
<option value="dotted">dotted</option>
<option value="dashed">dashed</option>
<option value="dashdot">dashdot</option>
<option value="none">none</option>
</select><br />
<label for="see_also_style">see also</label>
<select name="see_also_style" id="see_also_style" form="search">
<option value="solid">solid<hr /></option>
<option value="dotted" selected>dotted</option>
<option value="dashed">dashed</option>
<option value="dashdot">dashdot</option>
<option value="none">none</option>
</select>
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
<fieldset>
<legend>Edge Colors:</legend>
<div>
<label for="obsoletes_color">obsoletes</label>
<input type="color" id="obsoletes_color" name="obsoletes_color" form="search" value="#607d8d" /><br />
<label for="obsoleted_by_color">obsoleted by</label>
<input type="color" id="obsoleted_by_color" name="obsoleted_by_color" form="search" value="#303c50" /><br />
<label for="updates_color">updates</label>
<input type="color" id="updates_color" name="updates_color" form="search" value="#607d8d" /><br />
<label for="updated_by_color">updated by</label>
<input type="color" id="updated_by_color" name="updated_by_color" form="search" value="#303c50" /><br />
<label for="is_also_color">is also</label>
<input type="color" id="is_also_color" name="is_also_color" form="search" value="#132e41" /><br />
<label for="see_also_color">see also</label>
<input type="color" id="see_also_color" name="see_also_color" form="search" value="#008e90" />
</div>
<div><!--workaround to center content vertically--></div>
</fieldset>
</form>
{% endblock main %}

91
tests/test_details.py Normal file
View File

@ -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

34
tests/test_errors.py Normal file
View File

@ -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

715
tests/test_index_parser.py Normal file
View File

@ -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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<bcp-entry>
<doc-id>BCP0001</doc-id>
</bcp-entry>
<bcp-entry>
<doc-id>BCP0188</doc-id>
<is-also>
<doc-id>RFC7258</doc-id>
</is-also>
</bcp-entry>
<bcp-entry>
<doc-id>BCP0185</doc-id>
<is-also>
<doc-id>RFC7115</doc-id>
<doc-id>RFC9319</doc-id>
</is-also>
</bcp-entry>
</rfc-index>"""
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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<fyi-entry>
<doc-id>FYI0042</doc-id>
</fyi-entry>
<fyi-entry>
<doc-id>FYI0023</doc-id>
<is-also>
<doc-id>RFC1580</doc-id>
</is-also>
</fyi-entry>
<fyi-entry>
<doc-id>FYI0666</doc-id>
<is-also>
<doc-id>RFC1149</doc-id>
<doc-id>RFC2549</doc-id>
<doc-id>RFC6214</doc-id>
</is-also>
</fyi-entry>
</rfc-index>"""
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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<rfc-entry>
<doc-id>RFC0023</doc-id>
<title>Foo</title>
<author>
<name>J. Doe</name>
</author>
<date>
<month>April</month>
<year>2042</year>
</date>
<current-status>PROPOSED STANDARD</current-status>
<publication-status>INFORMATIONAL</publication-status>
</rfc-entry>
<rfc-entry>
<doc-id>RFC0042</doc-id>
<title>Bar</title>
<author>
<name>J. Doe</name>
<title>Editor</title>
<organization>A Company that Makes Everything</organization>
<org-abbrev>ACME</org-abbrev>
</author>
<date>
<day>28</day>
<month>February</month>
<year>2042</year>
</date>
<format>
<file-format>ASCII</file-format>
</format>
<page-count>42</page-count>
<keywords>
<kw>foo</kw>
</keywords>
<abstract>
<p>This is a test</p>
</abstract>
<draft>draft-test-test-test</draft>
<notes>this is a note</notes>
<obsoletes>
<doc-id>RFC0023</doc-id>
</obsoletes>
<obsoleted-by>
<doc-id>RFC0666</doc-id>
</obsoleted-by>
<updates>
<doc-id>RFC0023</doc-id>
</updates>
<updated-by>
<doc-id>RFC0666</doc-id>
</updated-by>
<is-also>
<doc-id>BCP0666</doc-id>
</is-also>
<see-also>
<doc-id>RFC0023</doc-id>
</see-also>
<current-status>HISTORIC</current-status>
<publication-status>UNKNOWN</publication-status>
<stream>IETF</stream>
<area>foo</area>
<wg_acronym>bar</wg_acronym>
<errata-url>http://example.org</errata-url>
<doi>10.17487/RFC0042</doi>
</rfc-entry>
<rfc-entry>
<doc-id>RFC0666</doc-id>
<title>FooBar</title>
<author>
<name>J. Doe</name>
<title>Editor</title>
<organization>A Company that Makes Everything</organization>
<org-abbrev>ACME</org-abbrev>
</author>
<author>
<name>M. Mustermann</name>
</author>
<date>
<day>28</day>
<month>February</month>
<year>2042</year>
</date>
<format>
<file-format>TEXT</file-format>
<file-format>HTML</file-format>
</format>
<page-count>666</page-count>
<keywords>
<kw>foo</kw>
<kw>bar</kw>
</keywords>
<abstract>
<p>This is a test</p>
<p>more testing</p>
</abstract>
<draft>draft-test-test-test</draft>
<notes>this is a note</notes>
<obsoletes>
<doc-id>RFC0023</doc-id>
<doc-id>RFC0042</doc-id>
</obsoletes>
<obsoleted-by>
<doc-id>RFC1337</doc-id>
<doc-id>RFC6666</doc-id>
</obsoleted-by>
<updates>
<doc-id>RFC0023</doc-id>
<doc-id>RFC0042</doc-id>
</updates>
<updated-by>
<doc-id>RFC1337</doc-id>
<doc-id>RFC6666</doc-id>
</updated-by>
<is-also>
<doc-id>BCP6666</doc-id>
<doc-id>STD0666</doc-id>
</is-also>
<see-also>
<doc-id>RFC0023</doc-id>
<doc-id>RFC0042</doc-id>
</see-also>
<current-status>INTERNET STANDARD</current-status>
<publication-status>DRAFT STANDARD</publication-status>
<stream>INDEPENDENT</stream>
<area>foo</area>
<wg_acronym>bar</wg_acronym>
<errata-url>http://example.org</errata-url>
<doi>10.17487/RFC0666</doi>
</rfc-entry>
</rfc-index>"""
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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<std-entry>
<doc-id>STD0666</doc-id>
<title>test</title>
</std-entry>
<std-entry>
<doc-id>STD0099</doc-id>
<title>HTTP/1.1</title>
<is-also>
<doc-id>RFC9112</doc-id>
</is-also>
</std-entry>
<std-entry>
<doc-id>STD0078</doc-id>
<title>Simple Network Management Protocol (SNMP) Security</title>
<is-also>
<doc-id>RFC5343</doc-id>
<doc-id>RFC5590</doc-id>
<doc-id>RFC5591</doc-id>
<doc-id>RFC6353</doc-id>
</is-also>
</std-entry>
</rfc-index>"""
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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<rfc-not-issued-entry>
<doc-id>RFC0042</doc-id>
</rfc-not-issued-entry>
</rfc-index>"""
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 = """<?xml version="1.0" encoding="UTF-8"?>
<rfc-index xmlns="http://www.rfc-editor.org/rfc-index"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.rfc-editor.org/rfc-index
http://www.rfc-editor.org/rfc-index.xsd">
<rfc-not-issued-entry>
<doc-id>RFC0042</doc-id>
</rfc-not-issued-entry>
</rfc-index>"""
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

View File

@ -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], '<svg')
self.assertEqual(svg[-7:], '</svg>\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

34
tests/test_routing.py Normal file
View File

@ -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

92
tests/test_search.py Normal file
View File

@ -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