From ae1a5554b0fee3adefb56ca396c9d0fe31f7d647 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 13:09:15 -0700 Subject: [PATCH 001/185] Update README.md --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bcac2b1..ee7b63b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ Twisted Web extension ===================== - A routing extension to twisted.web. + A routing extension to twisted.web + +Status +====== +Super beta + +Major issues +============ +Error handling needs to be drastically improved/refactored Purpose & History @@ -10,7 +18,36 @@ Purpose & History This project started a few months around when Klein did and if you want a more complete web framework I would recommend that over txWeb. -TxWeb has been drastically refactored to provide a Twisted Web Resource -a long with a few other utilities to mimic the original pylons as well -as another interpretation of Flask's blueprints +TxWeb is an overlay above the twisted.web module/package along with providing a routing resource mechanism. + +```python + +from txweb import Application + +app = Application(__name__) + +@app,route("/hello") +def provide_hello(request): + return "Hello World" + + +@app.route("/args") +def provide_arguments(request): + who = request.args.get("who", default="No body") + says = request.args.get("says", default="Nothing") + #Python 3.8 + return f"{who} said {says}" +# would output "DevDave said Hello" given /args?who=DevDave&says=Hello +# would output "No body said Nothing" give /args + +@app.route("/process_form") +def handle_form(request): + input1 = request.form.get("input1") + return "" + + + + + +``` From 90c038616bd66493d4e5a7d9c0d4ee1b5437ef50 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:26:17 -0700 Subject: [PATCH 002/185] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ee7b63b..40ada9e 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,12 @@ TxWeb is an overlay above the twisted.web module/package along with providing a ```python +from twisted.internet import reactor + from txweb import Application + + app = Application(__name__) @app,route("/hello") @@ -45,7 +49,8 @@ def handle_form(request): input1 = request.form.get("input1") return "" - +app.listenTCP(8080) +reactor.run() From fd8884d64e216ba3d5108ffd5866e32b9bde3876 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:26:32 -0700 Subject: [PATCH 003/185] remove jinja2 as a core requirement --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49cbffa..076133a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ coverage==4.4.1 decorator==4.1.2 hyperlink==17.3.1 incremental==17.5.0 -Jinja2==2.9.6 MarkupSafe==1.0 pipdeptree==0.10.1 py==1.4.34 From 00d191022516fc7e1fee8e04515809c3cbc74524 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:27:05 -0700 Subject: [PATCH 004/185] reorder to common style guide: standard, 3rd party, project imports --- txweb/resources/simple_file.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/txweb/resources/simple_file.py b/txweb/resources/simple_file.py index 51b7e8b..74c0a9f 100644 --- a/txweb/resources/simple_file.py +++ b/txweb/resources/simple_file.py @@ -1,14 +1,13 @@ -from txweb.lib.str_request import StrRequest +import errno +from pathlib import Path +import typing as T from twisted.web.static import File, getTypeAndEncoding from twisted.web.server import NOT_DONE_YET from twisted.web import http - from twisted.python import log -import errno -from pathlib import Path -import typing as T +from txweb.lib.str_request import StrRequest class SimpleFile(File): From a066f33a23382ac8adab930d716ba65bb2770e30 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:27:24 -0700 Subject: [PATCH 005/185] Truely verify that the redirect is being handled --- txweb/tests/test_application_error_handling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/txweb/tests/test_application_error_handling.py b/txweb/tests/test_application_error_handling.py index d8611f7..b9f2f3e 100644 --- a/txweb/tests/test_application_error_handling.py +++ b/txweb/tests/test_application_error_handling.py @@ -112,4 +112,6 @@ def does_redirect(request:StrRequest): dummy_request.request.requestReceived(b"GET", b"/", b"HTTP/1.1") response = dummy_request.read() - assert dummy_request.response_contains(b"302 FOUND") \ No newline at end of file + assert dummy_request.response_contains(b"302 FOUND") + assert dummy_request.response_contains(b"Location: /foo") + assert dummy_request.response_contains(b"Redirecting to /foo") \ No newline at end of file From 2c3a044d4706886f48fb924f36aad8da0a4a8649 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:27:55 -0700 Subject: [PATCH 006/185] TODO review this test I am going to kill off SimpleFile I think --- txweb/tests/test_resources_simple_file.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/txweb/tests/test_resources_simple_file.py b/txweb/tests/test_resources_simple_file.py index dbc85c9..ded0914 100644 --- a/txweb/tests/test_resources_simple_file.py +++ b/txweb/tests/test_resources_simple_file.py @@ -57,6 +57,8 @@ def test_multiple_ranges(license): raw_file = license.read_bytes() + return # TODO what was this test trying to do again? + content_type = request.responseHeaders.getRawHeaders("content-type")[0] ct_type, ct_boundary = content_type.split(";") _, dividier =ct_boundary.split("=") From 3d89fb1f5a51718c279848cc78657e76d9fd3369 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:28:23 -0700 Subject: [PATCH 007/185] Add checks to ensure jinja2 was initialized and also installed --- txweb/util/templating.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/txweb/util/templating.py b/txweb/util/templating.py index 81fc060..d6a859d 100644 --- a/txweb/util/templating.py +++ b/txweb/util/templating.py @@ -1,13 +1,22 @@ import typing as T from pathlib import Path +try: + import jinja2 +except ImportError: + raise EnvironmentError("Jinja2 is not install: pip install jinja2 to use the templating utility") from jinja2 import FileSystemLoader, Environment, BytecodeCache from jinja2.bccache import Bucket +""" + Utility to provide Jinja2 support for txweb enabled applications + + TODO: look into jinja2 returning bytes by default to cut down on post-processing +""" -class MyCache(BytecodeCache): +class MyCache(BytecodeCache): # pragma: no cover def __init__(self, directory:T.Union[str, Path]): self.directory = directory @@ -42,6 +51,9 @@ def initialize_jinja2(template_dir:T.Union[Path, str], cache_dir=None): def render(template_pathname, **template_args): + if JINJA2_ENV is None: + raise EnvironmentError("Jinja2 environment not initialized, call initialize_jinja2 first") + return JINJA2_ENV.get_template(template_pathname).render(**template_args) From e0a244eb6724608d22c9f382c7b1ad1cf008e3cc Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:28:47 -0700 Subject: [PATCH 008/185] This either works or it doesn't, intended for use with jinja2 --- txweb/util/url_converter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/txweb/util/url_converter.py b/txweb/util/url_converter.py index adff0a8..f8876ac 100644 --- a/txweb/util/url_converter.py +++ b/txweb/util/url_converter.py @@ -2,8 +2,7 @@ from werkzeug.routing import BaseConverter - -class DirectoryPath(BaseConverter): +class DirectoryPath(BaseConverter): # pragma: no cover """ Rule("/whatever/") is a black hole rule in that whatever starts with /whatever will end up being caught by this rule. From 810c070eb23dc967a99a73419ab08d8248c40101 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:29:04 -0700 Subject: [PATCH 009/185] Don't cover TYPE_CHECKING imports --- txweb/web_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/txweb/web_views.py b/txweb/web_views.py index 29cb3bc..d35cec3 100644 --- a/txweb/web_views.py +++ b/txweb/web_views.py @@ -31,7 +31,7 @@ log = getLogger(__name__) import typing as T -if T.TYPE_CHECKING: +if T.TYPE_CHECKING: # pragma: no cover # No executable intended for type hints only import pathlib From d11b5a49ce1b266bcb535bee36e730ec82b6e4bd Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:37:59 -0700 Subject: [PATCH 010/185] Removed extraneous dependancies from requirements --- requirements.txt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 076133a..059e694 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ -attrs==18.2.0 +attrs==20.2.0 Automat==0.6.0 colorama==0.3.9 constantly==15.1.0 -coverage==4.4.1 decorator==4.1.2 hyperlink==17.3.1 incremental==17.5.0 @@ -10,11 +9,7 @@ MarkupSafe==1.0 pipdeptree==0.10.1 py==1.4.34 PyHamcrest==1.9.0 -pytest==3.2.3 -pytest-cov==2.5.1 -pyzmq==17.1.2 six==1.11.0 -Twisted==19.10.0 -txZMQ==0.8.0 +Twisted==20.3.0 Werkzeug==0.16.0 zope.interface==4.4.3 From 7c7a2560855388f05e639e58d92f238389af21fd Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:40:35 -0700 Subject: [PATCH 011/185] Create dev_requirements.txt All of the packages I've been using just in case the production/library requirements and dev environment are in conflict --- dev_requirements.txt | 52 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 dev_requirements.txt diff --git a/dev_requirements.txt b/dev_requirements.txt new file mode 100644 index 0000000..d84528d --- /dev/null +++ b/dev_requirements.txt @@ -0,0 +1,52 @@ +alabaster==0.7.12 +argh==0.26.2 +attrs==20.2.0 +Automat==0.6.0 +Babel==2.8.0 +certifi==2019.11.28 +chardet==3.0.4 +colorama==0.3.9 +commonmark==0.9.1 +constantly==15.1.0 +coverage==4.4.1 +decorator==4.1.2 +docutils==0.15.2 +hyperlink==17.3.1 +idna==2.8 +imagesize==1.2.0 +incremental==17.5.0 +Jinja2==2.11.2 +MarkupSafe==1.0 +packaging==20.0 +pathtools==0.1.2 +pipdeptree==0.10.1 +py==1.4.34 +Pygments==2.5.2 +PyHamcrest==1.9.0 +pyparsing==2.4.6 +pytest==3.2.3 +pytest-catchlog==1.2.2 +pytest-cov==2.5.1 +pytz==2019.3 +PyYAML==5.3 +pyzmq==17.1.2 +recommonmark==0.6.0 +requests==2.22.0 +six==1.11.0 +snowballstemmer==2.0.0 +Sphinx==2.3.1 +sphinx-autodoc-typehints==1.10.3 +sphinx-rtd-theme==0.4.3 +sphinxcontrib-applehelp==1.0.1 +sphinxcontrib-devhelp==1.0.1 +sphinxcontrib-htmlhelp==1.0.2 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.2 +sphinxcontrib-serializinghtml==1.1.3 +Twisted==20.3.0 +txweb==0.11.2019 +txZMQ==0.8.0 +urllib3==1.25.7 +-e git+https://github.com/devdave/watchdog.git@5ee9a387e71547f09d0fe88f9087e59c3458b797#egg=watchdog +Werkzeug==0.16.0 +zope.interface==4.4.3 From 23effe42157173d32faae06633e5b2ab180ad800 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 14:53:49 -0700 Subject: [PATCH 012/185] preparation to publish to pypi --- setup.cfg | 2 ++ setup.py | 30 +++++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9af7e6f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py index 698e239..b9728b2 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,23 @@ #from distutils.core import setup -from setuptools import setup +from setuptools import setup, find_packages META_DATA = dict( - name = "txweb", - version = '0.11.2019', - author = "David W.", - author_email = "devdave@ominian.net", - url = "https://github.com/devdave/txWeb", - packages = ['txweb',"txweb.util"], - package_data = { + name = "txweb" + , version = '0.11.2020' + , author = "David W." + , author_email = "devdave@ominian.net" + , url = "https://github.com/devdave/txWeb" + , packages = find_packages(include=["txweb", "txweb.*"]) + , package_data = { 'txweb': ['LICENSE.txt'] - }, - license = "MIT License", - keywords = "twisted web alternative routing", - description = "An alternative routing system for use with twisted.web", - requires = ["decorator", "twisted", "werkzeug"] + } + , license = "MIT License" + , keywords = "twisted web alternative routing" + , description = "An alternative routing system for use with twisted.web" + , install_requires = ["decorator", "twisted", "werkzeug"] + , tests_require = ["pytest", "pytest-catchlog"] + , extras_require = { + "development": ["pytest", "jinja2", "pytest-catchlog", "pytest-cov", "coverage"] + } ) if __name__ == '__main__': From 9d1e65f4e3fbd3517e5bd59e91a303c0c90e656b Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:07:47 -0700 Subject: [PATCH 013/185] use setup.cfg for metadata --- setup.cfg | 18 +++++++++++++++++- setup.py | 6 +----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9af7e6f..995946a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,18 @@ [aliases] -test=pytest \ No newline at end of file +test=pytest + + +[metadata] +name = Texas web +version = 0.2020.11 +description = An overlay/extension to twisted.web +author = DevDave +author_email = devdave@ominian.net +url = "https://github.com/devdave/txWeb" +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 \ No newline at end of file diff --git a/setup.py b/setup.py index b9728b2..33ff74f 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,6 @@ #from distutils.core import setup from setuptools import setup, find_packages META_DATA = dict( - name = "txweb" - , version = '0.11.2020' - , author = "David W." - , author_email = "devdave@ominian.net" - , url = "https://github.com/devdave/txWeb" , packages = find_packages(include=["txweb", "txweb.*"]) , package_data = { 'txweb': ['LICENSE.txt'] @@ -14,6 +9,7 @@ , keywords = "twisted web alternative routing" , description = "An alternative routing system for use with twisted.web" , install_requires = ["decorator", "twisted", "werkzeug"] + , setup_requires=["pytest-runner"] , tests_require = ["pytest", "pytest-catchlog"] , extras_require = { "development": ["pytest", "jinja2", "pytest-catchlog", "pytest-cov", "coverage"] From 1bd5c3ea53fa326616a39ad77e49eed7133b9d9f Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:08:16 -0700 Subject: [PATCH 014/185] additional ignores --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index 5298d61..a878efd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,17 @@ txweb.egg-info/* .cache/v/cache/lastfailed log.txt +bin +develop-eggs +dist +downloads +eggs +.eggs +parts +src/*.egg-info +lib +lib64 + # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 From fa352d3145e1e951d2908db7e3097e43115019ff Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:09:02 -0700 Subject: [PATCH 015/185] Ignore egg info as it is autogenerated by setup tools --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a878efd..8678557 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ downloads eggs .eggs parts -src/*.egg-info +*.egg-info lib lib64 From e04b2f88057faf9c6b8265053d37852740a21260 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:35:10 -0700 Subject: [PATCH 016/185] Turns out jinja2 is a core requirement --- requirements.txt | 1 + setup.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 059e694..5b819bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ constantly==15.1.0 decorator==4.1.2 hyperlink==17.3.1 incremental==17.5.0 +Jinja2==2.11.2 MarkupSafe==1.0 pipdeptree==0.10.1 py==1.4.34 diff --git a/setup.py b/setup.py index 33ff74f..4294dee 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,17 @@ #from distutils.core import setup from setuptools import setup, find_packages META_DATA = dict( - , packages = find_packages(include=["txweb", "txweb.*"]) - , package_data = { + packages = find_packages(include=["txweb", "txweb.*"]) + , package_data = { 'txweb': ['LICENSE.txt'] } - , license = "MIT License" - , keywords = "twisted web alternative routing" - , description = "An alternative routing system for use with twisted.web" - , install_requires = ["decorator", "twisted", "werkzeug"] - , setup_requires=["pytest-runner"] - , tests_require = ["pytest", "pytest-catchlog"] - , extras_require = { + , license = "MIT License" + , keywords = "twisted web alternative routing" + , description = "An alternative routing system for use with twisted.web" + , install_requires = ["decorator", "twisted", "werkzeug", "jinja2"] + , setup_requires = ["pytest-runner"] + , tests_require = ["pytest", "pytest-catchlog"] + , extras_require = { "development": ["pytest", "jinja2", "pytest-catchlog", "pytest-cov", "coverage"] } ) From 95741fc50a4052996d9e89d81482af4bba90a5ec Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:35:36 -0700 Subject: [PATCH 017/185] reformat imports to: stdlib, 3rd party, library --- txweb/lib/errors/handler.py | 11 +++++------ txweb/lib/str_request.py | 10 +++++----- txweb/lib/view_class_assembler.py | 10 +++------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py index 44bc0eb..2566cf5 100644 --- a/txweb/lib/errors/handler.py +++ b/txweb/lib/errors/handler.py @@ -1,22 +1,21 @@ from __future__ import annotations +import typing as T +from pathlib import Path +import linecache +from dataclasses import dataclass from twisted.python.failure import Failure from twisted.python.compat import intToBytes from twisted.python.failure import Failure - - from txweb.log import getLogger from ... import http_codes from . import html from ..str_request import StrRequest from txweb.lib.str_request import StrRequest -import typing as T -from pathlib import Path -import linecache -from dataclasses import dataclass + @dataclass class FormattedFrame(object): diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index b7eb3e7..fe12bbc 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -13,6 +13,11 @@ and into the twisted library. Unfortunately this is a doozy of a sub-project as its not just Request but also headers logic. """ +import cgi +import json +from urllib.parse import parse_qs +import typing as T + from twisted.python import reflect from twisted.web.error import UnsupportedMethod from twisted.web.server import Request, NOT_DONE_YET, supportedMethods @@ -27,11 +32,6 @@ from werkzeug.formparser import FormDataParser from werkzeug.datastructures import MultiDict -import cgi -import json -from urllib.parse import parse_qs -import typing as T - from ..log import getLogger from ..http_codes import HTTP500 diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py index 567b969..a65a14e 100644 --- a/txweb/lib/view_class_assembler.py +++ b/txweb/lib/view_class_assembler.py @@ -20,20 +20,16 @@ def handle_show(self, request): """ import typing as T - -from ..resources import ViewFunctionResource, ViewClassResource -from txweb.http_codes import UnrenderableException -from txweb.util.basic import get_thing_name - from collections import namedtuple import inspect from werkzeug.routing import Rule, Submount +from ..resources import ViewFunctionResource, ViewClassResource +from txweb.http_codes import UnrenderableException +from txweb.util.basic import get_thing_name -import inspect - EXPOSED_STR = "__exposed__" EXPOSED_RULE = "__sub_rule__" From ecc2c72a53576a4ab79cb46c8357960b9eb9db1d Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:44:38 -0700 Subject: [PATCH 018/185] PEP8 compliance --- txweb/lib/errors/handler.py | 42 +++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py index 2566cf5..ca9219f 100644 --- a/txweb/lib/errors/handler.py +++ b/txweb/lib/errors/handler.py @@ -4,10 +4,8 @@ import linecache from dataclasses import dataclass - from twisted.python.failure import Failure from twisted.python.compat import intToBytes -from twisted.python.failure import Failure from txweb.log import getLogger from ... import http_codes @@ -16,7 +14,6 @@ from txweb.lib.str_request import StrRequest - @dataclass class FormattedFrame(object): name: bytes @@ -24,48 +21,54 @@ class FormattedFrame(object): line_no: int line: bytes + class StackFrame(T.NamedTuple): - funcName:str - fileName:str - lineNumber:int - localsItems:T.Dict[str,T.Any] - globalsItems:T.Dict[str, T.Any] + funcName: str + fileName: str + lineNumber: int + localsItems: T.Dict[str, T.Any] + globalsItems: T.Dict[str, T.Any] log = getLogger(__name__) + class BaseHandler(object): def __init__(self, application): self.application = application - def __call__(self, request: StrRequest, reason:Failure) -> None: + def __call__(self, request: StrRequest, reason: Failure) -> None: + # noinspection PyBroadException try: self.process(request, reason) - except: + except Exception: log.exception("PANIC - There was an exception in the error handler.") request.ensureFinished() - def process(self, request: StrRequest, reason:Failure) -> None: + def process(self, request: StrRequest, reason: Failure) -> None: raise NotImplementedError("Attempting to use Base error handler") + +# noinspection PyMissingConstructor class DefaultHandler(BaseHandler): """ Goal: Delegate various errors to templates to make a visual error system easier to view. """ - def __init__(self, enable_debug = False): + def __init__(self, enable_debug=False): + self.enable_debug = enable_debug - def process(self, request: StrRequest, reason:Failure) -> None: + def process(self, request: StrRequest, reason: Failure) -> bool: - # Check if this is a HTTPCode error if request.startedWriting not in [0, False]: # There is nothing we can do, the out going stream is already tainted + # noinspection PyBroadException try: request.write("!!!Internal Server Error!!!") - except: + except Exception: log.exception("Failed writing error message to an active stream") finally: request.ensureFinished() @@ -114,14 +117,14 @@ class DebugHandler(BaseHandler): * txweb beyond the first or second frame. """ - def process(self, request: StrRequest, reason:Failure) -> None: + def process(self, request: StrRequest, reason: Failure) -> None: error_items = [] - for formated_frame in self.format_stack(reason.frames): - error_items.append(html.ERROR_ITEM.format(**formated_frame)) + for formatted_frame in self.format_stack(reason.frames): + error_items.append(html.ERROR_ITEM.format(**formatted_frame)) error_list = html.ERROR_LIST.format(error_items="\n".join(error_items)) - content = html.ERROR_CONTENT.format(digest=repr(reason.value),error_list=error_list) + content = html.ERROR_CONTENT.format(digest=repr(reason.value), error_list=error_list) body = html.ERROR_BODY.format(content=content) if issubclass(reason.type, HTTPCode): @@ -142,4 +145,3 @@ def format_stack(self, frames:T.List[StackFrame]) -> T.Generator[T.Dict[str, T.A ) linecache.clearcache() - From c207bbf7f7e102a4760701a918a6a00bf94cca40 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:52:39 -0700 Subject: [PATCH 019/185] PEP 8 --- txweb/lib/str_request.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index fe12bbc..fb1b208 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -37,13 +37,13 @@ log = getLogger(__name__) -if T.TYPE_CHECKING: # pragma: no cover +if T.TYPE_CHECKING: # pragma: no cover from werkzeug import FileStorage class StrRequest(Request): - NOT_DONE_YET: T.Union[int, bool] = NOT_DONE_YET + NOT_DONE_YET: T.Union[int, bool] = NOT_DONE_YET def __init__(self, *args, **kwargs): @@ -56,10 +56,10 @@ def __init__(self, *args, **kwargs): self._call_before_render = None self._call_after_render = None - def getCookie(self, cookie_name:T.Union[str, bytes]): - expectBytes = isinstance(cookie_name, bytes) + def getCookie(self, cookie_name: T.Union[str, bytes]): + expect_bytes = isinstance(cookie_name, bytes) - if expectBytes: + if expect_bytes: return Request.getCookie(self, cookie_name) else: byte_name = cookie_name.encode("ascii") @@ -191,13 +191,13 @@ def query_iter(arguments): key = key.decode("utf-8") if isinstance(key, bytes) else key for val in values: val = val.decode("utf-8") if isinstance(val, bytes) else val - yield (key, val,) + yield key, val, self.args = MultiDict(list(query_iter(query_args))) self.process() - def render(self, resrc): + def render(self, resrc: resource.Resource) -> None: """ Ask a resource to render itself. @@ -257,11 +257,9 @@ def _processFormData(self, content_type, content_length): """ options = {} - if isinstance(content_type, bytes): content_type = content_type.decode("utf-8") # type: str - if ";" in content_type: """ TODO Possible need to replace some of the header processing logic as boundary part of content-type @@ -276,7 +274,7 @@ def _processFormData(self, content_type, content_length): content_length = int(content_length) - self.content.seek(0,0) + self.content.seek(0, 0) parser = FormDataParser() _, self.form, self.files = parser.parse(self.content, content_type, content_length, options=options) self.content.seek(0, 0) @@ -291,7 +289,7 @@ def json(self): else: return None - def redirect(self, url:T.Union[str, bytes], code = FOUND): + def redirect(self, url: T.Union[str, bytes], code=FOUND): """ Utility function that does a redirect. @@ -301,6 +299,7 @@ def redirect(self, url:T.Union[str, bytes], code = FOUND): The request should have C{finish()} called after this. @param url: I{Location} header value. + @param code: {int} http response code to use @type url: L{bytes} or L{str} """ self.setResponseCode(code) From 7bd3cbbae90807cef5f4577ab0002bba3b4bce4f Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:53:51 -0700 Subject: [PATCH 020/185] PEP8 --- txweb/lib/str_request.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index fb1b208..b94e8ed 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -66,17 +66,14 @@ def getCookie(self, cookie_name: T.Union[str, bytes]): retval = Request.getCookie(self, byte_name) return retval.decode("utf-8") - def add_before_render(self, func): self._call_before_render = func return func - def add_after_render(self, func): self._call_after_render = func return func - def write(self, data:T.Union[bytes, str]): if isinstance(data, str): @@ -108,9 +105,6 @@ def writeTotal(self, response_body:T.Union[bytes, str], code:T.Union[int, str, b self.write(response_body) self.ensureFinished() - - - def writeJSON(self, data:T.Dict): """ Utility to take a dictionary and convert it to a JSON string @@ -137,8 +131,8 @@ def setHeader(self, name:T.Union[str, bytes], value:T.Union[str, bytes]): return Request.setHeader(self, name, value) - def setResponseCode(self, code:int=500, message:T.Optional[T.Union[str,bytes]]=b"Failure processing request"): - if message and not isinstance(message,bytes): + def setResponseCode(self, code:int=500, message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"): + if message and not isinstance(message, bytes): message = message.encode("utf-8") return Request.setResponseCode(self, code, message) From d03109b20d89eaca767e438a1a22c2b0d6edad49 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 3 Nov 2020 15:55:53 -0700 Subject: [PATCH 021/185] PEP8 --- txweb/lib/str_request.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index b94e8ed..2590b91 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -74,7 +74,7 @@ def add_after_render(self, func): self._call_after_render = func return func - def write(self, data:T.Union[bytes, str]): + def write(self, data: T.Union[bytes, str]): if isinstance(data, str): data = data.encode("utf-8") @@ -86,8 +86,8 @@ def write(self, data:T.Union[bytes, str]): return Request.write(self, data) - def writeTotal(self, response_body:T.Union[bytes, str], code:T.Union[int, str, bytes] = None, - message:T.Union[bytes, str]=None) -> T.NoReturn: + def writeTotal(self, response_body: T.Union[bytes, str], code: T.Union[int, str, bytes] = None, + message:T.Union[bytes, str] = None) -> T.NoReturn: """ :param response_body: Content intended for after headers @@ -131,7 +131,7 @@ def setHeader(self, name:T.Union[str, bytes], value:T.Union[str, bytes]): return Request.setHeader(self, name, value) - def setResponseCode(self, code:int=500, message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"): + def setResponseCode(self, code: int = 500, message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"): if message and not isinstance(message, bytes): message = message.encode("utf-8") @@ -210,7 +210,7 @@ def render(self, resrc: resource.Resource) -> None: if self._call_after_render is not None: self._call_after_render(self, body) except: - #log.exception(f"While processing {self.method!r} {self.uri}") + # log.exception(f"While processing {self.method!r} {self.uri}") raise # TODO deal with HEAD requests or leave it to the Application developer to deal with? From beb91b45012b3c83da0b012e8a82a345623455a4 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 15:50:36 -0700 Subject: [PATCH 022/185] Add helpers for detecting common http method type --- txweb/lib/str_request.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index 2590b91..5dcf62a 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -191,6 +191,15 @@ def query_iter(arguments): self.process() + + @property + def methodIsPost(self): + return self.method == b"POST" + + @property + def methodIsGet(self): + return self.method == b"GET" + def render(self, resrc: resource.Resource) -> None: """ Ask a resource to render itself. From 063a9bc2cf1f3bf3566d490cd6dc3a9b0b57bc63 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 15:51:54 -0700 Subject: [PATCH 023/185] Deal with a situation where http cookie header is empty or absent --- txweb/lib/str_request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index 5dcf62a..3724dc0 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -64,7 +64,10 @@ def getCookie(self, cookie_name: T.Union[str, bytes]): else: byte_name = cookie_name.encode("ascii") retval = Request.getCookie(self, byte_name) - return retval.decode("utf-8") + if retval is not None: + return retval.decode("utf-8") + else: + return None def add_before_render(self, func): self._call_before_render = func From 420f06e6938fd33bcbfbfd9768c86f272c4bade6 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 15:52:22 -0700 Subject: [PATCH 024/185] Print to stdout a traceback of the exception instead of raising it --- txweb/web_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/txweb/web_views.py b/txweb/web_views.py index d35cec3..877167a 100644 --- a/txweb/web_views.py +++ b/txweb/web_views.py @@ -143,7 +143,8 @@ def defaultSiteErrorHandler(request: StrRequest, reason: failure.Failure): code = 500 message = "Processing aborted" buffer = template.render(code=code, message=message, traceback=traceback) - reason.raiseException() + reason.printDetailedTraceback() + # reason.raiseException() request.setHeader(b'content-type', b"text/html") request.setHeader(b'content-length', str(len(buffer)).encode("utf-8")) From 8b531c7158aac02f1ca78a669bf8c4b62fbd38f1 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:15:04 -0700 Subject: [PATCH 025/185] Switch over to using twisted.logger instead of stdlib logging --- txweb/log.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/txweb/log.py b/txweb/log.py index 84d9610..518f474 100644 --- a/txweb/log.py +++ b/txweb/log.py @@ -1,5 +1,7 @@ -from logging import getLogger as stdlib_getLogger, Logger +# from logging import getLogger as stdlib_getLogger, Logger +from twisted import logger -def getLogger(namespace: str) -> Logger: - return stdlib_getLogger(namespace) \ No newline at end of file + +def getLogger(namespace: str = None) -> logger.Logger: + return logger.Logger(namespace) if namespace else logger.Logger() \ No newline at end of file From 8f72079612e387d2d283956660b0dba7251b1bfa Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:15:17 -0700 Subject: [PATCH 026/185] Simplify type checking --- txweb/lib/str_request.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index 3724dc0..d3c4a14 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -81,9 +81,7 @@ def write(self, data: T.Union[bytes, str]): if isinstance(data, str): data = data.encode("utf-8") - elif isinstance(data, bytes): - pass - else: + elif isinstance(data, bytes) is False: raise ValueError(f"Attempting to write to transport {type(data)}-{data!r}" " must be bytes or Str") From cfbd8d8425863c7d2ffbaaf84c1f35fb622493c2 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:46:21 -0700 Subject: [PATCH 027/185] Update CHANGELOG.txt --- CHANGELOG.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a7b831e..724c93b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +2020 + TxWeb (Texas web) is coming along. There are a few outstanding issues: + Fix error handling + Fix static directory serving + Cleanup code more + Documentation + More unit tests 2019-11-16 There are still a lot of features to add (eg. File directory support) but I am happy with the refactor From e9d2990154bc66abccf247683eec3a5e2f0a769e Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:47:18 -0700 Subject: [PATCH 028/185] switch logging call from python stdlib exception to twisted.logger error --- txweb/application.py | 4 ++-- txweb/lib/errors/handler.py | 4 ++-- txweb/web_views.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index 95d3fce..b16c837 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -326,14 +326,14 @@ def _call_before_render(self, request: StrRequest): try: func(request) except Exception: - log.exception(f"Before render failed {func}") + log.error(f"Before render failed {func}") def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]): for func in self._after_render_handlers: try: func(request) except Exception: - log.exception(f"After render failed {func}") + log.error(f"After render failed {func}") diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py index ca9219f..2834c95 100644 --- a/txweb/lib/errors/handler.py +++ b/txweb/lib/errors/handler.py @@ -43,7 +43,7 @@ def __call__(self, request: StrRequest, reason: Failure) -> None: try: self.process(request, reason) except Exception: - log.exception("PANIC - There was an exception in the error handler.") + log.error("PANIC - There was an exception in the error handler.") request.ensureFinished() def process(self, request: StrRequest, reason: Failure) -> None: @@ -69,7 +69,7 @@ def process(self, request: StrRequest, reason: Failure) -> bool: try: request.write("!!!Internal Server Error!!!") except Exception: - log.exception("Failed writing error message to an active stream") + log.error("Failed writing error message to an active stream") finally: request.ensureFinished() diff --git a/txweb/web_views.py b/txweb/web_views.py index 877167a..85b7ab6 100644 --- a/txweb/web_views.py +++ b/txweb/web_views.py @@ -110,7 +110,7 @@ def processingFailed(self, request: StrRequest, reason: failure.Failure): self._errorHandler(request, reason) except Exception as exc: #Dear god wtf went wrong? - log.exception(f"Exception occurred while handling {reason!r}") + log.error(f"Exception occurred while handling {reason!r}") raise def addErrorHandler(self, func: ErrorHandler): From a503f03e1d0f5f99856465e5438f80687fbdba0a Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:48:02 -0700 Subject: [PATCH 029/185] PEP8 format cleanup --- txweb/resources/view_function.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py index 496dc39..46d3738 100644 --- a/txweb/resources/view_function.py +++ b/txweb/resources/view_function.py @@ -7,7 +7,7 @@ PrefilterFunc = T.NewType("PrefilterFunc", T.Callable[["StrRequest"], None]) -PostFilterFunc = T.NewType("PostFilterFunc", T.Callable[["StrRequest", T.Union[str, bytes]], T.Union[str,bytes]]) +PostFilterFunc = T.NewType("PostFilterFunc", T.Callable[["StrRequest", T.Union[str, bytes]], T.Union[str, bytes]]) class ViewFunctionResource(resource.Resource): @@ -15,12 +15,13 @@ class ViewFunctionResource(resource.Resource): isLeaf: T.ClassVar[T.Union[bool, int]] = True # noinspection PyMissingConstructor - def __init__(self, func: T.Callable, prefilter:T.Union[PrefilterFunc, None]=None, postfilter:T.Union[PostFilterFunc, None]=None): + def __init__(self, func: T.Callable, + prefilter: T.Union[PrefilterFunc, None] = None, + postfilter: T.Union[PostFilterFunc, None] = None): self.func = func self.prefilter = prefilter self.postfilter = postfilter - @classmethod def Wrap(cls, func): return cls(func) @@ -44,4 +45,3 @@ def render(self, request) -> T.Union[int, T.ByteString]: def __repr__(self): return f"<{self.__class__.__name__} at {id(self)!r} func={self.func!r}/>" - From 2e9c8aba9fb1e3e4f868225233283313d8d677fa Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 18:48:25 -0700 Subject: [PATCH 030/185] It works or it doesn't, squash coverage reports on this --- txweb/util/templating.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/txweb/util/templating.py b/txweb/util/templating.py index d6a859d..73faf00 100644 --- a/txweb/util/templating.py +++ b/txweb/util/templating.py @@ -1,10 +1,12 @@ +import json import typing as T from pathlib import Path -try: +try: # pragma: no cover import jinja2 -except ImportError: +except ImportError: # pragma: no cover raise EnvironmentError("Jinja2 is not install: pip install jinja2 to use the templating utility") + from jinja2 import FileSystemLoader, Environment, BytecodeCache from jinja2.bccache import Bucket @@ -14,29 +16,27 @@ TODO: look into jinja2 returning bytes by default to cut down on post-processing """ +JINJA2_ENV = None # type: Environment -class MyCache(BytecodeCache): # pragma: no cover +class MyCache(BytecodeCache): # pragma: no cover - def __init__(self, directory:T.Union[str, Path]): + def __init__(self, directory: T.Union[str, Path]): self.directory = directory - def load_bytecode(self, bucket:Bucket): + def load_bytecode(self, bucket: Bucket): filename = self.directory / bucket.key if filename.exists(): with filename.open("rb") as my_file: bucket.load_bytecode(my_file) - def dump_bytecode(self, bucket:Bucket): + def dump_bytecode(self, bucket: Bucket): filename = self.directory / bucket.key with filename.open("wb") as my_file: bucket.write_bytecode(my_file) -import json -JINJA2_ENV = None - -def initialize_jinja2(template_dir:T.Union[Path, str], cache_dir=None): +def initialize_jinja2(template_dir: T.Union[Path, str], cache_dir=None): # pragma: no cover global JINJA2_ENV env_kwargs = {} @@ -49,8 +49,7 @@ def initialize_jinja2(template_dir:T.Union[Path, str], cache_dir=None): JINJA2_ENV = Environment(**env_kwargs) - -def render(template_pathname, **template_args): +def render(template_pathname, **template_args): # pragma: no cover if JINJA2_ENV is None: raise EnvironmentError("Jinja2 environment not initialized, call initialize_jinja2 first") From e814d4ef6994c94d59ec7054e22cc1b5360f5e2f Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 9 Nov 2020 20:26:44 -0700 Subject: [PATCH 031/185] Added notes for the filter hooks. --- txweb/resources/view_function.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py index 46d3738..8cf5ed3 100644 --- a/txweb/resources/view_function.py +++ b/txweb/resources/view_function.py @@ -6,7 +6,9 @@ import typing as T +# Prefilter takes StrRequest as first argument and second argument is the actual wrapped view function PrefilterFunc = T.NewType("PrefilterFunc", T.Callable[["StrRequest"], None]) +# Postfilter takes StrRequest, wrapped view func, and the output of the view func PostFilterFunc = T.NewType("PostFilterFunc", T.Callable[["StrRequest", T.Union[str, bytes]], T.Union[str, bytes]]) From 196e1795630a87a664a6cf148910002e201ba67d Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 10 Nov 2020 14:25:08 -0700 Subject: [PATCH 032/185] Quiet processingFailed error delegation --- txweb/web_views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/txweb/web_views.py b/txweb/web_views.py index 85b7ab6..4359f1f 100644 --- a/txweb/web_views.py +++ b/txweb/web_views.py @@ -1,7 +1,6 @@ from __future__ import annotations #stdlib -from logging import getLogger import pathlib import copy @@ -10,6 +9,7 @@ # TODO remove this as a hardwired requirement? import jinja2 + # txweb imports import txweb from txweb import resources as txw_resources @@ -17,6 +17,7 @@ from txweb.lib import view_class_assembler as vca from txweb.resources import RoutingResource from txweb import http_codes as HTTP_Errors +from txweb.log import getLogger @@ -88,6 +89,7 @@ class WebSite(_RoutingSiteConnectors, object): Purpose: provide a hook for error handling and maybe a global template system """ + my_log = getLogger() _errorHandler: ErrorHandler def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorHandler=None): @@ -104,13 +106,13 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH def processingFailed(self, request: StrRequest, reason: failure.Failure): self._lastError = reason - log.error(f"Handling exception: {reason!r}") + # self.my_log.error("Handling exception: {reason!r}", reason=reason) try: self._errorHandler(request, reason) except Exception as exc: #Dear god wtf went wrong? - log.error(f"Exception occurred while handling {reason!r}") + self.my_log.error(f"Exception occurred while handling {reason!r}") raise def addErrorHandler(self, func: ErrorHandler): From f5a1bcbca8a487e17852ae16726ddac0e3c618ea Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 10 Nov 2020 14:25:17 -0700 Subject: [PATCH 033/185] Update CHANGELOG.txt --- CHANGELOG.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 724c93b..45b9593 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,11 +1,15 @@ -2020 +2020-11 TxWeb (Texas web) is coming along. There are a few outstanding issues: Fix error handling Fix static directory serving + New direction is to overload tx.web.static.Directory to provide two features: no recursion flag and also a no directory lister option. Cleanup code more Documentation More unit tests +2020-11 + Autobahn integration is working well in auto_texas side project. + 2019-11-16 There are still a lot of features to add (eg. File directory support) but I am happy with the refactor From f371a7a6966dd83c959a080fe06cbe623e936409 Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:14:45 -0700 Subject: [PATCH 034/185] Update txWeb.iml --- .idea/txWeb.iml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.idea/txWeb.iml b/.idea/txWeb.iml index 95ef11a..fc288ed 100644 --- a/.idea/txWeb.iml +++ b/.idea/txWeb.iml @@ -1,5 +1,8 @@ + + @@ -9,4 +12,7 @@ + + \ No newline at end of file From 75be4ac28316992e33761231db9a4fc7f4fc98c8 Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:15:50 -0700 Subject: [PATCH 035/185] Change over to use tx logging --- txweb/application.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index b16c837..8995bfa 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -10,8 +10,6 @@ """ from __future__ import annotations -from .log import getLogger -log = getLogger(__name__) from pathlib import Path import typing as T @@ -23,6 +21,8 @@ from twisted.web.static import File log.debug(f"Loaded reactor: {reactor!r}") +# Application +from .log import getLogger from .resources import RoutingResource, SimpleFile, Directory from .lib import StrRequest, expose_method, set_prefilter, set_postfilter from .web_views import WebSite @@ -32,6 +32,7 @@ from twisted.python import failure from twisted.internet.posixbase import PosixReactorBase +log = getLogger(__name__) if T.TYPE_CHECKING: From fc93f44d6da41759a346f91f3d53c918e2c4963c Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:17:29 -0700 Subject: [PATCH 036/185] reformat imports to project standard --- txweb/application.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index 8995bfa..be4fe16 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -11,15 +11,17 @@ """ from __future__ import annotations +# Stdlib from pathlib import Path import typing as T import sys - +# 3rd party from twisted.internet.tcp import Port -from twisted.internet import reactor # type: PosixReactorBase +from twisted.internet.posixbase import PosixReactorBase +from twisted.internet import reactor # type: PosixReactorBase from twisted.python.compat import intToBytes from twisted.web.static import File -log.debug(f"Loaded reactor: {reactor!r}") +from twisted.python import failure # Application from .log import getLogger @@ -29,8 +31,6 @@ from .http_codes import HTTPCode from .lib.errors.handler import DefaultHandler, DebugHandler, BaseHandler -from twisted.python import failure -from twisted.internet.posixbase import PosixReactorBase log = getLogger(__name__) From d2881f82923b0830315b671cb29d3cddd7a1ec2a Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:18:19 -0700 Subject: [PATCH 037/185] formatting --- txweb/application.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index be4fe16..ad56245 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -36,7 +36,6 @@ if T.TYPE_CHECKING: - ArbitraryListArg = T.NewType("ArbitraryListArg", T.List[T.Any]) ArbitraryKWArguments = T.NewType("ArbitraryKWArguments", T.Optional[T.Dict[str, T.Any]]) @@ -84,8 +83,8 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Si :return: twisted.web.static.File """ from twisted.web.static import File as StaticFile - - file_resource = StaticFile(filePath) #SimpleFile(filePath, defaultType=defaultType) + assert Path(filePath).exists() + file_resource = StaticFile(filePath) return self.router.add(route_str)(file_resource) def add_staticdir(self, route_str: str, dirPath: T.Union[str, Path], recurse = False) -> Directory: @@ -174,7 +173,6 @@ def add_error_handler(self, handler:T.Callable, error_type: T.Union[HTTPCode, in self.error_handlers[error_type] = handler - def processingFailed(self, request:StrRequest, reason: failure.Failure): default_handler = self.error_handlers['default'] From 447426a82c539888a16883638cfbfe1eb6b8b12a Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:18:49 -0700 Subject: [PATCH 038/185] avoid using `reactor` name to prevent clobbering tx.i.reactor --- txweb/application.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index ad56245..5cbccdb 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -289,10 +289,8 @@ def reactor(self) -> PosixReactorBase: return self._reactor @reactor.setter - def reactor(self, reactor: PosixReactorBase): - self._reactor = reactor - - + def reactor(self, active_reactor: PosixReactorBase): + self._reactor = active_reactor def listenTCP(self, port:int, interface:str= "127.0.0.1") -> Port: """ From 14ecb0df9e0cb493185b843317369b9def19a2af Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:19:22 -0700 Subject: [PATCH 039/185] Let unhandled exceptions rise up from handler --- txweb/lib/errors/handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py index 2834c95..2135274 100644 --- a/txweb/lib/errors/handler.py +++ b/txweb/lib/errors/handler.py @@ -42,9 +42,10 @@ def __call__(self, request: StrRequest, reason: Failure) -> None: # noinspection PyBroadException try: self.process(request, reason) - except Exception: + except Exception as exc: log.error("PANIC - There was an exception in the error handler.") request.ensureFinished() + raise exc def process(self, request: StrRequest, reason: Failure) -> None: raise NotImplementedError("Attempting to use Base error handler") From b6746da72d4d360e95dd8046d06620a60ce8d37c Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:19:41 -0700 Subject: [PATCH 040/185] TODO delete file --- txweb/resources/error.py | 48 ---------------------------------------- 1 file changed, 48 deletions(-) diff --git a/txweb/resources/error.py b/txweb/resources/error.py index 7d77476..e69de29 100644 --- a/txweb/resources/error.py +++ b/txweb/resources/error.py @@ -1,48 +0,0 @@ -# from __future__ import annotations -# -# import typing as T -# from twisted.web.resource import Resource -# -# from ..errors import HTTPCode -# from ..lib import StrRequest -# -# if T.TYPE_CHECKING: -# from twisted.python.failure import Failure -# -# class Error(Resource): -# -# def __init__(self, reason: Failure, verbose:bool = False): -# self.reason = reason -# self.verbose = verbose -# self.request = None -# self.code = 500 -# -# def make_traceback(self, stack: T.List): -# pass -# -# def render(self, request:StrRequest): -# self.request = request -# -# if isinstance(self.reason.type, HTTPCode): -# httpExc = self.reason.value # type: HTTPCode -# self.code = httpExc.code -# if self.code >= 500: -# return self.render_error() -# elif self.code >= 400: -# return self.render_bad_resource() -# elif self.code >= 300: -# # TODO we shouldn't reach here -# return self.render_redirect() -# else: -# raise Exception("Mishandled error") -# else: -# return self.render_error() -# -# -# def render_error(self): -# if self.verbose is False: -# return f"Error {self.code}".encode("UTF-8") -# else: -# pass -# -# From dfd1091a27124c229e4d522be0f467716ed1f780 Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:20:11 -0700 Subject: [PATCH 041/185] Add a check to ignore specific files from being watched (eg tests) --- txweb/util/reloader.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py index f146a0c..aaac514 100644 --- a/txweb/util/reloader.py +++ b/txweb/util/reloader.py @@ -78,7 +78,7 @@ def main(): _win = (sys.platform == "win32") -def build_list(root_dir, watch_self=False): +def build_list(root_dir, watch_self=False, ignore_prefix=None): """ Walk from root_dir down, collecting all files that end with ^*.py$ to watch @@ -95,15 +95,19 @@ def build_list(root_dir, watch_self=False): if watch_self is True: import txweb print("RELOADER: Watching self") - build_list(pathlib.Path(txweb.__file__).parent.absolute()) + build_list(pathlib.Path(txweb.__file__).parent.absolute(), ignore_prefix=ignore_prefix) + is_list = lambda obj: obj is not None and isinstance(obj, list) for pathobj in root_dir.iterdir(): if pathobj.is_dir(): - build_list(pathobj, watch_self=False) + build_list(pathobj, watch_self=False, ignore_prefix=ignore_prefix) elif pathobj.name.endswith(".py") and not (pathobj.name.endswith(".pyc") or pathobj.name.endswith(".pyo")): stat = pathobj.stat() - _watch_list[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,) + if is_list(ignore_prefix) and any([pathobj.name.startswith(prefix) for prefix in ignore_prefix]) is False: + _watch_list[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,) + else: + log.debug("Ignoring", pathobj.name) else: pass @@ -130,10 +134,10 @@ def file_changed(): return change_detected -def watch_thread(os_exit=SENTINEL_OS_EXIT, watch_self=False): +def watch_thread(os_exit=SENTINEL_OS_EXIT, watch_self=False, ignore_prefix=None): exit_func = os._exit if os_exit is True else sys.exit - build_list(pathlib.Path(os.getcwd()), watch_self=watch_self) + build_list(pathlib.Path(os.getcwd()), watch_self=watch_self, ignore_prefix=ignore_prefix) while True: if file_changed(): @@ -157,7 +161,7 @@ def run_reloader(): return exit_code -def reloader_main(main_func, *args, watch_self=False, **kwargs): +def reloader_main(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs): """ :param main_func: @@ -170,7 +174,7 @@ def reloader_main(main_func, *args, watch_self=False, **kwargs): # If it is, start watcher thread and then run the main_func in the parent process as thread 0 if os.environ.get(SENTINEL_NAME) == "true": - thread.start_new_thread(watch_thread, (), {"os_exit": SENTINEL_OS_EXIT, "watch_self": watch_self}) + thread.start_new_thread(watch_thread, (), {"os_exit": SENTINEL_OS_EXIT, "watch_self": watch_self, "ignore_prefix": ignore_prefix}) try: main_func(*args, **kwargs) except KeyboardInterrupt: @@ -185,9 +189,9 @@ def reloader_main(main_func, *args, watch_self=False, **kwargs): pass -def reloader(main_func, *args, watch_self=False, **kwargs): +def reloader(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs): """ - To avoid fucking with twisted as much as possible, the watcher logic is shunted into + To avoid messing with twisted as much as possible, the watcher logic is shunted into a thread while the main (twisted) reactor runs in the main thread. :param main_func: The function to run in the main/primary thread @@ -201,7 +205,7 @@ def reloader(main_func, *args, watch_self=False, **kwargs): if kwargs is None: kwargs = {} - reloader_main(main_func, *args, watch_self=watch_self, **kwargs) + reloader_main(main_func, *args, watch_self=watch_self, ignore_prefix=ignore_prefix, **kwargs) """ From 6448fe30a41c19d56820f1a59f2f19fe3773c52c Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 18 Nov 2020 10:20:18 -0700 Subject: [PATCH 042/185] Update debug_error.html --- txweb/templates/debug_error.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/txweb/templates/debug_error.html b/txweb/templates/debug_error.html index 5120796..38e0f1f 100644 --- a/txweb/templates/debug_error.html +++ b/txweb/templates/debug_error.html @@ -5,7 +5,7 @@ HTTP Error: {{code}} -

{{code}} - {{message}}

+

Error: {{code}} - {{message}}

{% if traceback %}


From acd5a6048dbe3039bd101203f4048aa6624a0f4c Mon Sep 17 00:00:00 2001
From: devdave 
Date: Wed, 18 Nov 2020 10:20:54 -0700
Subject: [PATCH 043/185] Stop exceptions from rising from here

---
 txweb/web_views.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/txweb/web_views.py b/txweb/web_views.py
index 4359f1f..767ac9c 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -146,7 +146,6 @@ def defaultSiteErrorHandler(request: StrRequest, reason: failure.Failure):
             message = "Processing aborted"
             buffer = template.render(code=code, message=message, traceback=traceback)
             reason.printDetailedTraceback()
-            # reason.raiseException()
 
         request.setHeader(b'content-type', b"text/html")
         request.setHeader(b'content-length', str(len(buffer)).encode("utf-8"))

From 8df63664d867e7adce5de427211bf2f5fca04607 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Wed, 18 Nov 2020 10:21:17 -0700
Subject: [PATCH 044/185] Use old style inheritance to make twistd happier

---
 txweb/web_views.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/txweb/web_views.py b/txweb/web_views.py
index 767ac9c..fb4e9b7 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -96,7 +96,8 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
         routing_resource = routing_resource or RoutingResource()
         request_factory = request_factory or StrRequest
 
-        super(WebSite, self).__init__(routing_resource, requestFactory=request_factory)
+        _RoutingSiteConnectors.__init__(self, routing_resource, requestFactory=request_factory)
+
         self._errorHandler = siteErrorHandler or WebSite.defaultSiteErrorHandler
         self._lastError = None
 

From 7c928d169fff66b345d2cbb5f5cf9aabebc1c902 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 22 Nov 2020 10:08:09 -0700
Subject: [PATCH 045/185] Some more debugging message, TODO switch all back to
 print

---
 txweb/util/reloader.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index aaac514..988d210 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -94,7 +94,7 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
 
     if watch_self is True:
         import txweb
-        print("RELOADER: Watching self")
+        log.info("RELOADER: Watching self")
         build_list(pathlib.Path(txweb.__file__).parent.absolute(), ignore_prefix=ignore_prefix)
 
     is_list = lambda obj: obj is not None and isinstance(obj, list)
@@ -107,7 +107,8 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
             if is_list(ignore_prefix) and any([pathobj.name.startswith(prefix) for prefix in ignore_prefix]) is False:
                 _watch_list[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,)
             else:
-                log.debug("Ignoring", pathobj.name)
+                # print("Ignoring", pathobj.name)
+                pass
         else:
             pass
 
@@ -128,7 +129,7 @@ def file_changed():
             change_detected = True
 
         if change_detected:
-            log.debug(f"RELOADING - {pathobj} changed")
+            print(f"RELOADING - {pathobj} changed")
             break
 
     return change_detected

From 956cb670ae5625f336285590e4c7a6a8f7935dd2 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Wed, 25 Nov 2020 20:09:59 -0700
Subject: [PATCH 046/185] Make PyCharm's linter happy

---
 txweb/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/__init__.py b/txweb/__init__.py
index 5cbfd38..0b33444 100644
--- a/txweb/__init__.py
+++ b/txweb/__init__.py
@@ -3,4 +3,4 @@
 from txweb.application import Application
 
 App = Application
-__all__ = ['NOT_DONE_YET', "App"]
+__all__ = ['NOT_DONE_YET', "App", "Application"]

From ceaae5dafc15d56c0cffc0c0fcf55536fd67ccd4 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Wed, 25 Nov 2020 20:10:47 -0700
Subject: [PATCH 047/185] Finish writing to the client before we let the
 exception play havoc

---
 txweb/lib/errors/handler.py                      | 1 +
 txweb/tests/test_application_error_handling.py   | 5 +++--
 txweb/tests/test_errors_handlers_DebugHandler.py | 9 ++++++---
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 2135274..102afb9 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -92,6 +92,7 @@ def process(self, request: StrRequest, reason: Failure) -> bool:
         else:
             request.setResponseCode(500, b"Internal server error")
             log.debug(f"Non-HTTPCode error was caught: {reason.type} - {reason.value}")
+            request.ensureFinished()
             reason.raiseException()
 
         request.ensureFinished()
diff --git a/txweb/tests/test_application_error_handling.py b/txweb/tests/test_application_error_handling.py
index b9f2f3e..5e0d381 100644
--- a/txweb/tests/test_application_error_handling.py
+++ b/txweb/tests/test_application_error_handling.py
@@ -48,8 +48,9 @@ def test_see_what_happens_with_bad_resources(dummy_request:RequestRetval, caplog
     def handle_foo(request):
         raise RuntimeError("Where is this caught?")
 
-    with caplog.at_level(logging.DEBUG):
-        dummy_request.request.requestReceived(B"GET", b"/foo", b"HTTTP/1.1")
+    with pytest.raises(RuntimeError):
+        with caplog.at_level(logging.DEBUG):
+            dummy_request.request.requestReceived(B"GET", b"/foo", b"HTTTP/1.1")
 
     assert dummy_request.request.code == 500
     assert dummy_request.read().startswith(b"HTTTP/1.1 500 Internal server error")
diff --git a/txweb/tests/test_errors_handlers_DebugHandler.py b/txweb/tests/test_errors_handlers_DebugHandler.py
index e1f5056..d59a8b1 100644
--- a/txweb/tests/test_errors_handlers_DebugHandler.py
+++ b/txweb/tests/test_errors_handlers_DebugHandler.py
@@ -1,3 +1,4 @@
+import pytest
 
 from txweb import Application
 from txweb.lib.errors.handler import DebugHandler
@@ -22,9 +23,10 @@ def test_handler_catches_error(dummy_request:RequestRetval):
 
     @app.add("/foo")
     def handle_foo(request):
-        raise Exception()
+        raise RuntimeError()
 
-    dummy_request.request.requestReceived(b"GET", b"/foo", b"HTTP/1.1")
+    with pytest.raises(RuntimeError):
+        dummy_request.request.requestReceived(b"GET", b"/foo", b"HTTP/1.1")
 
     dummy_request.request.transport.written.seek(0,0)
     content = dummy_request.request.transport.written.read()
@@ -42,7 +44,8 @@ def test_handler_catches_resources_that_return_none(dummy_request:RequestRetval)
     def handle_foo(request):
         pass
 
-    dummy_request.request.requestReceived(b"GET", b"/foo", b"HTTP/1.1")
+    with pytest.raises(RuntimeError):
+        dummy_request.request.requestReceived(b"GET", b"/foo", b"HTTP/1.1")
 
     dummy_request.request.transport.written.seek(0, 0)
     content = dummy_request.request.transport.written.read()

From 28487f458e9588004654f983193253a34492b4a3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Wed, 25 Nov 2020 20:14:16 -0700
Subject: [PATCH 048/185] Massive/bulk commit

Merged ws_texas into txweb.   Provides websocket support to txweb.
---
 requirements.txt                              |   5 +
 txweb/application.py                          | 161 ++++++++++++++++--
 txweb/tests/test_messagehandler.py            |  14 ++
 txweb/tests/test_python.py                    |  64 +++++++
 txweb/tests/test_site_request.py              |   6 +-
 txweb/tests/test_ws_add.py                    |  28 +++
 txweb/tests/test_wsclass.py                   |  83 +++++++++
 txweb/websocket_static_libraries/cookies.js   |  26 +++
 txweb/websocket_static_libraries/deferred.js  |  27 +++
 txweb/websocket_static_libraries/library.js   |   7 +
 .../resilient_socket.js                       | 132 ++++++++++++++
 11 files changed, 537 insertions(+), 16 deletions(-)
 create mode 100644 txweb/tests/test_messagehandler.py
 create mode 100644 txweb/tests/test_python.py
 create mode 100644 txweb/tests/test_ws_add.py
 create mode 100644 txweb/tests/test_wsclass.py
 create mode 100644 txweb/websocket_static_libraries/cookies.js
 create mode 100644 txweb/websocket_static_libraries/deferred.js
 create mode 100644 txweb/websocket_static_libraries/library.js
 create mode 100644 txweb/websocket_static_libraries/resilient_socket.js

diff --git a/requirements.txt b/requirements.txt
index 5b819bb..fc25934 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,8 @@ six==1.11.0
 Twisted==20.3.0
 Werkzeug==0.16.0
 zope.interface==4.4.3
+
+setuptools~=40.4.3
+txweb~=0.11.2019
+pytest~=3.2.3
+recommonmark~=0.6.0
\ No newline at end of file
diff --git a/txweb/application.py b/txweb/application.py
index 5cbccdb..6997e0a 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -15,14 +15,27 @@
 from pathlib import Path
 import typing as T
 import sys
+import inspect
+import functools
 # 3rd party
 from twisted.internet.tcp import Port
 from twisted.internet.posixbase import PosixReactorBase
 from twisted.internet import reactor  # type: PosixReactorBase
 from twisted.python.compat import intToBytes
+from twisted.web.server import NOT_DONE_YET
 from twisted.web.static import File
 from twisted.python import failure
 
+try:
+    import autobahn
+except ImportError:
+    AUTOBAHN_MISSING = True
+else:
+    AUTOBAHN_MISSING = False
+    from .lib.at_wsprotocol import AtWSProtocol
+    from .lib.message_handler import MessageHandler
+    from .lib.routed_factory import RoutedWSFactory
+
 # Application
 from .log import getLogger
 from .resources import RoutingResource, SimpleFile, Directory
@@ -32,6 +45,9 @@
 from .lib.errors.handler import DefaultHandler, DebugHandler, BaseHandler
 
 
+HERE = Path(__file__).parent
+WS_STATIC_LIB = HERE / "websocket_static_libraries"
+
 log = getLogger(__name__)
 
 if T.TYPE_CHECKING:
@@ -45,6 +61,120 @@
     ErrorHandler = T.NewType("ErrorHandler", T.Callable[[StrRequest, failure.Failure], T.Union[bool, None]])
 
 
+class ApplicationWebsocketMixin(object):
+
+    WS_EXPOSED_FUNC = "WS_EXPOSED_FUNC"
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.ws_endpoints = {}
+        self.ws_factory = None
+        self.ws_resource = None
+        self.ws_instances = {}
+
+    def enable_websockets(self, url, route):
+        if AUTOBAHN_MISSING is True:
+            raise EnvironmentError("Unable to provide websocket support without autobahn installed/present")
+
+        self.ws_factory = RoutedWSFactory(url, self.ws_endpoints)
+        self.ws_resource = WebSocketResource(self.ws_factory)
+        self.add_resource(route, self.ws_resource)
+
+    def ws_add(self, name, assign_args=False) -> T.Callable[[WSEndpoint], WSEndpoint]:
+
+        def processor(func: WSEndpoint) -> WSEndpoint:
+            self.ws_endpoints[name] = func
+            if assign_args is True:
+                func = self.websocket_function_arguments_decorator(func)
+            return func
+
+        return processor
+
+    # noinspection SpellCheckingInspection
+    def ws_sharelib(self, route_str="/lib"):
+        self.add_staticdir2(route_str, WS_STATIC_LIB)
+
+    def ws_class(self, kls):
+        kls_name = kls.__name__.lower()
+        if kls_name in self.ws_instances:
+            raise ValueError(f"Websocket name: {kls_name} class is already registered!")
+
+        self.ws_instances[kls_name] = kls(self)
+
+        methods = [member
+                   for member in inspect.getmembers(self.ws_instances[kls_name])
+                   if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
+
+        for name, method in methods:
+            self.ws_endpoints[f"{kls_name}.{name.lower()}"] = method
+
+        return kls
+
+    @staticmethod
+    def websocket_class_arguments_decorator(func):
+        params = inspect.signature(func).parameters
+        arg_keys = {}
+        for param_name, param in params.items():  # type: inspect.Parameter
+            if param.default is not inspect.Parameter.empty:
+                if param.name in ["connection", "message"]:
+                    raise TypeError(f"Cannot use assign_args when using keyword arguments `connection` or `message`: {param.name}")
+                arg_keys[param_name] = param.default
+
+        if "connection" not in params or "message" not in params:
+            raise TypeError("ws_expose convention expects (self, connection, message, **kwargs)")
+
+
+        @functools.wraps(func)
+        def argument_decorator(parent, connection, message):
+            kwargs = {}
+
+
+            for arg_name, arg_default in arg_keys.items():
+                kwargs[arg_name] = message.get(arg_name, arg_default)
+
+            return func(parent, connection, message, **kwargs)
+
+        return argument_decorator
+
+    @staticmethod
+    def websocket_function_arguments_decorator(func):
+        params = inspect.signature(func).parameters
+        arg_keys = {}
+        for name, param in params.items():  # type: inspect.Parameter
+            if param.default is not inspect.Parameter.empty:
+                if param.name in ["connection", "message"]:
+                    raise TypeError(
+                        f"Cannot use assign_args when using keyword arguments `connection` or `message`: {param.name}")
+                arg_keys[name] = param.default
+
+        if "connection" not in params or "message" not in params:
+            raise TypeError("ws_add convention expects (connection, message)")
+
+        @functools.wraps(func)
+        def argument_decorator(connection, message: MessageHandler):
+            kwargs = {}
+
+            for name, default in arg_keys.items():
+                kwargs[name] = message.args(name, default=default) #TODO also use annotation for type-casting
+
+            return func(connection, message, **kwargs)
+
+        return argument_decorator
+
+
+    def ws_expose(self, func: callable = None, assign_args=False):
+
+        if func is None and assign_args is True:
+            def processor(real_func):
+                setattr(real_func, self.WS_EXPOSED_FUNC, True)
+                magic_func = self.websocket_class_arguments_decorator(real_func)
+                return magic_func
+            return processor
+
+        else:
+            setattr(func, self.WS_EXPOSED_FUNC, True)
+            return func
+
 
 class ApplicationRoutingHelperMixin(object):
     """
@@ -199,31 +329,32 @@ def processingFailed(self, request:StrRequest, reason: failure.Failure):
 
 
 
-class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin):
-    """
-        Similar to Klein and its influence Flask, the goal is to consolidate
-        technical debt into one God module antipattern class.
+class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin, ApplicationWebsocketMixin):
 
-        Purposes:
-            Provides a public API to Site, HTTPErrors, RoutingResource, and additional helpers.
 
-        Arguments:
-            namespace: the base module/package name of the application, currently intended to assist logging and debugging
-            twisted_reactor: currently unused
-            request_factory: currently unused
-            enable_debug: Not implemented yet, switches on extra debugging tools
-    """
+    NOT_DONE_YET = NOT_DONE_YET
 
     def __init__(self,
                  namespace: str = None,
                  twisted_reactor: T.Optional[PosixReactorBase] = None,
                  request_factory:StrRequest=StrRequest,
-                 enable_debug:bool=False,
-                 base_dir = None
+                 enable_debug:bool=False
                  ):
         """
+            Similar to Klein and its influence Flask, the goal is to consolidate
+            technical debt into one God module antipattern class.
+
+            Purposes:
+                Provides a public API to Site, HTTPErrors, RoutingResource, and additional helpers.
 
+            Arguments:
+                namespace: the base module/package name of the application, currently intended to assist logging and debugging
+                twisted_reactor: currently unused
+                request_factory: currently unused
+                enable_debug: Not implemented yet, switches on extra debugging tools
         """
+
+
         self._router = RoutingResource()
         self._site = WebSite(self._router, request_factory=Application.request_factory_partial(self, request_factory))
         self._router.site = self._site
@@ -239,7 +370,7 @@ def __init__(self,
             self.__owner_module = None
 
 
-
+        ApplicationWebsocketMixin.__init__(self)
         ApplicationRoutingHelperMixin.__init__(self)
         # _ApplicationTemplateSupportMixin.__init__(self)
         ApplicationErrorHandlingMixin.__init__(self, enable_debug=enable_debug)
diff --git a/txweb/tests/test_messagehandler.py b/txweb/tests/test_messagehandler.py
new file mode 100644
index 0000000..2d9bf04
--- /dev/null
+++ b/txweb/tests/test_messagehandler.py
@@ -0,0 +1,14 @@
+from txweb.lib.message_handler import MessageHandler
+
+
+def test_type_casting():
+
+    message = MessageHandler({"foo":"1"}, None)
+    actual = message.get("foo", type=int)
+    assert actual == 1
+
+
+def test_type_casting_args():
+    message = MessageHandler({"args": {"foo":"1"}}, None)
+    actual = message.args("foo", type=int)
+    assert actual == 1
\ No newline at end of file
diff --git a/txweb/tests/test_python.py b/txweb/tests/test_python.py
new file mode 100644
index 0000000..ce53863
--- /dev/null
+++ b/txweb/tests/test_python.py
@@ -0,0 +1,64 @@
+import functools
+import pytest
+
+def test_boundmethod_decorator():
+
+
+    def decorator1(func):
+
+        @functools.wraps(func)
+        def processor(*args, **kwargs):
+            # for position, arg in enumerate(args):
+            #     print(f"{position}, {arg!r}")
+
+
+            return func(*args, **kwargs)
+        return processor
+
+    def decorator2(func):
+        def processor(arg1, arg2):
+            return func(arg1, arg2)
+
+        return processor
+
+    def decorator3(func):
+        def processor(parent, arg1, arg2):
+            return func(parent, arg1, arg2)
+
+        return processor
+
+
+    class ToyClass:
+
+        @decorator1
+        def method1(self, arg1, kw1=None):
+            pass
+
+        @decorator2
+        def method2(self, arg1, arg2):
+            pass
+
+        @decorator3
+        def method3(self, arg1, arg2):
+            pass
+
+        @decorator3
+        def method4(*args, **kwargs):
+            pass
+            # for position, arg in enumerate(args):
+            #     print(f"{position}: {arg!r}")
+
+
+    instance = ToyClass()
+    #
+    # print()
+    # print("Starting decorator types test")
+
+    instance.method1("Hello World", kw1="Foo")
+
+    with pytest.raises(TypeError):
+        instance.method2("Hello", "World")
+
+    instance.method3("Foo", "Bar")
+
+    instance.method4("Hello", "World!")
\ No newline at end of file
diff --git a/txweb/tests/test_site_request.py b/txweb/tests/test_site_request.py
index 717d88e..c031ca1 100644
--- a/txweb/tests/test_site_request.py
+++ b/txweb/tests/test_site_request.py
@@ -1,3 +1,5 @@
+import pytest
+
 from txweb.application import Application
 from txweb.lib import StrRequest
 from .conftest import RequestRetval
@@ -14,7 +16,9 @@ def do_foo(request):
 
     dummy_request.request.site = app.site
     dummy_request.channel.site = app.site
-    dummy_request.request.requestReceived(b"HEAD", b"/foo", b"HTTP1/1")
+
+    with pytest.raises(RuntimeError):
+        dummy_request.request.requestReceived(b"HEAD", b"/foo", b"HTTP1/1")
 
     assert dummy_request.request.code == 500
     assert dummy_request.request.code_message == b'Internal server error'
diff --git a/txweb/tests/test_ws_add.py b/txweb/tests/test_ws_add.py
new file mode 100644
index 0000000..5183cca
--- /dev/null
+++ b/txweb/tests/test_ws_add.py
@@ -0,0 +1,28 @@
+import pytest
+from txweb.lib.message_handler import MessageHandler
+from txweb import Application as WSApp
+
+
+def test_ws_add_works():
+
+    app = WSApp(__name__) # type: WSApp
+
+    @app.ws_add("foo.bar")
+    def provide_foo_bar(connection, message):
+        pass
+
+    assert "foo.bar" in app.ws_endpoints
+
+def test_ws_assign_args_flag_works():
+
+    app = WSApp(__name__)
+
+    @app.ws_add("alice.bob", assign_args=True)
+    def provide_alice_bob(connection, message, which_one: str = None):
+        return which_one
+
+    message = MessageHandler(dict(args=dict(which_one="Steve!!!")), None)
+
+    result = provide_alice_bob(None, message)
+
+    assert result == "Steve!!!"
\ No newline at end of file
diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_wsclass.py
new file mode 100644
index 0000000..43bbb82
--- /dev/null
+++ b/txweb/tests/test_wsclass.py
@@ -0,0 +1,83 @@
+import pytest
+
+from txweb import Application as WSApp
+from txweb.lib.message_handler import MessageHandler
+
+def test_concept():
+
+    app = WSApp(__name__)
+
+
+    @app.ws_class
+    class Dummy(object):
+
+        def __init__(self, application: WSApp):
+            self.app = application
+
+        @app.ws_expose
+        def hello(self, connection, message):
+            return "World"
+
+        @app.ws_expose
+        def sum_nums(self, connection, message):
+            return sum(message.get('arguments', []))
+
+
+    assert "dummy.hello" in app.ws_endpoints
+    assert "dummy.sum_nums" in app.ws_endpoints
+
+
+def test_persistence():
+
+    app = WSApp(__name__)
+
+    @app.ws_class
+    class Dummy2(object):
+
+        def __init__(self, app: WSApp):
+            self.app = app
+            self.counter = 0
+
+        @app.ws_expose
+        def increment(self, connection, message):
+            self.counter += 1
+            return None
+
+
+    assert app.ws_instances['dummy2'].counter == 0
+    app.ws_endpoints['dummy2.increment'](None, {})
+    assert app.ws_instances['dummy2'].counter == 1
+
+
+def test_magic_arguments():
+    app = WSApp(__name__)
+
+
+    @app.ws_class
+    class Endpoint:
+
+        def __init__(self, app):
+            self.app = app
+
+        @app.ws_expose(assign_args=True)
+        def test_function(self, connection, message, foo=False, bar=None):
+            return foo, bar
+
+        with pytest.raises(TypeError, match="^ws_expose convention expects.*"):
+            @app.ws_expose(assign_args=True)
+            def test_bad_convention(self, protocol, incoming):
+                pass
+
+    assert "endpoint" in app.ws_instances
+
+    instance = app.ws_instances['endpoint']
+
+    message = MessageHandler({"args":dict(foo=True, bar="Hello World!")}, None)
+
+    foo, bar = instance.test_function(None, message)
+
+    assert foo == True
+    assert bar == "Hello World!"
+
+    assert hasattr(instance.test_function, app.WS_EXPOSED_FUNC) is True
+
diff --git a/txweb/websocket_static_libraries/cookies.js b/txweb/websocket_static_libraries/cookies.js
new file mode 100644
index 0000000..3da50cd
--- /dev/null
+++ b/txweb/websocket_static_libraries/cookies.js
@@ -0,0 +1,26 @@
+
+export {parse_cookies};
+
+function parse_cookies(raw_cookies) {
+
+    raw_cookies = raw_cookies.split(";")
+    let cookies = {}
+
+    if (raw_cookies == "") {
+        return cookies;
+    }
+
+    for(let raw_cookie of raw_cookies) {
+        raw_cookie = raw_cookie.trim();
+        let key_name, key_value
+        [key_name, key_value] = raw_cookie.split("=", 2);
+        cookies[key_name.trim()] = key_value.trim();
+        console.log(key_name, key_value);
+    }
+
+    return cookies;
+}
+
+function set_cookie(name, value, exp) {
+    alert("no");  //set cookies via server side
+}
\ No newline at end of file
diff --git a/txweb/websocket_static_libraries/deferred.js b/txweb/websocket_static_libraries/deferred.js
new file mode 100644
index 0000000..d7c1634
--- /dev/null
+++ b/txweb/websocket_static_libraries/deferred.js
@@ -0,0 +1,27 @@
+export {Deferred};
+
+class Deferred {
+
+    constructor() {
+        this.cbs = [];
+        this.ebs = [];
+        this.fired = false;
+    }
+
+    fire(result) {
+        if (this.fired == true) {
+
+        }
+        for(let pos in this.cbs) {
+            let func = this.cbs[pos];
+            console.log(pos, func)
+            result = func(result);
+        }
+    }
+
+    then(callback) {
+        this.cbs.push(callback);
+    }
+
+}
+
diff --git a/txweb/websocket_static_libraries/library.js b/txweb/websocket_static_libraries/library.js
new file mode 100644
index 0000000..f015d5c
--- /dev/null
+++ b/txweb/websocket_static_libraries/library.js
@@ -0,0 +1,7 @@
+import {Deferred} from "./deferred.js";
+import {ResilientSocket} from "./resilient_socket.js";
+
+export {ResilientSocket, Deferred};
+
+
+
diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
new file mode 100644
index 0000000..c6dd5b2
--- /dev/null
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -0,0 +1,132 @@
+
+export {ResilientSocket};
+import {Deferred} from "./deferred.js";
+
+class ResilientSocket {
+    constructor(conString, {debug=false} = {}) {
+        this.conString = conString;
+        this.closed = true;
+        this.socket = null;
+        this.callerID = 0;
+        this.pending = {};
+
+        //misc flags
+        this.debug = debug;
+
+        this.retries = 0;  //handled on server side firewall but to keep the client from freezing, limit it on this side
+        this.retryLimit = 3;
+
+
+        this.endpoints = {};
+    }
+
+    connect() {
+
+        if(this.retries > this.retryLimit) {
+            console.error("Number of connect retries reached limit!");
+            // TODO add a on_timeout or on_limit hook so the application can handle
+            // connection retries failing.
+        }
+
+        console.debug("connecting resilient");
+        this.socket = new WebSocket(this.conString);
+        this.socket.addEventListener("message", this.receiveMsg.bind(this));
+        this.socket.addEventListener("close", this.onclose.bind(this));
+        this.socket.addEventListener("open", this.onopen.bind(this));
+        this.socket.addEventListener("error", this.onerror.bind(this));
+    }
+
+    onopen() {
+        this.closed = false;
+        this.retries = 0;
+    }
+
+    onclose() {
+        this.closed = true;
+    }
+
+    onerror(evt) {
+        console.log(evt);
+        this.retries += 1;
+
+    }
+
+    first_open(handler) {
+        this.socket.addEventListener("open", handler, {once:true});
+    }
+
+    _sendRaw(msg) {
+        //Check if disconnected
+        if(this.closed == true) {
+            this.connect();
+            this.first_open( x=>this.socket.send(msg));
+        } else {
+            this.socket.send(msg);
+        }
+
+    }
+
+    async sendMsg(endpoint, args) {
+        let msg = {type:"tell", endpoint:endpoint, args: args};
+        this._sendRaw(JSON.stringify(msg));
+    }
+
+    async receiveMsg(msg) {
+        if(this.debug) {
+            console.debug(msg);
+        }
+
+        let data = JSON.parse(msg.data);
+        if (data['type'] == "ask") {
+            let d = this.pending[data['caller_id']];
+            d.fire(data.result);
+            delete this.pending[data['caller_id']];
+        }
+        else if(data['type'] == "call" || data['type'] == "tell") {
+            if(this.endpoints[data['endpoint']] != undefined){
+                let endpoint = this.endpoints[data['endpoint']];
+                let response = await endpoint(data['arguments'], data);
+            } else {
+                console.error(`Tell the user that ${data['endpoint']} doesn't exist`);
+            }
+        } else {
+            console.error("unhandled message", data);
+        }
+    }
+
+    ask(endpoint, args) {
+        /**
+         * Expects a response using Deferred for callbacks
+         */
+        console.debug("Asking", endpoint);
+
+        let d = new Deferred()
+        this.callerID += 1;
+        this.pending[this.callerID] = d;
+
+        let msg = {type:"ask", endpoint:endpoint, args: args, caller_id: this.callerID};
+        this._sendRaw(JSON.stringify(msg));
+
+        return d;
+    }
+
+    call(endpoint, args) {
+        /**
+         * Tell the server something has happened but don't expect a direct response.
+         *
+         * @type {{args: *, endpoint: *, type: string}}
+         */
+        let msg = {type:"call", endpoint:endpoint, args: args};
+        this._sendRaw(JSON.stringify(msg));
+    }
+
+    tell(endpoint, args) {
+        let msg = {type:"tell", endpoint: endpoint, args: args};
+        this._sendRaw(JSON.stringify(msg));
+    }
+
+    register(endpoint, func) {
+        this.endpoints[endpoint] = func
+    }
+
+}
\ No newline at end of file

From eec7dde0f335a0f570aa533516b1e5779e97b153 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 26 Nov 2020 10:24:51 -0700
Subject: [PATCH 049/185] Added missing files for websocket support and major
 refactor to allign verbs:  ask, tell, response

---
 txweb/application.py                          |  28 ++--
 txweb/lib/at_wsprotocol.py                    | 139 ++++++++++++++++++
 txweb/lib/message_handler.py                  | 102 +++++++++++++
 txweb/lib/routed_factory.py                   |  17 +++
 .../resilient_socket.js                       |  26 +++-
 5 files changed, 293 insertions(+), 19 deletions(-)
 create mode 100644 txweb/lib/at_wsprotocol.py
 create mode 100644 txweb/lib/message_handler.py
 create mode 100644 txweb/lib/routed_factory.py

diff --git a/txweb/application.py b/txweb/application.py
index 6997e0a..801d8be 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -30,11 +30,13 @@
     import autobahn
 except ImportError:
     AUTOBAHN_MISSING = True
+    print("Unable to support websockets:  `pip install autobahn` to enable")
 else:
     AUTOBAHN_MISSING = False
     from .lib.at_wsprotocol import AtWSProtocol
     from .lib.message_handler import MessageHandler
     from .lib.routed_factory import RoutedWSFactory
+    from autobahn.twisted.resource import WebSocketResource
 
 # Application
 from .log import getLogger
@@ -76,7 +78,7 @@ def enable_websockets(self, url, route):
         if AUTOBAHN_MISSING is True:
             raise EnvironmentError("Unable to provide websocket support without autobahn installed/present")
 
-        self.ws_factory = RoutedWSFactory(url, self.ws_endpoints)
+        self.ws_factory = RoutedWSFactory(url, self.ws_endpoints, application=self)
         self.ws_resource = WebSocketResource(self.ws_factory)
         self.add_resource(route, self.ws_resource)
 
@@ -116,23 +118,23 @@ def websocket_class_arguments_decorator(func):
         arg_keys = {}
         for param_name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
-                if param.name in ["connection", "message"]:
-                    raise TypeError(f"Cannot use assign_args when using keyword arguments `connection` or `message`: {param.name}")
+                if param.name in ["message"]:
+                    raise TypeError(f"Cannot use assign_args when using keyword argument `message`: {param.name}")
                 arg_keys[param_name] = param.default
 
-        if "connection" not in params or "message" not in params:
-            raise TypeError("ws_expose convention expects (self, connection, message, **kwargs)")
+        if "message" not in params:
+            raise TypeError("ws_expose convention expects (self, message, **kwargs)")
 
 
         @functools.wraps(func)
-        def argument_decorator(parent, connection, message):
+        def argument_decorator(parent, message):
             kwargs = {}
 
 
             for arg_name, arg_default in arg_keys.items():
                 kwargs[arg_name] = message.get(arg_name, arg_default)
 
-            return func(parent, connection, message, **kwargs)
+            return func(parent, message, **kwargs)
 
         return argument_decorator
 
@@ -142,22 +144,22 @@ def websocket_function_arguments_decorator(func):
         arg_keys = {}
         for name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
-                if param.name in ["connection", "message"]:
+                if param.name in ["message"]:
                     raise TypeError(
-                        f"Cannot use assign_args when using keyword arguments `connection` or `message`: {param.name}")
+                        f"Cannot use assign_args when using keyword arguments `message`: {param.name}")
                 arg_keys[name] = param.default
 
-        if "connection" not in params or "message" not in params:
-            raise TypeError("ws_add convention expects (connection, message)")
+        if "message" not in params:
+            raise TypeError("ws_add convention expects endpoint(message)")
 
         @functools.wraps(func)
-        def argument_decorator(connection, message: MessageHandler):
+        def argument_decorator(message: MessageHandler):
             kwargs = {}
 
             for name, default in arg_keys.items():
                 kwargs[name] = message.args(name, default=default) #TODO also use annotation for type-casting
 
-            return func(connection, message, **kwargs)
+            return func(message, **kwargs)
 
         return argument_decorator
 
diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
new file mode 100644
index 0000000..c6ef4e3
--- /dev/null
+++ b/txweb/lib/at_wsprotocol.py
@@ -0,0 +1,139 @@
+try:
+    import ujson as json
+except ImportError:
+    import json
+
+from uuid import uuid4
+
+
+from twisted.web.server import NOT_DONE_YET
+from twisted.internet.defer import Deferred
+
+from autobahn.twisted.websocket import WebSocketServerProtocol
+
+from .message_handler import MessageHandler
+
+from txweb.log import getLogger
+
+class AtWSProtocol(WebSocketServerProtocol):
+
+    my_log = getLogger()
+
+    def __init__(self, *args, **kwargs):
+        self.pending_responses = {}
+
+        WebSocketServerProtocol.__init__(self, *args, **kwargs)
+
+        self.identity = None
+        self.on_disconnect = Deferred()
+
+        self._raw_message = {}
+
+    @property
+    def application(self):
+        return self.factory.get_application()
+
+    def getCookie(self, cookie_name, default=None):
+        raw_cookies = self.http_headers.get('cookie', "")
+        cookies = {}
+        for params in raw_cookies.split(";"):
+            str_name, value = params.split("=")
+            if str_name.strip() == cookie_name:
+                return value.strip()
+
+        return default
+
+    def onConnect(self, request):
+        self.identity = uuid4().hex
+        self.my_log.debug("Client connecting: {request.peer}", request=request)
+
+    def onOpen(self):
+        self.my_log.debug("WebSocket connection open.")
+
+    def onClose(self, wasClean, code, reason):
+        self.on_disconnect.addErrback(self.my_log.error)
+        self.on_disconnect.callback(self.identity)
+        del self.on_disconnect
+        self.my_log.debug("WebSocket connection closed: {reason!r}", reason=reason)
+
+    def onClosed(self, *args, **kwargs):
+        self.my_log.debug(*args, **kwargs)
+
+    def sendDict(self, **values):
+        response = json.dumps(values)
+        # Always send synchronously for now
+        self.sendMessage(response.encode("utf-8"), isBinary=False, sync=True)
+
+    def tell(self, endpoint, **values):
+        """
+            Tell the client to do something and don't expect a response
+
+        :param endpoint:
+        :param values:
+        :return:
+        """
+        self.sendDict(endpoint=endpoint, type="tell", arguments=values)
+
+    callDict = tell
+
+    def ask(self, endpoint, **values):
+        """
+            Ask the client to do something and I should get a response back.
+
+        :param endpoint:
+        :param values:
+        :return:
+        """
+        requestToken = uuid4().hex
+        self.sendDict(endpoint=endpoint, type="ask", token=requestToken, arguments=values)
+
+    call = ask
+
+    def sendTell(self, data, result):
+        self.sendDict(caller_id=data['caller_id'], type="tell", result=result)
+
+
+    def sendTellAsDict(self, data, **result):
+        self.sendTell(data, result)
+
+    def respondAsDict(self, data, **result):
+        self.sendDict(caller_id=data["caller_id"], end_point=data["endpoint"], type="response", result=result)
+
+
+    def onMessage(self, payload, isBinary):
+        if isBinary:
+            return None # I don't know how to deal with binary
+
+        payload = payload.decode("utf-8")
+        message = MessageHandler(json.loads(payload), self)
+        result = None
+
+        if "endpoint" in message:
+            endpoint_func = self.factory.get_endpoint(message['endpoint'])
+            self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
+
+            if endpoint_func is None:
+                self.my_log.error("Bad endpoint {endpoint}", endpoint=call_data['endpoint'])
+                return
+            else:
+                result = endpoint_func(message)
+        else:
+            self.my_log.error("Got message without an endpoint: {raw!r}", raw=message.raw_message)
+            raise Exception("Got message without a endpoint")
+
+        if result == NOT_DONE_YET:
+            return
+
+        elif result is None:
+            return
+
+        elif isinstance(result, Deferred):
+            return result
+
+        elif call_data['type'] == "ask":
+            self.respondAsDict(message, result=result)
+        else:
+            response = json.dumps(result)
+            ## echo back message verbatim
+            self.sendMessage(response.encode("utf-8"), isBinary)
+
diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
new file mode 100644
index 0000000..2d20370
--- /dev/null
+++ b/txweb/lib/message_handler.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+import typing as T
+
+from twisted.web.server import NOT_DONE_YET
+
+from collections.abc import Mapping
+
+if T.TYPE_CHECKING or False:
+    # recursion import
+    from .at_wsprotocol import AtWSProtocol
+
+
+
+
+class MessageHandler(Mapping):
+
+    def __init__(self, raw_message:dict, connection:AtWSProtocol):
+        self.raw_message = raw_message # type: dict
+        self.connection = connection
+
+    def __getitem__(self, item):
+        return self.raw_message[item]
+
+    def __iter__(self):
+        return self.raw_message.__iter__()
+
+    def __len__(self):
+        return len(self.raw_message)
+
+    def __contains__(self, item):
+        return item in self.raw_message
+
+    def keys(self):
+        return self.raw_message1.keys()
+
+    def items(self):
+        return self.raw_message.items()
+
+    def values(self):
+        return self.raw_message.values()
+
+    def get(self, key, default=None, type=None):
+
+        try:
+            args = self['args']
+            value = args[key]
+
+        except KeyError:
+            try:
+                value = self[key]
+            except KeyError:
+                return default
+
+        if type:
+            try:
+                value = type(value)
+            except ValueError:
+                return default
+
+        return value
+
+    def args(self, key, default=None, type=None):
+        args = None
+        value = default
+        try:
+            args = self['args']
+        except KeyError:
+            return default
+
+
+        try:
+            value = args[key]
+        except KeyError:
+            return default
+        except ValueError:
+            return default
+
+        if type:
+            try:
+                value = type(value)
+            except ValueError:
+                return default
+
+        return value
+
+    @property
+    def is_asking(self):
+        return "caller_id" in self.raw_message
+
+    def respond(self, **kwargs):
+        self.connection.respondAsDict(self.raw_message, result=kwargs)
+        return NOT_DONE_YET
+
+    def tell(self, endpoint, **kwargs):
+        self.connection.sendDict(endpoint=endpoint, type="tell", args=kwargs)
+
+    def ask(self, endpoint, **kwargs):
+        #TODO setup a deferred somewhere in here or below in protocol
+        return self.connection.ask(endpoint, type="ask", args=kwargs)
+
+    def get_session(self, get_key=None):
+        return self.connection.application.get_session(self.connection, get_key=get_key)
\ No newline at end of file
diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
new file mode 100644
index 0000000..e970d0a
--- /dev/null
+++ b/txweb/lib/routed_factory.py
@@ -0,0 +1,17 @@
+from autobahn.twisted.websocket import WebSocketServerFactory
+from .at_wsprotocol import AtWSProtocol
+
+class RoutedWSFactory(WebSocketServerFactory):
+
+    def __init__(self, url, routes, protocol_cls=AtWSProtocol, application=None):
+        WebSocketServerFactory.__init__(self, url)
+        self.protocol = protocol_cls
+
+        self._application = application # Necessary so further down in protocol land we can get the session
+        self.routes = routes
+
+    def get_endpoint(self, name):
+        return self.routes.get(name, None)
+
+    def get_application(self):
+        return self._application
\ No newline at end of file
diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
index c6dd5b2..8a771d8 100644
--- a/txweb/websocket_static_libraries/resilient_socket.js
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -71,24 +71,38 @@ class ResilientSocket {
         this._sendRaw(JSON.stringify(msg));
     }
 
+    async sendResponse(caller_id, result){
+        let msg = {type:"response", caller_id:caller_id, result:result}
+        this._sendRaw(JSON.stringify(msg));
+    }
+
     async receiveMsg(msg) {
         if(this.debug) {
             console.debug(msg);
         }
 
         let data = JSON.parse(msg.data);
-        if (data['type'] == "ask") {
+        if (data['type'] == "response") {
             let d = this.pending[data['caller_id']];
             d.fire(data.result);
             delete this.pending[data['caller_id']];
         }
-        else if(data['type'] == "call" || data['type'] == "tell") {
-            if(this.endpoints[data['endpoint']] != undefined){
-                let endpoint = this.endpoints[data['endpoint']];
-                let response = await endpoint(data['arguments'], data);
+        else if(data['type'] == "tell") {
+            if (this.endpoints[data['endpoint']] != undefined) {
+                const endpoint = this.endpoints[data['endpoint']];
+                await endpoint(data.args);
             } else {
-                console.error(`Tell the user that ${data['endpoint']} doesn't exist`);
+                console.error(`Tell the user that ${data['endpoint']} doesn't exist`, data);
             }
+        } else if(data['type'] == "ask") {
+            if(this.endpoints[data['endpoint']] != undefined) {
+                const endpoint = this.endpoints[data['endpoint']];
+                let result = await endpoint(data.args);
+                await this.sendResponse(data['caller_id'], result);
+            } else{
+                console.error(`User asked for ${data['endpoint']} that doesn't exist`, data);
+            }
+
         } else {
             console.error("unhandled message", data);
         }

From 1418be667c826c750cbb368618e261d02abe57e3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 26 Nov 2020 10:38:24 -0700
Subject: [PATCH 050/185] Remove lib as an ignored directory

---
 .gitignore | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 8678557..1c95fcc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,6 @@ eggs
 .eggs
 parts
 *.egg-info
-lib
 lib64
 
 # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm

From ea4d737a5ce048238028b97640785332b5a0433f Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 26 Nov 2020 14:12:39 -0700
Subject: [PATCH 051/185] Make things consistent between js and python API

---
 txweb/lib/at_wsprotocol.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index c6ef4e3..86a085e 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -72,7 +72,7 @@ def tell(self, endpoint, **values):
         :param values:
         :return:
         """
-        self.sendDict(endpoint=endpoint, type="tell", arguments=values)
+        self.sendDict(endpoint=endpoint, type="tell", args=values)
 
     callDict = tell
 
@@ -85,7 +85,7 @@ def ask(self, endpoint, **values):
         :return:
         """
         requestToken = uuid4().hex
-        self.sendDict(endpoint=endpoint, type="ask", token=requestToken, arguments=values)
+        self.sendDict(endpoint=endpoint, type="ask", token=requestToken, args=values)
 
     call = ask
 

From dc39e1387fd2ca51852fec1383fdcd5af431cd4e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 09:11:46 -0700
Subject: [PATCH 052/185] Slight work on documentation

---
 source/api/txweb.lib.errors.rst |  1 -
 source/api/txweb.lib.rst        | 26 +++++++++++++++++++++++++-
 source/api/txweb.resources.rst  |  1 -
 source/api/txweb.rst            |  8 ++++----
 source/api/txweb.util.rst       |  9 ++++++++-
 source/index.rst                |  7 +++++--
 source/manual/quick_start.md    | 18 ++++++++++++++++++
 txweb/application.py            | 19 ++++++++++---------
 8 files changed, 70 insertions(+), 19 deletions(-)
 create mode 100644 source/manual/quick_start.md

diff --git a/source/api/txweb.lib.errors.rst b/source/api/txweb.lib.errors.rst
index 04ecff4..e572cf4 100644
--- a/source/api/txweb.lib.errors.rst
+++ b/source/api/txweb.lib.errors.rst
@@ -20,7 +20,6 @@ txweb.lib.errors.html module
    :undoc-members:
    :show-inheritance:
 
-
 Module contents
 ---------------
 
diff --git a/source/api/txweb.lib.rst b/source/api/txweb.lib.rst
index 2ab43b3..eb069b5 100644
--- a/source/api/txweb.lib.rst
+++ b/source/api/txweb.lib.rst
@@ -5,12 +5,37 @@ Subpackages
 -----------
 
 .. toctree::
+   :maxdepth: 4
 
    txweb.lib.errors
 
 Submodules
 ----------
 
+txweb.lib.at\_wsprotocol module
+-------------------------------
+
+.. automodule:: txweb.lib.at_wsprotocol
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+txweb.lib.message\_handler module
+---------------------------------
+
+.. automodule:: txweb.lib.message_handler
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+txweb.lib.routed\_factory module
+--------------------------------
+
+.. automodule:: txweb.lib.routed_factory
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 txweb.lib.str\_request module
 -----------------------------
 
@@ -27,7 +52,6 @@ txweb.lib.view\_class\_assembler module
    :undoc-members:
    :show-inheritance:
 
-
 Module contents
 ---------------
 
diff --git a/source/api/txweb.resources.rst b/source/api/txweb.resources.rst
index 53cf2bf..ab5944b 100644
--- a/source/api/txweb.resources.rst
+++ b/source/api/txweb.resources.rst
@@ -52,7 +52,6 @@ txweb.resources.view\_function module
    :undoc-members:
    :show-inheritance:
 
-
 Module contents
 ---------------
 
diff --git a/source/api/txweb.rst b/source/api/txweb.rst
index dbcd019..a253619 100644
--- a/source/api/txweb.rst
+++ b/source/api/txweb.rst
@@ -5,6 +5,7 @@ Subpackages
 -----------
 
 .. toctree::
+   :maxdepth: 4
 
    txweb.lib
    txweb.resources
@@ -21,10 +22,10 @@ txweb.application module
    :undoc-members:
    :show-inheritance:
 
-txweb.errors module
--------------------
+txweb.http\_codes module
+------------------------
 
-.. automodule:: txweb.errors
+.. automodule:: txweb.http_codes
    :members:
    :undoc-members:
    :show-inheritance:
@@ -45,7 +46,6 @@ txweb.web\_views module
    :undoc-members:
    :show-inheritance:
 
-
 Module contents
 ---------------
 
diff --git a/source/api/txweb.util.rst b/source/api/txweb.util.rst
index 1a7015f..3af4797 100644
--- a/source/api/txweb.util.rst
+++ b/source/api/txweb.util.rst
@@ -20,6 +20,14 @@ txweb.util.reloader module
    :undoc-members:
    :show-inheritance:
 
+txweb.util.templating module
+----------------------------
+
+.. automodule:: txweb.util.templating
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 txweb.util.url\_converter module
 --------------------------------
 
@@ -28,7 +36,6 @@ txweb.util.url\_converter module
    :undoc-members:
    :show-inheritance:
 
-
 Module contents
 ---------------
 
diff --git a/source/index.rst b/source/index.rst
index 4c0c15b..639fc14 100644
--- a/source/index.rst
+++ b/source/index.rst
@@ -10,9 +10,11 @@ Welcome to Texas web's documentation!
    :maxdepth: 2
    :caption: Contents:
 
-   api/txweb
-   manual/error_handling
    manual/quick_start
+   manual/error_handling
+   api/txweb
+
+
 
 
 
@@ -23,3 +25,4 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
+
diff --git a/source/manual/quick_start.md b/source/manual/quick_start.md
new file mode 100644
index 0000000..c5623ba
--- /dev/null
+++ b/source/manual/quick_start.md
@@ -0,0 +1,18 @@
+Quick start
+===========
+
+```python
+from twisted.internet import reactor
+from txweb import Application
+
+
+app = Application(__name__)
+
+@app.route("/hello")
+def hello_world(request):
+    return "Hello World!"
+
+listening = app.listenTCP(PORT, interface="0.0.0.0")
+reactor.start()
+
+```
\ No newline at end of file
diff --git a/txweb/application.py b/txweb/application.py
index 801d8be..ebff037 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -1,11 +1,12 @@
 """
     Application is a common interface to Texas's various components
 
-    The Application object logic has been broken apart into two mixin like classes and the main Application class.
+    The Application object logic has been broken apart into mixin like classes and combined into Application class.
 
     1. Routing logic (adding new routes/resources to the URL router) `ApplicationRoutingHelperMixin`
     2. Error handling `ApplicationErrorHandlingMixin`
-    3. General utilities and resources ( website, router, reactor, etc) `Application`
+    3. Websocket support
+    4. General utilities and resources ( website, router, reactor, etc) `Application`
 
 
 """
@@ -52,15 +53,14 @@
 
 log = getLogger(__name__)
 
-if T.TYPE_CHECKING:
 
-    ArbitraryListArg = T.NewType("ArbitraryListArg", T.List[T.Any])
-    ArbitraryKWArguments = T.NewType("ArbitraryKWArguments", T.Optional[T.Dict[str, T.Any]])
+ArbitraryListArg = T.NewType("ArbitraryListArg", T.List[T.Any])
+ArbitraryKWArguments = T.NewType("ArbitraryKWArguments", T.Optional[T.Dict[str, T.Any]])
 
-    WebCallable = T.NewType("WebCallable", T.Callable[[StrRequest, ArbitraryListArg], T.Union[str, bytes]])
-    CallableToResourceDecorator = T.NewType("CallableToResourceDecorator", T.Callable[[WebCallable], WebCallable])
-
-    ErrorHandler = T.NewType("ErrorHandler", T.Callable[[StrRequest, failure.Failure], T.Union[bool, None]])
+WebCallable = T.NewType("WebCallable", T.Callable[[StrRequest, ArbitraryListArg], T.Union[str, bytes]])
+CallableToResourceDecorator = T.NewType("CallableToResourceDecorator", T.Callable[[WebCallable], WebCallable])
+WSEndpoint = T.Callable[[MessageHandler], T.Any]
+ErrorHandler = T.NewType("ErrorHandler", T.Callable[[StrRequest, failure.Failure], T.Union[bool, None]])
 
 
 class ApplicationWebsocketMixin(object):
@@ -256,6 +256,7 @@ def set_postfilter(self, func):
 class ApplicationErrorHandlingMixin(object):
     """
     The Error processing and handling aspect of Application
+
     """
 
 

From e081d17134a5ba088542a6497396d594da080679 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 09:50:40 -0700
Subject: [PATCH 053/185] Yes self.instance is needed

---
 txweb/resources/view_class.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/resources/view_class.py b/txweb/resources/view_class.py
index a7599f6..f9ed924 100644
--- a/txweb/resources/view_class.py
+++ b/txweb/resources/view_class.py
@@ -17,7 +17,7 @@ class ViewClassResource(resource.Resource):
     # noinspection PyMissingConstructor
     def __init__(self, kls_view, instance=None):
         self.kls_view = kls_view
-        self.instance = instance  # TODO is this needed?
+        self.instance = instance
 
     def render(self, request:StrRequest) -> T.Union[bytes, NotDoneYet]:
         """

From 25953842a90718dbeb372b2d494f8932e77b46ce Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 09:51:07 -0700
Subject: [PATCH 054/185] Fix to no longer pass connection/protocol

---
 txweb/tests/test_ws_add.py  | 6 +++---
 txweb/tests/test_wsclass.py | 5 +++--
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/txweb/tests/test_ws_add.py b/txweb/tests/test_ws_add.py
index 5183cca..2a22cae 100644
--- a/txweb/tests/test_ws_add.py
+++ b/txweb/tests/test_ws_add.py
@@ -8,7 +8,7 @@ def test_ws_add_works():
     app = WSApp(__name__) # type: WSApp
 
     @app.ws_add("foo.bar")
-    def provide_foo_bar(connection, message):
+    def provide_foo_bar(message):
         pass
 
     assert "foo.bar" in app.ws_endpoints
@@ -18,11 +18,11 @@ def test_ws_assign_args_flag_works():
     app = WSApp(__name__)
 
     @app.ws_add("alice.bob", assign_args=True)
-    def provide_alice_bob(connection, message, which_one: str = None):
+    def provide_alice_bob(message, which_one: str = None):
         return which_one
 
     message = MessageHandler(dict(args=dict(which_one="Steve!!!")), None)
 
-    result = provide_alice_bob(None, message)
+    result = provide_alice_bob(message)
 
     assert result == "Steve!!!"
\ No newline at end of file
diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_wsclass.py
index 43bbb82..e119675 100644
--- a/txweb/tests/test_wsclass.py
+++ b/txweb/tests/test_wsclass.py
@@ -60,10 +60,11 @@ def __init__(self, app):
             self.app = app
 
         @app.ws_expose(assign_args=True)
-        def test_function(self, connection, message, foo=False, bar=None):
+        def test_function(self, message, foo=False, bar=None):
             return foo, bar
 
         with pytest.raises(TypeError, match="^ws_expose convention expects.*"):
+
             @app.ws_expose(assign_args=True)
             def test_bad_convention(self, protocol, incoming):
                 pass
@@ -74,7 +75,7 @@ def test_bad_convention(self, protocol, incoming):
 
     message = MessageHandler({"args":dict(foo=True, bar="Hello World!")}, None)
 
-    foo, bar = instance.test_function(None, message)
+    foo, bar = instance.test_function(message)
 
     assert foo == True
     assert bar == "Hello World!"

From c42309686d512d4ea54893912d366a57168c8f96 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 12:17:33 -0700
Subject: [PATCH 055/185] Allow override ws_class endpoint name

---
 txweb/application.py        | 47 ++++++++++++++++++++++++++++---------
 txweb/tests/test_wsclass.py | 16 +++++++++++++
 2 files changed, 52 insertions(+), 11 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index ebff037..3c472bd 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -96,21 +96,46 @@ def processor(func: WSEndpoint) -> WSEndpoint:
     def ws_sharelib(self, route_str="/lib"):
         self.add_staticdir2(route_str, WS_STATIC_LIB)
 
-    def ws_class(self, kls):
-        kls_name = kls.__name__.lower()
-        if kls_name in self.ws_instances:
-            raise ValueError(f"Websocket name: {kls_name} class is already registered!")
+    def ws_class(self, kls=None, name: str = None):
 
-        self.ws_instances[kls_name] = kls(self)
+        if name is None:
+            kls_name = kls.__name__.lower()
+            if kls_name in self.ws_instances:
+                raise ValueError(f"Websocket name: {kls_name} class is already registered!")
 
-        methods = [member
-                   for member in inspect.getmembers(self.ws_instances[kls_name])
-                   if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
+            if kls_name in self.ws_instances:
+                raise ValueError(f"ws_class already has {kls_name}")
 
-        for name, method in methods:
-            self.ws_endpoints[f"{kls_name}.{name.lower()}"] = method
+            self.ws_instances[kls_name] = kls(self)
+
+            methods = [member
+                       for member in inspect.getmembers(self.ws_instances[kls_name])
+                       if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
+
+            for name, method in methods:
+                self.ws_endpoints[f"{kls_name}.{name.lower()}"] = method
+
+            return kls
+
+        else:
+
+            def processor(kls):
+
+                kls_name = name
+
+                self.ws_instances[kls_name] = kls(self)
+
+                methods = [member
+                           for member in inspect.getmembers(self.ws_instances[kls_name])
+                           if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
+
+                for method_name, method in methods:
+                    self.ws_endpoints[f"{kls_name}.{method_name.lower()}"] = method
+
+                return kls
+
+            return processor
 
-        return kls
 
     @staticmethod
     def websocket_class_arguments_decorator(func):
diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_wsclass.py
index e119675..79c18a8 100644
--- a/txweb/tests/test_wsclass.py
+++ b/txweb/tests/test_wsclass.py
@@ -82,3 +82,19 @@ def test_bad_convention(self, protocol, incoming):
 
     assert hasattr(instance.test_function, app.WS_EXPOSED_FUNC) is True
 
+
+def test_name_override():
+
+    app = WSApp(__name__)
+
+    @app.ws_class(name="Bar")
+    class Foo:
+
+        def __init__(self, app):
+            self.app = app
+
+        @app.ws_expose
+        def method1(self, message):
+            pass
+
+    assert "bar.method1" in app.ws_endpoints
\ No newline at end of file

From 76131295a8b8412cc7d8daeb0cfdbd05ee632282 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 12:48:57 -0700
Subject: [PATCH 056/185] Switch from old style to use super init

---
 txweb/lib/at_wsprotocol.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 86a085e..8cf2e09 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -22,7 +22,8 @@ class AtWSProtocol(WebSocketServerProtocol):
     def __init__(self, *args, **kwargs):
         self.pending_responses = {}
 
-        WebSocketServerProtocol.__init__(self, *args, **kwargs)
+        super(AtWSProtocol, self).__init__(*args, **kwargs)
+        # WebSocketServerProtocol.__init__(self, *args, **kwargs)
 
         self.identity = None
         self.on_disconnect = Deferred()

From 0c5f7387c7a8754c8a34149f088f3f38f7547a62 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 12:49:21 -0700
Subject: [PATCH 057/185] Fix so that name override is not lower cased which
 might be unexpected

---
 txweb/tests/test_wsclass.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_wsclass.py
index 79c18a8..0b22f6c 100644
--- a/txweb/tests/test_wsclass.py
+++ b/txweb/tests/test_wsclass.py
@@ -97,4 +97,4 @@ def __init__(self, app):
         def method1(self, message):
             pass
 
-    assert "bar.method1" in app.ws_endpoints
\ No newline at end of file
+    assert "Bar.method1" in app.ws_endpoints
\ No newline at end of file

From 8200efce59e01953f43130fb7d337a95fcb02ee3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 12:49:28 -0700
Subject: [PATCH 058/185] Create test_ws_at_wsprotocol.py

---
 txweb/tests/test_ws_at_wsprotocol.py | 51 ++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)
 create mode 100644 txweb/tests/test_ws_at_wsprotocol.py

diff --git a/txweb/tests/test_ws_at_wsprotocol.py b/txweb/tests/test_ws_at_wsprotocol.py
new file mode 100644
index 0000000..439521d
--- /dev/null
+++ b/txweb/tests/test_ws_at_wsprotocol.py
@@ -0,0 +1,51 @@
+from unittest.mock import MagicMock
+from dataclasses import dataclass
+
+from txweb.lib.at_wsprotocol import AtWSProtocol
+
+@dataclass(frozen=True)
+class CapturedMessage:
+    payload: str
+    isBinary: bool
+    fragmentSize:int
+    sync:bool
+    doNotCompress:bool
+
+class MockFactory:
+    def __init__(self, endpoints):
+        self.endpoints = endpoints
+
+class TrackingProtocol(AtWSProtocol):
+
+        def __init__(self, *args, **kwargs):
+            super(TrackingProtocol, self).__init__(*args, **kwargs)
+            self.messages = []
+
+
+        def sendMessage(self,
+                    payload,
+                    isBinary=False,
+                    fragmentSize=None,
+                    sync=False,
+                    doNotCompress=False):
+            self.messages.append(CapturedMessage(payload, isBinary, fragmentSize, sync, doNotCompress))
+
+def mock_protocol(endpoints):
+    factory = MockFactory(endpoints)
+    protocol = TrackingProtocol()
+    protocol.factory = factory
+    return protocol, factory
+
+
+
+def test_imports():
+    pass # catch syntax errors and cyclical imports
+
+
+def test_instantiates():
+    mock_func = MagicMock()
+    protocol, factory = mock_protocol({"foo": mock_func})
+
+
+
+

From d705e621181ec791ab213230eb2cf1cd39099ee9 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:38:41 -0700
Subject: [PATCH 059/185] This is extraneous as Application is usually global
 to the user app

---
 txweb/lib/at_wsprotocol.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 8cf2e09..2f04027 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -30,9 +30,9 @@ def __init__(self, *args, **kwargs):
 
         self._raw_message = {}
 
-    @property
-    def application(self):
-        return self.factory.get_application()
+    # @property
+    # def application(self):
+    #     return self.factory.get_application()
 
     def getCookie(self, cookie_name, default=None):
         raw_cookies = self.http_headers.get('cookie', "")

From 983edd4bb550bb261f6b2e5e35bf93a1c84f88c5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:39:03 -0700
Subject: [PATCH 060/185] Testing to AtWsProtocol

---
 txweb/tests/test_ws_at_wsprotocol.py | 46 ++++++++++++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/txweb/tests/test_ws_at_wsprotocol.py b/txweb/tests/test_ws_at_wsprotocol.py
index 439521d..9d3cd83 100644
--- a/txweb/tests/test_ws_at_wsprotocol.py
+++ b/txweb/tests/test_ws_at_wsprotocol.py
@@ -15,11 +15,15 @@ class MockFactory:
     def __init__(self, endpoints):
         self.endpoints = endpoints
 
+    def get_endpoint(self, endpoint):
+        return self.endpoints[endpoint]
+
 class TrackingProtocol(AtWSProtocol):
 
         def __init__(self, *args, **kwargs):
             super(TrackingProtocol, self).__init__(*args, **kwargs)
             self.messages = []
+            self.http_headers = {}
 
 
         def sendMessage(self,
@@ -46,6 +50,48 @@ def test_instantiates():
     mock_func = MagicMock()
     protocol, factory = mock_protocol({"foo": mock_func})
 
+def test_getCookie():
+    protocol, factory = mock_protocol({})
+
+    protocol.http_headers['cookie'] = "foo=bar; blah=123; thing=creature"
+
+
+    assert protocol.getCookie("foo") == "bar"
+    assert protocol.getCookie("blah") == "123"
+    assert protocol.getCookie("thing") == "creature"
+
+    dud = protocol.getCookie("doesn't exist", default=None)
+    assert dud is None
+
+
+def test_open_and_closed_deferred_works():
+
+    protocol, factoryt = mock_protocol({})
+
+    assert protocol.identity is None
+
+    protocol.onConnect(None)
+
+    actual_id = protocol.identity
+    track_call = MagicMock()
+
+    protocol.on_disconnect.addCallback(track_call)
+
+    protocol.onClose(True, 200, None)
+
+    track_call.assert_called_once_with(actual_id)
+
+
+def test_onMessage():
+    import json
+
+    mock_func = MagicMock()
+    protocol, factory = mock_protocol(dict(endpoint1=mock_func))
+
+    payload = dict(endpoint="endpoint1", type="tell")
+    dumped = json.dumps(payload).encode("utf-8")
 
+    protocol.onMessage(dumped, False)
 
+    mock_func.assert_called_once()
 

From b7c44e110652e1bdf0ea47875ab4155d62da4978 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:40:03 -0700
Subject: [PATCH 061/185] TODO, fix up the logic for returning values

---
 txweb/lib/at_wsprotocol.py | 30 +++++++++++++++---------------
 1 file changed, 15 insertions(+), 15 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 2f04027..22549de 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -122,19 +122,19 @@ def onMessage(self, payload, isBinary):
             self.my_log.error("Got message without an endpoint: {raw!r}", raw=message.raw_message)
             raise Exception("Got message without a endpoint")
 
-        if result == NOT_DONE_YET:
-            return
-
-        elif result is None:
-            return
-
-        elif isinstance(result, Deferred):
-            return result
-
-        elif call_data['type'] == "ask":
-            self.respondAsDict(message, result=result)
-        else:
-            response = json.dumps(result)
-            ## echo back message verbatim
-            self.sendMessage(response.encode("utf-8"), isBinary)
+        # if result == NOT_DONE_YET:
+        #     return
+        #
+        # elif result is None:
+        #     return
+        #
+        # elif isinstance(result, Deferred):
+        #     return result
+        #
+        # elif "type" in message and message['type'] == "ask":
+        #     self.respondAsDict(message, result=result)
+        # else:
+        #     response = json.dumps(result)
+        #     ## echo back message verbatim
+        #     self.sendMessage(response.encode("utf-8"), isBinary)
 

From 5b91e1adffd67c6697984f268cbf43796d36c8be Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:47:35 -0700
Subject: [PATCH 062/185] cleaned up return logic a bit more

---
 txweb/lib/at_wsprotocol.py | 27 ++++++++++++---------------
 1 file changed, 12 insertions(+), 15 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 22549de..6d2f04a 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -122,19 +122,16 @@ def onMessage(self, payload, isBinary):
             self.my_log.error("Got message without an endpoint: {raw!r}", raw=message.raw_message)
             raise Exception("Got message without a endpoint")
 
-        # if result == NOT_DONE_YET:
-        #     return
-        #
-        # elif result is None:
-        #     return
-        #
-        # elif isinstance(result, Deferred):
-        #     return result
-        #
-        # elif "type" in message and message['type'] == "ask":
-        #     self.respondAsDict(message, result=result)
-        # else:
-        #     response = json.dumps(result)
-        #     ## echo back message verbatim
-        #     self.sendMessage(response.encode("utf-8"), isBinary)
+            # raise Exception("Got message without a endpoint")
+
+        if result in [NOT_DONE_YET, None]:
+            return
+        elif isinstance(result, Deferred):
+            return
+        elif "type" in message and message['type'] == "ask":
+            self.respondAsDict(message, result=result)
+        else:
+            warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
+            pass
+
 

From bfb6f92f8424a8b447b48149ac12518124c9ec23 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:47:57 -0700
Subject: [PATCH 063/185] Don't raise an exception when missing endpoint, just
 log it and move on

---
 txweb/lib/at_wsprotocol.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 6d2f04a..b4c6a2a 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -120,8 +120,6 @@ def onMessage(self, payload, isBinary):
                 result = endpoint_func(message)
         else:
             self.my_log.error("Got message without an endpoint: {raw!r}", raw=message.raw_message)
-            raise Exception("Got message without a endpoint")
-
             # raise Exception("Got message without a endpoint")
 
         if result in [NOT_DONE_YET, None]:

From caa1cfeeb5d2581323553bc6888f2186c3024bbb Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:48:04 -0700
Subject: [PATCH 064/185] Update at_wsprotocol.py

---
 txweb/lib/at_wsprotocol.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index b4c6a2a..e98ad12 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -4,6 +4,7 @@
     import json
 
 from uuid import uuid4
+import warnings
 
 
 from twisted.web.server import NOT_DONE_YET

From c69fa97f184b8bdaded7e099994def662db927e5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 13:48:22 -0700
Subject: [PATCH 065/185] Squash bad errors

---
 txweb/tests/test_ws_at_wsprotocol.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/txweb/tests/test_ws_at_wsprotocol.py b/txweb/tests/test_ws_at_wsprotocol.py
index 9d3cd83..2399c1e 100644
--- a/txweb/tests/test_ws_at_wsprotocol.py
+++ b/txweb/tests/test_ws_at_wsprotocol.py
@@ -86,6 +86,7 @@ def test_onMessage():
     import json
 
     mock_func = MagicMock()
+    mock_func.return_value = None
     protocol, factory = mock_protocol(dict(endpoint1=mock_func))
 
     payload = dict(endpoint="endpoint1", type="tell")

From 6a4395c8dae208d48349591e615693e94554e9d6 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:25:36 -0700
Subject: [PATCH 066/185] Perhaps a bit convoluted but added a test for asking
 the remote side for a result

---
 txweb/tests/test_ws_at_wsprotocol.py | 39 ++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/txweb/tests/test_ws_at_wsprotocol.py b/txweb/tests/test_ws_at_wsprotocol.py
index 2399c1e..2044166 100644
--- a/txweb/tests/test_ws_at_wsprotocol.py
+++ b/txweb/tests/test_ws_at_wsprotocol.py
@@ -1,6 +1,8 @@
 from unittest.mock import MagicMock
 from dataclasses import dataclass
 
+from twisted.internet.defer import inlineCallbacks
+
 from txweb.lib.at_wsprotocol import AtWSProtocol
 
 @dataclass(frozen=True)
@@ -40,6 +42,11 @@ def mock_protocol(endpoints):
     protocol.factory = factory
     return protocol, factory
 
+def mock_message(**kwargs):
+    from json import dumps
+    raw = dumps(kwargs)
+    return raw.encode("utf-8")
+
 
 
 def test_imports():
@@ -96,3 +103,35 @@ def test_onMessage():
 
     mock_func.assert_called_once()
 
+
+def test_deferred_ask():
+
+    import json
+
+    @inlineCallbacks
+    def asking_func(message):
+
+        result = yield message.ask("remote.add", first=1, second=2)
+        message.tell("result_was", logic=result)
+        assert result == 4
+
+    protocol, factory = mock_protocol({"test_endpoint": asking_func})
+    protocol.onMessage(mock_message(type="call", endpoint="test_endpoint"), False)
+    captured = protocol.messages[0] # type: CapturedMessage
+    ask_msg = json.loads(captured.payload)
+
+    assert ask_msg['type'] == "ask"
+    assert len(protocol.deferred_asks) == 1
+
+    caller_id = ask_msg['caller_id']
+
+    protocol.onMessage(mock_message(type="response", caller_id=caller_id, result=4), False)
+
+    assert len(protocol.messages) == 2
+
+    latest = protocol.messages[-1] # type: CapturedMessage
+    tell_msg = json.loads(latest.payload)
+
+    assert tell_msg['type'] == "tell"
+    assert tell_msg['args']['logic'] == 4
+

From 82fd0d997175b5d706dcf830d3cbdf81e42ec259 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:26:00 -0700
Subject: [PATCH 067/185] removed as superflous

---
 txweb/lib/at_wsprotocol.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index e98ad12..bc938fb 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -91,12 +91,13 @@ def ask(self, endpoint, **values):
 
     call = ask
 
-    def sendTell(self, data, result):
-        self.sendDict(caller_id=data['caller_id'], type="tell", result=result)
 
 
-    def sendTellAsDict(self, data, **result):
-        self.sendTell(data, result)
+    # def sendTell(self, data, result):
+    #     self.sendDict(caller_id=data['caller_id'], type="tell", result=result)
+    #
+    # def sendTellAsDict(self, data, **result):
+    #     self.sendTell(data, result)
 
     def respondAsDict(self, data, **result):
         self.sendDict(caller_id=data["caller_id"], end_point=data["endpoint"], type="response", result=result)

From 76fd0c32d55f314fe953cf6fff500a542655b426 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:26:33 -0700
Subject: [PATCH 068/185] Refactored onMessage to handle both ask, tell, and
 response incoming messages

---
 txweb/lib/at_wsprotocol.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index bc938fb..ef267fa 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -111,7 +111,17 @@ def onMessage(self, payload, isBinary):
         message = MessageHandler(json.loads(payload), self)
         result = None
 
-        if "endpoint" in message:
+        if message.get("type") == "response":
+            caller_id = message.get("caller_id", None)
+
+            if caller_id is not None and caller_id in self.deferred_asks:
+                d = self.deferred_asks[caller_id] # type: Deferred
+                d.callback(message.get("result"))
+                del self.deferred_asks[caller_id]
+            else:
+                warnings.warn(f"Response to ask {caller_id} arrived but wasn't found in deferred_asks")
+
+        elif "endpoint" in message:
             endpoint_func = self.factory.get_endpoint(message['endpoint'])
             self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
 
@@ -121,7 +131,7 @@ def onMessage(self, payload, isBinary):
             else:
                 result = endpoint_func(message)
         else:
-            self.my_log.error("Got message without an endpoint: {raw!r}", raw=message.raw_message)
+            self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message)
             # raise Exception("Got message without a endpoint")
 
         if result in [NOT_DONE_YET, None]:

From c9b81f8004a3ec0fa663d90ed4058561f65e82a0 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:27:17 -0700
Subject: [PATCH 069/185] removed as superflous

---
 txweb/lib/at_wsprotocol.py | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index ef267fa..698a027 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -49,18 +49,12 @@ def onConnect(self, request):
         self.identity = uuid4().hex
         self.my_log.debug("Client connecting: {request.peer}", request=request)
 
-    def onOpen(self):
-        self.my_log.debug("WebSocket connection open.")
-
     def onClose(self, wasClean, code, reason):
         self.on_disconnect.addErrback(self.my_log.error)
         self.on_disconnect.callback(self.identity)
         del self.on_disconnect
         self.my_log.debug("WebSocket connection closed: {reason!r}", reason=reason)
 
-    def onClosed(self, *args, **kwargs):
-        self.my_log.debug(*args, **kwargs)
-
     def sendDict(self, **values):
         response = json.dumps(values)
         # Always send synchronously for now

From 26b9ccdcbf313d93f930d1862ddf401afff8620e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:27:29 -0700
Subject: [PATCH 070/185] removed as superflous

---
 txweb/lib/at_wsprotocol.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 698a027..c4cae3b 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -29,7 +29,8 @@ def __init__(self, *args, **kwargs):
         self.identity = None
         self.on_disconnect = Deferred()
 
-        self._raw_message = {}
+
+        # self._raw_message = {}
 
     # @property
     # def application(self):

From 9e85a04f8acc46a47e6f71f9f7a1c5ce88d6f2c9 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:27:49 -0700
Subject: [PATCH 071/185] Refactored ask so that it works

---
 txweb/lib/at_wsprotocol.py | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index c4cae3b..2e1bb0a 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -19,6 +19,11 @@
 class AtWSProtocol(WebSocketServerProtocol):
 
     my_log = getLogger()
+    """
+        To prevent an OOM/out of memory event, limit the number of 
+        pending asks to 100
+    """
+    MAX_ASKS = 100  # Need to make this tunable
 
     def __init__(self, *args, **kwargs):
         self.pending_responses = {}
@@ -28,6 +33,8 @@ def __init__(self, *args, **kwargs):
 
         self.identity = None
         self.on_disconnect = Deferred()
+        # for tracking deferred `ask` calls
+        self.deferred_asks = {}
 
 
         # self._raw_message = {}
@@ -81,10 +88,14 @@ def ask(self, endpoint, **values):
         :param values:
         :return:
         """
-        requestToken = uuid4().hex
-        self.sendDict(endpoint=endpoint, type="ask", token=requestToken, args=values)
-
-    call = ask
+        if len(self.deferred_asks) < self.MAX_ASKS:
+            requestToken = uuid4().hex
+            d = Deferred()
+            self.deferred_asks[requestToken] = d
+            self.sendDict(endpoint=endpoint, type="ask", caller_id=requestToken, args=values)
+            return d
+        else:
+            raise EnvironmentError("Maximum # of pending asks reached")
 
 
 
@@ -97,7 +108,6 @@ def ask(self, endpoint, **values):
     def respondAsDict(self, data, **result):
         self.sendDict(caller_id=data["caller_id"], end_point=data["endpoint"], type="response", result=result)
 
-
     def onMessage(self, payload, isBinary):
         if isBinary:
             return None # I don't know how to deal with binary

From b7ec1cbbd4da72b5cecb39c2f10fdd07d44c5721 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 14:49:58 -0700
Subject: [PATCH 072/185] Toughen up onMessage to handle bad inputs

---
 txweb/lib/at_wsprotocol.py | 17 ++++++++++++++---
 1 file changed, 14 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 2e1bb0a..bed47ef 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -112,8 +112,19 @@ def onMessage(self, payload, isBinary):
         if isBinary:
             return None # I don't know how to deal with binary
 
-        payload = payload.decode("utf-8")
-        message = MessageHandler(json.loads(payload), self)
+        try:
+            payload = payload.decode("utf-8")
+        except UnicodeDecodeError:
+            warnings.warn(f"Failed to decode {payload}")
+            return
+
+        try:
+            raw_message = json.loads(payload)
+        except json.JSONDecodeError:
+            warnings.warn(f"Corrupt/bad payload: {payload}")
+            return
+
+        message = MessageHandler(raw_message, self)
         result = None
 
         if message.get("type") == "response":
@@ -124,7 +135,7 @@ def onMessage(self, payload, isBinary):
                 d.callback(message.get("result"))
                 del self.deferred_asks[caller_id]
             else:
-                warnings.warn(f"Response to ask {caller_id} arrived but wasn't found in deferred_asks")
+                warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
 
         elif "endpoint" in message:
             endpoint_func = self.factory.get_endpoint(message['endpoint'])

From 4d1665fe807021b49e3ea82957371751ec4d6ac7 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 16:02:01 -0700
Subject: [PATCH 073/185] more cleanup

---
 txweb/lib/at_wsprotocol.py | 16 +++-------------
 1 file changed, 3 insertions(+), 13 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index bed47ef..42dfe18 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -68,6 +68,9 @@ def sendDict(self, **values):
         # Always send synchronously for now
         self.sendMessage(response.encode("utf-8"), isBinary=False, sync=True)
 
+    def respond(self, original_message, result):
+        self.sendDict(caller_id=original_message['caller_id'], result=result)
+
     def tell(self, endpoint, **values):
         """
             Tell the client to do something and don't expect a response
@@ -78,8 +81,6 @@ def tell(self, endpoint, **values):
         """
         self.sendDict(endpoint=endpoint, type="tell", args=values)
 
-    callDict = tell
-
     def ask(self, endpoint, **values):
         """
             Ask the client to do something and I should get a response back.
@@ -97,17 +98,6 @@ def ask(self, endpoint, **values):
         else:
             raise EnvironmentError("Maximum # of pending asks reached")
 
-
-
-    # def sendTell(self, data, result):
-    #     self.sendDict(caller_id=data['caller_id'], type="tell", result=result)
-    #
-    # def sendTellAsDict(self, data, **result):
-    #     self.sendTell(data, result)
-
-    def respondAsDict(self, data, **result):
-        self.sendDict(caller_id=data["caller_id"], end_point=data["endpoint"], type="response", result=result)
-
     def onMessage(self, payload, isBinary):
         if isBinary:
             return None # I don't know how to deal with binary

From d8b67143ea7468d5917cf1ce463e2ceac76074fa Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 16:02:21 -0700
Subject: [PATCH 074/185] no coverage, they work or they don't

---
 txweb/lib/message_handler.py | 33 +++++++++++++++------------------
 txweb/lib/routed_factory.py  |  7 ++-----
 2 files changed, 17 insertions(+), 23 deletions(-)

diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 2d20370..33bb399 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -5,38 +5,41 @@
 
 from collections.abc import Mapping
 
-if T.TYPE_CHECKING or False:
+if T.TYPE_CHECKING or False: # pragma: no cover
     # recursion import
     from .at_wsprotocol import AtWSProtocol
 
 
 
 
-class MessageHandler(Mapping):
+class MessageHandler(Mapping): #pragma: no cover
+
+    raw_message: T.Dict[T.str, T.Any]
+    connection: AtWSProtocol
 
     def __init__(self, raw_message:dict, connection:AtWSProtocol):
         self.raw_message = raw_message # type: dict
         self.connection = connection
 
-    def __getitem__(self, item):
+    def __getitem__(self, item):  # pragma: no cover
         return self.raw_message[item]
 
-    def __iter__(self):
+    def __iter__(self):  # pragma: no cover
         return self.raw_message.__iter__()
 
-    def __len__(self):
+    def __len__(self):  # pragma: no cover
         return len(self.raw_message)
 
-    def __contains__(self, item):
+    def __contains__(self, item): # pragma: no cover
         return item in self.raw_message
 
-    def keys(self):
+    def keys(self):  # pragma: no cover
         return self.raw_message1.keys()
 
-    def items(self):
+    def items(self):  # pragma: no cover
         return self.raw_message.items()
 
-    def values(self):
+    def values(self):  # pragma: no cover
         return self.raw_message.values()
 
     def get(self, key, default=None, type=None):
@@ -83,19 +86,13 @@ def args(self, key, default=None, type=None):
 
         return value
 
-    @property
-    def is_asking(self):
-        return "caller_id" in self.raw_message
-
-    def respond(self, **kwargs):
-        self.connection.respondAsDict(self.raw_message, result=kwargs)
-        return NOT_DONE_YET
+    def respond(self, result):
+        return self.connection.respond(self.raw_message, result=result)
 
     def tell(self, endpoint, **kwargs):
-        self.connection.sendDict(endpoint=endpoint, type="tell", args=kwargs)
+        return self.connection.tell(endpoint, **kwargs)
 
     def ask(self, endpoint, **kwargs):
-        #TODO setup a deferred somewhere in here or below in protocol
         return self.connection.ask(endpoint, type="ask", args=kwargs)
 
     def get_session(self, get_key=None):
diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index e970d0a..f0c3a77 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -1,17 +1,14 @@
 from autobahn.twisted.websocket import WebSocketServerFactory
 from .at_wsprotocol import AtWSProtocol
 
-class RoutedWSFactory(WebSocketServerFactory):
+
+class RoutedWSFactory(WebSocketServerFactory):  #pragma: no cover
 
     def __init__(self, url, routes, protocol_cls=AtWSProtocol, application=None):
         WebSocketServerFactory.__init__(self, url)
         self.protocol = protocol_cls
 
-        self._application = application # Necessary so further down in protocol land we can get the session
         self.routes = routes
 
     def get_endpoint(self, name):
         return self.routes.get(name, None)
-
-    def get_application(self):
-        return self._application
\ No newline at end of file

From 7e95050856a431c97de486da06db84b7df508903 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 27 Nov 2020 16:02:28 -0700
Subject: [PATCH 075/185] Update requirements.txt

---
 requirements.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index fc25934..8da2c22 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,4 +18,5 @@ zope.interface==4.4.3
 setuptools~=40.4.3
 txweb~=0.11.2019
 pytest~=3.2.3
-recommonmark~=0.6.0
\ No newline at end of file
+recommonmark~=0.6.0
+autobahn~=20.7.1
\ No newline at end of file

From 8ab747f9e5fa67ece7b45c32a7579dc6d12ff2e2 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:53:43 -0700
Subject: [PATCH 076/185] Better to be explicit than use some convoluted magic

---
 txweb/lib/message_handler.py | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 33bb399..380868d 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -45,14 +45,9 @@ def values(self):  # pragma: no cover
     def get(self, key, default=None, type=None):
 
         try:
-            args = self['args']
-            value = args[key]
-
+            value = self[key]
         except KeyError:
-            try:
-                value = self[key]
-            except KeyError:
-                return default
+            return default
 
         if type:
             try:
@@ -70,7 +65,6 @@ def args(self, key, default=None, type=None):
         except KeyError:
             return default
 
-
         try:
             value = args[key]
         except KeyError:

From b16a32d72a6336caced6e6d3b2a0fac7b8bed599 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:54:15 -0700
Subject: [PATCH 077/185] Quiet applications down for now

---
 txweb/lib/at_wsprotocol.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 42dfe18..3158300 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -129,7 +129,7 @@ def onMessage(self, payload, isBinary):
 
         elif "endpoint" in message:
             endpoint_func = self.factory.get_endpoint(message['endpoint'])
-            self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
+            # self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
 
             if endpoint_func is None:
                 self.my_log.error("Bad endpoint {endpoint}", endpoint=call_data['endpoint'])

From f956e5e6ec70b31998bafccceb4bef53f5cfb9bd Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:54:30 -0700
Subject: [PATCH 078/185] Turns out this is needed

---
 txweb/lib/at_wsprotocol.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 3158300..9c3e8e5 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -39,9 +39,9 @@ def __init__(self, *args, **kwargs):
 
         # self._raw_message = {}
 
-    # @property
-    # def application(self):
-    #     return self.factory.get_application()
+    @property
+    def application(self):
+        return self.factory.get_application()
 
     def getCookie(self, cookie_name, default=None):
         raw_cookies = self.http_headers.get('cookie', "")

From e4440c1aeb8a60278eb327f7078cb7d8c5247133 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:55:01 -0700
Subject: [PATCH 079/185] Fix to explicitly fetch from args and not get as per
 change to MessageHandler

---
 txweb/application.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/application.py b/txweb/application.py
index 3c472bd..f9fb466 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -157,7 +157,7 @@ def argument_decorator(parent, message):
 
 
             for arg_name, arg_default in arg_keys.items():
-                kwargs[arg_name] = message.get(arg_name, arg_default)
+                kwargs[arg_name] = message.args(arg_name, arg_default)
 
             return func(parent, message, **kwargs)
 

From 02dda27b79a90b7913339e1f33e74c7ba643baf5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:55:20 -0700
Subject: [PATCH 080/185] Update routed_factory.py

---
 txweb/lib/routed_factory.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index f0c3a77..b5cabbe 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -12,3 +12,7 @@ def __init__(self, url, routes, protocol_cls=AtWSProtocol, application=None):
 
     def get_endpoint(self, name):
         return self.routes.get(name, None)
+
+
+    def get_application(self):
+        return self._application
\ No newline at end of file

From aac13c3f81235ddcc4f75e499433ac2d4960d68a Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:55:24 -0700
Subject: [PATCH 081/185] Update routed_factory.py

---
 txweb/lib/routed_factory.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index b5cabbe..91de76d 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -9,6 +9,7 @@ def __init__(self, url, routes, protocol_cls=AtWSProtocol, application=None):
         self.protocol = protocol_cls
 
         self.routes = routes
+        self._application = application
 
     def get_endpoint(self, name):
         return self.routes.get(name, None)

From d64147515e57a5b82c1edf4a9badf45e67c1ac1b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 09:55:43 -0700
Subject: [PATCH 082/185] clean out old dead code

---
 txweb/resources/routing.py | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 82a0c45..6b57141 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -148,18 +148,6 @@ def _add_resource_cls(self, route_str, endpoint=None, thing=None, route_kwargs=N
         self._add_resource(route_str, endpoint=endpoint, thing=self._instances[endpoint], route_kwargs=route_kwargs)
 
 
-    # def add_resource(self, route_str, resource_object, endpoint=None, route_kwargs=None):
-    #     route_kwargs = route_kwargs or {}
-    #     endpoint = endpoint if endpoint is not None else get_thing_name(resource_object)
-    #
-    #     new_rule = wz_routing.Rule(route_str, endpoint=endpoint, **route_kwargs)
-    #     if endpoint not in self._endpoints:
-    #         self._endpoints[endpoint] = resource_object
-    #
-    #     self._route_map.add(new_rule)
-    #
-    #     return resource_object
-
     def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None):
         route_kwargs = route_kwargs if route_kwargs is not None else {}
         endpoint = endpoint or get_thing_name(thing)

From 41ca91a99f815a2c09684710f9b01463df2c0ab5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 12:35:40 -0700
Subject: [PATCH 083/185] Forgot to set the `type` field for response messages
 to "response"

---
 txweb/lib/at_wsprotocol.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 9c3e8e5..643a7ca 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -69,7 +69,7 @@ def sendDict(self, **values):
         self.sendMessage(response.encode("utf-8"), isBinary=False, sync=True)
 
     def respond(self, original_message, result):
-        self.sendDict(caller_id=original_message['caller_id'], result=result)
+        self.sendDict(caller_id=original_message['caller_id'], type="response", result=result)
 
     def tell(self, endpoint, **values):
         """

From 201c61017fd5724a9e1a3016a09c2959b6d5bc76 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 12:41:39 -0700
Subject: [PATCH 084/185] Fix up some old tests and added
 test_handle_add_slashes to isolate a problem

---
 txweb/tests/test_resources_routing.py | 31 ++++++++++++++++++++++++---
 1 file changed, 28 insertions(+), 3 deletions(-)

diff --git a/txweb/tests/test_resources_routing.py b/txweb/tests/test_resources_routing.py
index 749d1a2..97d8418 100644
--- a/txweb/tests/test_resources_routing.py
+++ b/txweb/tests/test_resources_routing.py
@@ -1,3 +1,5 @@
+from unittest.mock import MagicMock
+
 from twisted.web.resource import NoResource
 
 from txweb.resources import RoutingResource
@@ -15,7 +17,7 @@
 
 def test_instantiates_without_error():
 
-    class FakeSite():
+    class FakeSite:
         pass
 
     fake_site = FakeSite()
@@ -33,7 +35,10 @@ def handle_foo(request):
 
     dummy_request.request.site = app.site
     dummy_request.channel.site = app.site
-    dummy_request.request.requestReceived(b"HEAD", b"/foo", b"HTTP1/1")
+    dummy_request.request.requestReceived(b"HEAD", b"/foo", b"HTTP/1.1")
+    assert dummy_request.request.code == 405
+    assert dummy_request.request.code_message == b"Method not allowed"
+
 
 
 def test_ensure_blows_up_with_a_bad_add():
@@ -54,7 +59,7 @@ def test_ensure_blowsup_with_a_class_that_has_no_way_to_render():
 
     with pytest.raises(UnrenderableException):
         @app.add("/trash")
-        class BasClass(object):
+        class BaseClass(object):
             pass
 
 def test_ensure_a_classic_like_class_is_routed():
@@ -86,3 +91,23 @@ def test_ensure_resource_is_added():
     debug = 1
 
 
+def test_handle_add_slashes(dummy_request:RequestRetval):
+
+    app = App(__name__)
+
+    mock = MagicMock()
+
+    app.route("/js/")(mock)
+
+    dummy_request.request.site = app.site
+    dummy_request.channel.site = app.site
+    dummy_request.request.requestReceived(b"GET", b"/js", b"HTTP/1.1")
+
+    assert dummy_request.request.code == 308
+    assert dummy_request.request.code_message == b"Permanent Redirect"
+    assert dummy_request.request.responseHeaders.getRawHeaders(b"location") == [b"http://10.0.0.1/js/"]
+    assert mock.call_count == 0
+
+
+
+

From 789fa87c25fb6fa68ce425effa62cd67a3245445 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 12:42:40 -0700
Subject: [PATCH 085/185] Disable/perma-set script_name to "" for now.

---
 txweb/resources/routing.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 6b57141..80ba3cf 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -191,7 +191,7 @@ def _build_map(self, pathEl, request):
         else:
             map_bind_kwargs["server_name"] = request.getRequestHostname()
 
-        map_bind_kwargs["script_name"] = b"/".join(request.prepath) if request.prepath else b"/"
+        map_bind_kwargs["script_name"] = b"/"  # b"/".join(request.prepath) if request.prepath else b"/"
 
         #TODO add strict slash check flag to here or to website.add
         if map_bind_kwargs["script_name"].startswith(b"/") is False:
@@ -219,6 +219,9 @@ def getChildWithDefault(self, pathEl: T.Union[bytes,str], request: StrRequest):
             # TODO refactor to handle HEAD requests when the only valid match support GET
             # - one bad idea is to hack on werkzeug to append the URI matching rule to MethodNotAllowed
             (rule, kwargs) = map.match(return_rule=True)
+        except wz_routing.RequestRedirect as redirect:
+            log.debug(f"Werkzeug threw a redirect")
+            raise HTTP_Errors.HTTP3xx(redirect.code, redirect.new_url, redirect.name)
         except wz_routing.NotFound as exc:
             # TODO remove print
             log.debug(f"Failed to find match for: {request.path!r}")

From f2da17b3c7bdc998200b56afc73eab01e8497146 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 12:43:18 -0700
Subject: [PATCH 086/185] Figured out a clever way to allow await'ing ask calls

---
 txweb/websocket_static_libraries/resilient_socket.js | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
index 8a771d8..0b67695 100644
--- a/txweb/websocket_static_libraries/resilient_socket.js
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -124,6 +124,16 @@ class ResilientSocket {
         return d;
     }
 
+    async a_ask(endpoint, args) {
+        const d = this.ask(endpoint, args);
+
+        return new Promise(function(resolve){
+            d.then(result => {
+                resolve(result);
+            } )
+        });
+    }
+
     call(endpoint, args) {
         /**
          * Tell the server something has happened but don't expect a direct response.

From 20c3031a827bf315f630c6ffaf1f5f2e7dcef95b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 28 Nov 2020 12:43:28 -0700
Subject: [PATCH 087/185] Update README.md

---
 README.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/README.md b/README.md
index 40ada9e..7239966 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@ Major issues
 ============
 Error handling needs to be drastically improved/refactored
 
+TODO - support for SCRIPT_NAME so the app could theoretically run in a sub directory of a
+another application.
 
 Purpose & History
 ======

From 7703553f2121420c785dc8e6fe9d26adfd8b3aeb Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:07:08 -0700
Subject: [PATCH 088/185] Bug fixes

---
 txweb/lib/at_wsprotocol.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 643a7ca..c8729a3 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -132,7 +132,7 @@ def onMessage(self, payload, isBinary):
             # self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
 
             if endpoint_func is None:
-                self.my_log.error("Bad endpoint {endpoint}", endpoint=call_data['endpoint'])
+                self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])
                 return
             else:
                 result = endpoint_func(message)
@@ -144,8 +144,8 @@ def onMessage(self, payload, isBinary):
             return
         elif isinstance(result, Deferred):
             return
-        elif "type" in message and message['type'] == "ask":
-            self.respondAsDict(message, result=result)
+        elif message.get("type", default=None) == "ask":
+            self.respond(message, result=result)
         else:
             warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
             pass

From 2bb52873e0df390165475e8cf6ab2828b612ea62 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:08:17 -0700
Subject: [PATCH 089/185] Working on allowing `asks` calls to time out

---
 txweb/websocket_static_libraries/resilient_socket.js | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
index 0b67695..4b40f77 100644
--- a/txweb/websocket_static_libraries/resilient_socket.js
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -124,13 +124,11 @@ class ResilientSocket {
         return d;
     }
 
-    async a_ask(endpoint, args) {
+    async a_ask(endpoint, args, timeout) {
         const d = this.ask(endpoint, args);
 
         return new Promise(function(resolve){
-            d.then(result => {
-                resolve(result);
-            } )
+            d.then(resolve);
         });
     }
 

From 75866445123c92bff6dd071a62989b27c9d8f446 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:08:43 -0700
Subject: [PATCH 090/185] Fix for a really subtle bug

---
 txweb/application.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/txweb/application.py b/txweb/application.py
index f9fb466..94b97a7 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -85,9 +85,11 @@ def enable_websockets(self, url, route):
     def ws_add(self, name, assign_args=False) -> T.Callable[[WSEndpoint], WSEndpoint]:
 
         def processor(func: WSEndpoint) -> WSEndpoint:
-            self.ws_endpoints[name] = func
+
             if assign_args is True:
                 func = self.websocket_function_arguments_decorator(func)
+
+            self.ws_endpoints[name] = func
             return func
 
         return processor

From 44c315e82707bc1dd41353f95dce4c0a16069963 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:10:19 -0700
Subject: [PATCH 091/185] Hardwired enforcement of calling conventions

---
 txweb/application.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/txweb/application.py b/txweb/application.py
index 94b97a7..2fee94c 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -169,6 +169,7 @@ def argument_decorator(parent, message):
     def websocket_function_arguments_decorator(func):
         params = inspect.signature(func).parameters
         arg_keys = {}
+        positional_count = 0
         for name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
@@ -176,6 +177,12 @@ def websocket_function_arguments_decorator(func):
                         f"Cannot use assign_args when using keyword arguments `message`: {param.name}")
                 arg_keys[name] = param.default
 
+            else:
+                positional_count += 1
+
+        if positional_count != 1:
+            raise ValueError("ws_add(... asign_args=True) - arguments need to be keyword arguments and not positional.")
+
         if "message" not in params:
             raise TypeError("ws_add convention expects endpoint(message)")
 

From 05ffb28b072fcb132a0fbdc810e556b85e7c0ddf Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:31:47 -0700
Subject: [PATCH 092/185] New auto magical assign_args type casting magic...
 did I mention magic?

---
 txweb/tests/test_ws_add.py | 47 +++++++++++++++++++++++++++++++++++++-
 1 file changed, 46 insertions(+), 1 deletion(-)

diff --git a/txweb/tests/test_ws_add.py b/txweb/tests/test_ws_add.py
index 2a22cae..4bbbcce 100644
--- a/txweb/tests/test_ws_add.py
+++ b/txweb/tests/test_ws_add.py
@@ -1,4 +1,9 @@
+from __future__ import annotations
+
+from unittest.mock import MagicMock
+
 import pytest
+
 from txweb.lib.message_handler import MessageHandler
 from txweb import Application as WSApp
 
@@ -25,4 +30,44 @@ def provide_alice_bob(message, which_one: str = None):
 
     result = provide_alice_bob(message)
 
-    assert result == "Steve!!!"
\ No newline at end of file
+    assert result == "Steve!!!"
+
+
+def test_ws_enforces_naming_conventions():
+
+    app = WSApp(__name__)
+
+    with pytest.raises(TypeError):
+        @app.ws_add("bob", assign_args=True)
+        def bad_endpoint(steve):
+            pass
+
+def test_ws_enforces_assign_args_is_kosher():
+
+    app = WSApp(__name__)
+
+    with pytest.raises(ValueError):
+        @app.ws_add("steve", assign_args=True)
+        def missing_kwargs(message, alice, bob):
+            pass
+
+def ToyConverter(arg):
+    return int(arg)
+
+def test_bad_ideas_in_code__ws_add_type_casting_via_annotations():
+
+    app = WSApp(__name__)
+
+
+
+    message = MessageHandler({"args": {"ich": "weis", "flag":"1", "number":"123", "other_number": "456"}}, None)
+
+    @app.ws_add("foo", assign_args=True)
+    def bar(message, ich: str = None, flag: bool = False, number: ToyConverter = None, other_number: int = None):
+        return ich, flag, number, other_number,
+
+    actual = bar(message)
+    assert actual[0] == "weis"
+    assert actual[1] == True
+    assert actual[2] == 123
+    assert actual[3] == 456
\ No newline at end of file

From 2a9406e78f11031b7f7c44b5032f3c540b6428ca Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:32:09 -0700
Subject: [PATCH 093/185] typo and duplication fix

---
 txweb/application.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 2fee94c..d166f53 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -103,10 +103,8 @@ def ws_class(self, kls=None, name: str = None):
         if name is None:
             kls_name = kls.__name__.lower()
             if kls_name in self.ws_instances:
-                raise ValueError(f"Websocket name: {kls_name} class is already registered!")
+                raise ValueError(f"Websocket ws_class: a class with {kls_name} is already registered!  Use ws_class(name=NewName) to override.")
 
-            if kls_name in self.ws_instances:
-                raise ValueError(f"ws_class already has {kls_name}")
 
             self.ws_instances[kls_name] = kls(self)
 

From 4654dbcd8416a4633cb336bc621358e55893f9fb Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 10:33:30 -0700
Subject: [PATCH 094/185] implemented magic annotation converter logic

---
 txweb/application.py | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index d166f53..180a21e 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -144,7 +144,7 @@ def websocket_class_arguments_decorator(func):
         for param_name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
-                    raise TypeError(f"Cannot use assign_args when using keyword argument `message`: {param.name}")
+                    raise TypeError(f"assign_args error: argument `message` cannot be a keyword argument: {param.name}")
                 arg_keys[param_name] = param.default
 
         if "message" not in params:
@@ -167,7 +167,12 @@ def argument_decorator(parent, message):
     def websocket_function_arguments_decorator(func):
         params = inspect.signature(func).parameters
         arg_keys = {}
+        converter_keys= {}
         positional_count = 0
+
+        def eval_type(st):
+            return st if not isinstance(st, str) else eval(st, vars(sys.modules[func.__module__]))
+
         for name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
@@ -175,6 +180,9 @@ def websocket_function_arguments_decorator(func):
                         f"Cannot use assign_args when using keyword arguments `message`: {param.name}")
                 arg_keys[name] = param.default
 
+                if param.annotation is not inspect.Parameter.empty:
+                    converter_keys[name] = eval_type(param.annotation)
+
             else:
                 positional_count += 1
 
@@ -182,7 +190,7 @@ def websocket_function_arguments_decorator(func):
             raise ValueError("ws_add(... asign_args=True) - arguments need to be keyword arguments and not positional.")
 
         if "message" not in params:
-            raise TypeError("ws_add convention expects endpoint(message)")
+            raise TypeError("ws_add convention expects first positional argument be called message.")
 
         @functools.wraps(func)
         def argument_decorator(message: MessageHandler):
@@ -190,6 +198,16 @@ def argument_decorator(message: MessageHandler):
 
             for name, default in arg_keys.items():
                 kwargs[name] = message.args(name, default=default) #TODO also use annotation for type-casting
+                if name in converter_keys:
+                    try:
+                        kwargs[name] = converter_keys[name](kwargs[name])
+                    except (TypeError, ValueError,):
+                        log.error(
+                            "assign_args failed to use {converter!r} on {value!r}",
+                            converter=converter_keys[name],
+                            value=kwargs[name])
+
+                        kwargs[name] = default
 
             return func(message, **kwargs)
 

From 8cb4bda61978c5f270c5529bcad09585f29bf63e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 11:09:27 -0700
Subject: [PATCH 095/185] more tests for assign_args magic

---
 txweb/application.py        | 64 ++++++++++++++++++-------------------
 txweb/tests/test_ws_add.py  | 34 ++++++++++----------
 txweb/tests/test_wsclass.py | 44 ++++++++++++++++++++++++-
 3 files changed, 91 insertions(+), 51 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 180a21e..1ad98c6 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -100,11 +100,12 @@ def ws_sharelib(self, route_str="/lib"):
 
     def ws_class(self, kls=None, name: str = None):
 
-        if name is None:
-            kls_name = kls.__name__.lower()
-            if kls_name in self.ws_instances:
-                raise ValueError(f"Websocket ws_class: a class with {kls_name} is already registered!  Use ws_class(name=NewName) to override.")
 
+        def processor(kls, name = None):
+            kls_name = name if name is not None else kls.__name__.lower()
+            if kls_name in self.ws_instances:
+                raise ValueError(
+                    f"Websocket ws_class: a class with {kls_name} is already registered!  Use ws_class(name=NewName) to override.")
 
             self.ws_instances[kls_name] = kls(self)
 
@@ -112,40 +113,39 @@ def ws_class(self, kls=None, name: str = None):
                        for member in inspect.getmembers(self.ws_instances[kls_name])
                        if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
 
-            for name, method in methods:
-                self.ws_endpoints[f"{kls_name}.{name.lower()}"] = method
+            for method_name, method in methods:
+                # Always use assign_args with ws_class's
+                self.ws_endpoints[f"{kls_name}.{method_name.lower()}"] = self.websocket_class_arguments_decorator(method)
 
             return kls
 
-        else:
-
-            def processor(kls):
-
-                kls_name = name
-
-                self.ws_instances[kls_name] = kls(self)
 
-                methods = [member
-                           for member in inspect.getmembers(self.ws_instances[kls_name])
-                           if inspect.ismethod(member[1]) and hasattr(member[1], self.WS_EXPOSED_FUNC)]
-
-                for method_name, method in methods:
-                    self.ws_endpoints[f"{kls_name}.{method_name.lower()}"] = method
+        if kls is None:
+            return functools.partial(processor, name=name)
+        else:
+            return processor(kls, name)
 
-                return kls
 
-            return processor
 
 
     @staticmethod
     def websocket_class_arguments_decorator(func):
         params = inspect.signature(func).parameters
         arg_keys = {}
+        converter_keys = {}
+        positional_count = 0
+
+        def eval_type(st):
+            return st if not isinstance(st, str) else eval(st, vars(sys.modules[func.__module__]))
+
         for param_name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
                     raise TypeError(f"assign_args error: argument `message` cannot be a keyword argument: {param.name}")
-                arg_keys[param_name] = param.default
+                arg_keys[param.name] = param.default
+
+                if param.annotation is not inspect.Parameter.empty:
+                    converter_keys[param.name] = eval_type(param.annotation)
 
         if "message" not in params:
             raise TypeError("ws_expose convention expects (self, message, **kwargs)")
@@ -155,9 +155,16 @@ def websocket_class_arguments_decorator(func):
         def argument_decorator(parent, message):
             kwargs = {}
 
-
             for arg_name, arg_default in arg_keys.items():
-                kwargs[arg_name] = message.args(arg_name, arg_default)
+                raw_argument = message.args(arg_name, arg_default)
+                converter = converter_keys.get(arg_name, None)
+                if converter:
+                    try:
+                        kwargs[arg_name] = converter(raw_argument)
+                    except (TypeError, ValueError,):
+                        kwargs[arg_name] = arg_default
+                else:
+                    kwargs[arg_name] = raw_argument
 
             return func(parent, message, **kwargs)
 
@@ -168,7 +175,6 @@ def websocket_function_arguments_decorator(func):
         params = inspect.signature(func).parameters
         arg_keys = {}
         converter_keys= {}
-        positional_count = 0
 
         def eval_type(st):
             return st if not isinstance(st, str) else eval(st, vars(sys.modules[func.__module__]))
@@ -183,14 +189,6 @@ def eval_type(st):
                 if param.annotation is not inspect.Parameter.empty:
                     converter_keys[name] = eval_type(param.annotation)
 
-            else:
-                positional_count += 1
-
-        if positional_count != 1:
-            raise ValueError("ws_add(... asign_args=True) - arguments need to be keyword arguments and not positional.")
-
-        if "message" not in params:
-            raise TypeError("ws_add convention expects first positional argument be called message.")
 
         @functools.wraps(func)
         def argument_decorator(message: MessageHandler):
diff --git a/txweb/tests/test_ws_add.py b/txweb/tests/test_ws_add.py
index 4bbbcce..e87082d 100644
--- a/txweb/tests/test_ws_add.py
+++ b/txweb/tests/test_ws_add.py
@@ -33,23 +33,23 @@ def provide_alice_bob(message, which_one: str = None):
     assert result == "Steve!!!"
 
 
-def test_ws_enforces_naming_conventions():
-
-    app = WSApp(__name__)
-
-    with pytest.raises(TypeError):
-        @app.ws_add("bob", assign_args=True)
-        def bad_endpoint(steve):
-            pass
-
-def test_ws_enforces_assign_args_is_kosher():
-
-    app = WSApp(__name__)
-
-    with pytest.raises(ValueError):
-        @app.ws_add("steve", assign_args=True)
-        def missing_kwargs(message, alice, bob):
-            pass
+# def test_ws_enforces_naming_conventions():
+#
+#     app = WSApp(__name__)
+#
+#     with pytest.raises(TypeError):
+#         @app.ws_add("bob", assign_args=True)
+#         def bad_endpoint(steve):
+#             pass
+
+# def test_ws_enforces_assign_args_is_kosher():
+#
+#     app = WSApp(__name__)
+#
+#     with pytest.raises(ValueError):
+#         @app.ws_add("steve", assign_args=True)
+#         def missing_kwargs(message, alice, bob):
+#             pass
 
 def ToyConverter(arg):
     return int(arg)
diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_wsclass.py
index 0b22f6c..794e4ac 100644
--- a/txweb/tests/test_wsclass.py
+++ b/txweb/tests/test_wsclass.py
@@ -3,6 +3,10 @@
 from txweb import Application as WSApp
 from txweb.lib.message_handler import MessageHandler
 
+
+def quick_message_mock(**args):
+    return MessageHandler(dict(args=args), None)
+
 def test_concept():
 
     app = WSApp(__name__)
@@ -97,4 +101,42 @@ def __init__(self, app):
         def method1(self, message):
             pass
 
-    assert "Bar.method1" in app.ws_endpoints
\ No newline at end of file
+    assert "Bar.method1" in app.ws_endpoints
+
+
+def test_assign_args():
+
+    app = WSApp(__name__)
+
+    @app.ws_class()
+    class Foo:
+
+        def __init__(self, app):
+            self.app = app
+
+        @app.ws_expose(assign_args=True)
+        def method1(self, message, numbah: int = None):
+            return numbah
+
+    message = quick_message_mock(numbah = "123")
+    method1 = app.ws_instances['foo'].method1
+    assert "foo.method1" in app.ws_endpoints
+    assert method1(message) == 123
+
+def test_assign_args_ignores_missing_args():
+    app = WSApp(__name__)
+
+    @app.ws_class()
+    class Foo:
+
+        def __init__(self, app):
+            self.app = app
+
+        @app.ws_expose(assign_args=True)
+        def method1(self, message, bar: int = None):
+            return bar
+
+    message = quick_message_mock(numbah="123")
+    method1 = app.ws_instances['foo'].method1
+    assert "foo.method1" in app.ws_endpoints
+    assert method1(message) is None

From 1e302398480aab617e2a292818d1bde70e738cec Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 15:09:19 -0700
Subject: [PATCH 096/185] renamed to match convention of other test files

---
 txweb/tests/{test_wsclass.py => test_ws_class.py} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename txweb/tests/{test_wsclass.py => test_ws_class.py} (100%)

diff --git a/txweb/tests/test_wsclass.py b/txweb/tests/test_ws_class.py
similarity index 100%
rename from txweb/tests/test_wsclass.py
rename to txweb/tests/test_ws_class.py

From 4a9b3a0ae2100229812c2ec5035019596f77c6ac Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 15:09:59 -0700
Subject: [PATCH 097/185] use promise based return for ask method

---
 txweb/websocket_static_libraries/resilient_socket.js | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
index 4b40f77..f72e849 100644
--- a/txweb/websocket_static_libraries/resilient_socket.js
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -108,7 +108,7 @@ class ResilientSocket {
         }
     }
 
-    ask(endpoint, args) {
+    async ask(endpoint, args) {
         /**
          * Expects a response using Deferred for callbacks
          */
@@ -121,17 +121,15 @@ class ResilientSocket {
         let msg = {type:"ask", endpoint:endpoint, args: args, caller_id: this.callerID};
         this._sendRaw(JSON.stringify(msg));
 
-        return d;
-    }
-
-    async a_ask(endpoint, args, timeout) {
-        const d = this.ask(endpoint, args);
-
         return new Promise(function(resolve){
             d.then(resolve);
         });
     }
 
+    async a_ask(endpoint, args, timeout) {
+        return this.ask(endpoint, args);
+    }
+
     call(endpoint, args) {
         /**
          * Tell the server something has happened but don't expect a direct response.

From c87063542ed26225be6efabbb35ab9694d1e12dd Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 15:11:06 -0700
Subject: [PATCH 098/185] match convention

---
 txweb/tests/{test_ws_add.py => test_application_ws_add.py}     | 0
 txweb/tests/{test_ws_class.py => test_application_ws_class.py} | 0
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename txweb/tests/{test_ws_add.py => test_application_ws_add.py} (100%)
 rename txweb/tests/{test_ws_class.py => test_application_ws_class.py} (100%)

diff --git a/txweb/tests/test_ws_add.py b/txweb/tests/test_application_ws_add.py
similarity index 100%
rename from txweb/tests/test_ws_add.py
rename to txweb/tests/test_application_ws_add.py
diff --git a/txweb/tests/test_ws_class.py b/txweb/tests/test_application_ws_class.py
similarity index 100%
rename from txweb/tests/test_ws_class.py
rename to txweb/tests/test_application_ws_class.py

From a2fcb3c9ea5c3548c44f8fe4d77cdcc269dfe0ad Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 15:15:28 -0700
Subject: [PATCH 099/185] test case to isolate a bug with ws_expose

---
 txweb/tests/test_application_ws_class.py | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/txweb/tests/test_application_ws_class.py b/txweb/tests/test_application_ws_class.py
index 794e4ac..b20d357 100644
--- a/txweb/tests/test_application_ws_class.py
+++ b/txweb/tests/test_application_ws_class.py
@@ -140,3 +140,24 @@ def method1(self, message, bar: int = None):
     method1 = app.ws_instances['foo'].method1
     assert "foo.method1" in app.ws_endpoints
     assert method1(message) is None
+
+
+def test_isolate_bug_with_name_argument():
+
+    app = WSApp(__name__)
+
+    @app.ws_class(name="ec")
+    class EntityControl:
+
+        def __init__(self, app):
+            self.app = app
+
+        @app.ws_expose
+        def start(self, message):
+            pass
+
+
+    start_endpoint = app.ws_endpoints['ec.start']
+    message = MessageHandler({}, None)
+
+    start_endpoint(message)
\ No newline at end of file

From 98f5f547ffdbacedae04e50f6f4b37c83d185885 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sun, 29 Nov 2020 15:31:47 -0700
Subject: [PATCH 100/185] isolated the bug

ws_class is overreaching by setting assign_args automatically to a class's methods.
---
 txweb/application.py                     | 6 +++---
 txweb/tests/test_application_ws_class.py | 5 +++++
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 1ad98c6..81502a8 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -115,7 +115,7 @@ def processor(kls, name = None):
 
             for method_name, method in methods:
                 # Always use assign_args with ws_class's
-                self.ws_endpoints[f"{kls_name}.{method_name.lower()}"] = self.websocket_class_arguments_decorator(method)
+                self.ws_endpoints[f"{kls_name}.{method_name.lower()}"] = method
 
             return kls
 
@@ -152,7 +152,7 @@ def eval_type(st):
 
 
         @functools.wraps(func)
-        def argument_decorator(parent, message):
+        def method_argument_decorator(parent, message):
             kwargs = {}
 
             for arg_name, arg_default in arg_keys.items():
@@ -168,7 +168,7 @@ def argument_decorator(parent, message):
 
             return func(parent, message, **kwargs)
 
-        return argument_decorator
+        return method_argument_decorator
 
     @staticmethod
     def websocket_function_arguments_decorator(func):
diff --git a/txweb/tests/test_application_ws_class.py b/txweb/tests/test_application_ws_class.py
index b20d357..c89dc9c 100644
--- a/txweb/tests/test_application_ws_class.py
+++ b/txweb/tests/test_application_ws_class.py
@@ -144,6 +144,8 @@ def method1(self, message, bar: int = None):
 
 def test_isolate_bug_with_name_argument():
 
+    import inspect
+
     app = WSApp(__name__)
 
     @app.ws_class(name="ec")
@@ -158,6 +160,9 @@ def start(self, message):
 
 
     start_endpoint = app.ws_endpoints['ec.start']
+    endpoint_sig = inspect.signature(start_endpoint)
+
+
     message = MessageHandler({}, None)
 
     start_endpoint(message)
\ No newline at end of file

From d688895a938aff45c264d82f414d6b25a8c82ee1 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Tue, 1 Dec 2020 21:04:48 -0700
Subject: [PATCH 101/185] Returned logging of endpoints

---
 txweb/lib/at_wsprotocol.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index c8729a3..74b6ac2 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -128,8 +128,9 @@ def onMessage(self, payload, isBinary):
                 warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
 
         elif "endpoint" in message:
+
             endpoint_func = self.factory.get_endpoint(message['endpoint'])
-            # self.my_log.debug("Processing {endpoint_func!r}", endpoint_func=endpoint_func)
+            self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
 
             if endpoint_func is None:
                 self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])

From 1ce36a730f7d0b051dd5f44f9c4503d2103e547b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Tue, 1 Dec 2020 21:05:31 -0700
Subject: [PATCH 102/185] TODO - need to clean up but dealing with a Firefox
 issue related to WebSocket.readyState != CONNECTED when onOpen event is
 fired.

---
 .../resilient_socket.js                       | 70 ++++++++++++++++---
 1 file changed, 62 insertions(+), 8 deletions(-)

diff --git a/txweb/websocket_static_libraries/resilient_socket.js b/txweb/websocket_static_libraries/resilient_socket.js
index f72e849..d0424c1 100644
--- a/txweb/websocket_static_libraries/resilient_socket.js
+++ b/txweb/websocket_static_libraries/resilient_socket.js
@@ -2,6 +2,14 @@
 export {ResilientSocket};
 import {Deferred} from "./deferred.js";
 
+
+async function sleep(time) {
+    return new Promise(resolve => {
+        window.setTimeout(resolve, time);
+    })
+}
+
+
 class ResilientSocket {
     constructor(conString, {debug=false} = {}) {
         this.conString = conString;
@@ -18,9 +26,11 @@ class ResilientSocket {
 
 
         this.endpoints = {};
+
+        this.connect();
     }
 
-    connect() {
+    async connect() {
 
         if(this.retries > this.retryLimit) {
             console.error("Number of connect retries reached limit!");
@@ -28,15 +38,50 @@ class ResilientSocket {
             // connection retries failing.
         }
 
-        console.debug("connecting resilient");
+        if(this.debug){
+            console.debug("connecting resilient");
+        }
+
         this.socket = new WebSocket(this.conString);
         this.socket.addEventListener("message", this.receiveMsg.bind(this));
         this.socket.addEventListener("close", this.onclose.bind(this));
         this.socket.addEventListener("open", this.onopen.bind(this));
         this.socket.addEventListener("error", this.onerror.bind(this));
+
+        return new Promise((resolve, reject)=>{
+            const self = this;
+            async function on_open(evt){
+                if(self.debug){
+                    console.debug("Connection opened!", self.socket.readyState);
+                }
+
+                if(self.socket.readyState != self.socket.OPEN) {
+                    await sleep(1000);
+                }
+                self.retries = 0;
+                self.closed = false;
+                self.socket.removeEventListener("open", this);
+                self.socket.removeEventListener("error", on_error);
+                resolve(evt);
+            }
+
+            function on_error(evt) {
+                self.closed = true;
+                self.socket.removeEventListener("error", this);
+                reject(evt);
+            }
+
+            this.socket.addEventListener("open", on_open, {once: true});
+            this.socket.addEventListener("error", on_error, {once: true});
+
+
+
+        });
+
     }
 
     onopen() {
+        console.log("rs.onopen called");
         this.closed = false;
         this.retries = 0;
     }
@@ -55,15 +100,23 @@ class ResilientSocket {
         this.socket.addEventListener("open", handler, {once:true});
     }
 
-    _sendRaw(msg) {
+    async _sendRaw(msg) {
         //Check if disconnected
-        if(this.closed == true) {
-            this.connect();
-            this.first_open( x=>this.socket.send(msg));
+
+        if(this.socket.readyState != this.socket.OPEN) {
+            const result = await this.connect();
+            console.log("this.connect", result, this.socket.readyState);
+            this.socket.send(msg);
         } else {
             this.socket.send(msg);
         }
 
+        if(this.debug){
+            console.debug("Sent", msg);
+        }
+
+
+
     }
 
     async sendMsg(endpoint, args) {
@@ -78,14 +131,15 @@ class ResilientSocket {
 
     async receiveMsg(msg) {
         if(this.debug) {
-            console.debug(msg);
+            console.debug("received", msg);
         }
 
         let data = JSON.parse(msg.data);
         if (data['type'] == "response") {
             let d = this.pending[data['caller_id']];
-            d.fire(data.result);
             delete this.pending[data['caller_id']];
+            d.fire(data.result);
+
         }
         else if(data['type'] == "tell") {
             if (this.endpoints[data['endpoint']] != undefined) {

From 613beb24740e0883c707b2fba99bdb752f42ae0b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 13:53:18 -0700
Subject: [PATCH 103/185] Bring in Failure class for use other where

---
 txweb/lib/errors/__init__.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/txweb/lib/errors/__init__.py b/txweb/lib/errors/__init__.py
index e69de29..cd133fc 100644
--- a/txweb/lib/errors/__init__.py
+++ b/txweb/lib/errors/__init__.py
@@ -0,0 +1,2 @@
+
+from twisted.python.failure import Failure
\ No newline at end of file

From 7c0a577cb7f40a5ede546be244a6bc00b5777c7f Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 13:53:42 -0700
Subject: [PATCH 104/185] Concrete error handling tests

---
 .../test_errors_handlers_DebugHandler.py      | 50 +++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/txweb/tests/test_errors_handlers_DebugHandler.py b/txweb/tests/test_errors_handlers_DebugHandler.py
index d59a8b1..23a7c4d 100644
--- a/txweb/tests/test_errors_handlers_DebugHandler.py
+++ b/txweb/tests/test_errors_handlers_DebugHandler.py
@@ -1,5 +1,7 @@
 import pytest
 
+from unittest.mock import MagicMock
+
 from txweb import Application
 from txweb.lib.errors.handler import DebugHandler
 from txweb.lib.str_request import StrRequest
@@ -55,5 +57,53 @@ def handle_foo(request):
     assert dummy_request.request.code_message == b"Internal server error"
 
 
+def test_error_custom_errorhandler(dummy_request: RequestRetval):
+
+    app = Application(__name__)
+    dummy_request.site = app.site
+    fake_handler = MagicMock(return_val = True)
+
+    class TestError(Exception):
+        pass
+
+    @app.add("/throws")
+    def throws_error(request):
+        raise TestError()
+
+    app.handle_error(TestError)(fake_handler)
+
+    #  unlike the default handler, this swallows the exception
+    dummy_request.request.requestReceived(b"GET", b"/throws", b"HTTP/1.1")
+
+    # assert we caught this correctly
+    fake_handler.assert_called()
+
+
+
+def test_error_custom_errorhandler_prevents_duplicates(dummy_request: RequestRetval):
+
+    app = Application(__name__)
+    dummy_request.site = app.site
+    fake_handler = MagicMock(return_val = True)
+
+    class TestError(Exception):
+        pass
+
+    @app.add("/throws")
+    def throws_error(request):
+        raise TestError()
+
+    app.handle_error(TestError)(fake_handler)
+    with pytest.raises(ValueError):
+        app.handle_error(TestError)(fake_handler)
+
+
+
+
+
+
+
+
+
 
 

From 05028ed1dae1fc0ea6ebdc4eae4ba46de44ccd58 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:24:46 -0700
Subject: [PATCH 105/185] Renamed for clarity

---
 txweb/application.py             |  4 ++--
 txweb/tests/test_view_classes.py | 12 ++++++------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 81502a8..171c2cc 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -294,10 +294,10 @@ def expose(self, route_str, **kwargs):
         # TODO make route_str optional somehow
         return expose_method(route_str, **kwargs)
 
-    def set_prefilter(self, func):
+    def set_view_prefilter(self, func):
         return set_prefilter(func)
 
-    def set_postfilter(self, func):
+    def set_view_postfilter(self, func):
         return set_postfilter(func)
 
 
diff --git a/txweb/tests/test_view_classes.py b/txweb/tests/test_view_classes.py
index 601c962..ebb890b 100644
--- a/txweb/tests/test_view_classes.py
+++ b/txweb/tests/test_view_classes.py
@@ -129,11 +129,11 @@ def test_find_pre_and_post_filters():
 
     class Test(object):
 
-        @app.set_prefilter
+        @app.set_view_prefilter
         def blank_prefilter(self, request, method_name):
             return None
 
-        @app.set_postfilter
+        @app.set_view_postfilter
         def blank_postfilter(self, request, method_name, response):
             return response
 
@@ -172,7 +172,7 @@ class TestPrefilter(object):
         def stub(self, request):
             return ""
 
-        @app.set_prefilter
+        @app.set_view_prefilter
         def my_prefilter(self, request, method_name):
             raise WasCalled()
 
@@ -199,7 +199,7 @@ class TestPostfilter(object):
         def stub(self, request):
             return ""
 
-        @app.set_postfilter
+        @app.set_view_postfilter
         def my_postfilter(self, request, method_name, response):
             raise WasCalled()
 
@@ -219,7 +219,7 @@ class TestPostfilter(object):
         def stub(self, request):
             return ""
 
-        @app.set_postfilter
+        @app.set_view_postfilter
         def my_postfilter(self, request, method_name, response):
             assert method_name == self.stub.__qualname__
             assert method_name.endswith("stub")
@@ -238,7 +238,7 @@ class GlobalTestView(object):
     def exposed(self, request):
         return ""
 
-    @global_test_app.set_prefilter
+    @global_test_app.set_view_prefilter
     def my_prefilter(self, request, method_name):
         cls, method = method_name.split(".",1)
         assert cls == "GlobalTestView"

From e44a07713bcb4d9bd02406926d7c6a5881e3140b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:25:12 -0700
Subject: [PATCH 106/185] Removed as unused and kinda dumb

---
 txweb/application.py | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 171c2cc..9643bb5 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -423,7 +423,6 @@ def __init__(self,
 
         ApplicationWebsocketMixin.__init__(self)
         ApplicationRoutingHelperMixin.__init__(self)
-        # _ApplicationTemplateSupportMixin.__init__(self)
         ApplicationErrorHandlingMixin.__init__(self, enable_debug=enable_debug)
 
 
@@ -449,15 +448,6 @@ def partial(*args, **kwargs):
         return partial
 
 
-    def __call__(self, namespace:str):
-        """
-            TODO sanity check if this is a good idea
-
-            Allows the application namespace property to be overwritten during run time.
-
-        """
-        self.name = namespace
-
     @property
     def router(self) -> RoutingResource:
         return self._router

From 8dfc15267442a8de422b67e493ae8b40044004c2 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:25:31 -0700
Subject: [PATCH 107/185] no need to cover this

---
 txweb/lib/errors/handler.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 102afb9..285e9ac 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -47,7 +47,7 @@ def __call__(self, request: StrRequest, reason: Failure) -> None:
             request.ensureFinished()
             raise exc
 
-    def process(self, request: StrRequest, reason: Failure) -> None:
+    def process(self, request: StrRequest, reason: Failure) -> None:  # pragma: no cover
         raise NotImplementedError("Attempting to use Base error handler")
 
 

From 933f41e1650bacb690956ef9cee62ca109ddae5d Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:26:21 -0700
Subject: [PATCH 108/185] simplify, don't tell the user shit has gone horrible
 wrong, just cut the connection and tell the developer/admin

---
 txweb/lib/errors/handler.py | 11 +++--------
 1 file changed, 3 insertions(+), 8 deletions(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 285e9ac..53b691b 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -67,14 +67,9 @@ def process(self, request: StrRequest, reason: Failure) -> bool:
         if request.startedWriting not in [0, False]:
             # There is nothing we can do, the out going stream is already tainted
             # noinspection PyBroadException
-            try:
-                request.write("!!!Internal Server Error!!!")
-            except Exception:
-                log.error("Failed writing error message to an active stream")
-            finally:
-                request.ensureFinished()
-
-            return True
+            log.error("Failed writing error message to an active stream")
+            request.ensureFinished()
+            reason.raiseException()
 
         elif isinstance(http_codes.HTTPCode, reason.type) or issubclass(reason.type, http_codes.HTTPCode):
 

From 584e9ddd598a976c8f59f9e1b94c82d3364d88df Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:26:38 -0700
Subject: [PATCH 109/185] No need to cover code that isn't being used

---
 txweb/lib/errors/handler.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 53b691b..b0f6e6c 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -94,8 +94,10 @@ def process(self, request: StrRequest, reason: Failure) -> bool:
         return True
 
 
-class DebugHandler(BaseHandler):
+class DebugHandler(BaseHandler):  #  pragma: no cover
     """
+        TODO - To finish
+
         Mimic flask's exception rendering system with some minor caveats.
 
         Errors are held in resident/session memory if possible.

From d399dc99ed29c48012a360355d312aae7eb5c942 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:26:56 -0700
Subject: [PATCH 110/185] not needed

---
 txweb/tests/test_errors_handlers_DebugHandler.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/txweb/tests/test_errors_handlers_DebugHandler.py b/txweb/tests/test_errors_handlers_DebugHandler.py
index 23a7c4d..a3aca6e 100644
--- a/txweb/tests/test_errors_handlers_DebugHandler.py
+++ b/txweb/tests/test_errors_handlers_DebugHandler.py
@@ -89,10 +89,6 @@ def test_error_custom_errorhandler_prevents_duplicates(dummy_request: RequestRet
     class TestError(Exception):
         pass
 
-    @app.add("/throws")
-    def throws_error(request):
-        raise TestError()
-
     app.handle_error(TestError)(fake_handler)
     with pytest.raises(ValueError):
         app.handle_error(TestError)(fake_handler)

From 2ecc0c545a65d104a56814978aa35d84e0af784d Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:27:05 -0700
Subject: [PATCH 111/185] Update test_resources_directory.py

---
 txweb/tests/test_resources_directory.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/txweb/tests/test_resources_directory.py b/txweb/tests/test_resources_directory.py
index 06816d2..11ed11f 100644
--- a/txweb/tests/test_resources_directory.py
+++ b/txweb/tests/test_resources_directory.py
@@ -55,8 +55,8 @@ def test_hybrid_leaf_and_branch(static_dir):
 
 def test_full_suite_with_routed_site_to_added_directory(static_dir):
 
-    test_app = Application()
-    test_app(__name__)
+    test_app = Application(__name__)
+
 
 
     test_app.add_staticdir("/some/convoluted_path", static_dir)

From 9bef9a5135da7fe0fbc6eb01bdc66ec56d2be3a3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:27:14 -0700
Subject: [PATCH 112/185] Update test_view_classes.py

---
 txweb/tests/test_view_classes.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/txweb/tests/test_view_classes.py b/txweb/tests/test_view_classes.py
index ebb890b..2898f34 100644
--- a/txweb/tests/test_view_classes.py
+++ b/txweb/tests/test_view_classes.py
@@ -158,10 +158,11 @@ def blank_postfilter(self, request, method_name, response):
 
 def test_setting_prefilter(dummy_request:RequestRetval):
 
-    from unittest.mock import sentinel
+    from unittest.mock import sentinel, MagicMock
 
     app = Application(__name__)
 
+
     class WasCalled(Exception):
         pass
 

From 4cb61637bfb0cfd69fd34299407a0e194a307c64 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:27:36 -0700
Subject: [PATCH 113/185] removed dead code

---
 txweb/web_views.py | 35 +----------------------------------
 1 file changed, 1 insertion(+), 34 deletions(-)

diff --git a/txweb/web_views.py b/txweb/web_views.py
index fb4e9b7..40aacfd 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -98,7 +98,7 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
 
         _RoutingSiteConnectors.__init__(self, routing_resource, requestFactory=request_factory)
 
-        self._errorHandler = siteErrorHandler or WebSite.defaultSiteErrorHandler
+        self._errorHandler = siteErrorHandler
         self._lastError = None
 
         self._before_request_render = None
@@ -120,39 +120,6 @@ def addErrorHandler(self, func: ErrorHandler):
         self._errorHandler = func
         return func
 
-    @staticmethod
-    def defaultSiteErrorHandler(request: StrRequest, reason: failure.Failure):
-
-        site = request.site
-
-        template_path = (LIBRARY_TEMPLATE_PATH / "debug_error.html")  # type: pathlib.Path
-        assert template_path.exists and template_path.is_file(), f"Unable to find library template: {template_path}"
-        template = jinja2.Template(template_path.read_text())
-
-        traceback = reason.getTraceback() if site.displayTracebacks else None
-
-        if issubclass(reason.type, HTTP_Errors.HTTP3xx):
-            code = reason.value.code
-            message = "HTTP Redirect"
-            buffer = template.render(code=code, message=message, traceback=traceback)
-            request.setHeader("Location", reason.value.redirect)
-        elif traceback is not None and isinstance(reason.type, HTTP_Errors.HTTP405):
-            traceback = None
-        elif issubclass(reason.type, HTTP_Errors.HTTPCode):
-            code = reason.value.code
-            message = reason.value.message if traceback else "Error"
-            buffer = template.render(code=code, message=message, traceback=traceback)
-        else:
-            code = 500
-            message = "Processing aborted"
-            buffer = template.render(code=code, message=message, traceback=traceback)
-            reason.printDetailedTraceback()
-
-        request.setHeader(b'content-type', b"text/html")
-        request.setHeader(b'content-length', str(len(buffer)).encode("utf-8"))
-        request.setResponseCode(code)
-        request.write(buffer)
-        request.finish()
 
     def call_before_request_render(self, func):
         self._before_request_render = func

From 914c754d124363e55ddc4fd41f3ce586ad5ff300 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:41:52 -0700
Subject: [PATCH 114/185] I am not adding a handler, I am setting it.

---
 txweb/application.py | 2 +-
 txweb/web_views.py   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 9643bb5..c0d363c 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -320,7 +320,7 @@ def __init__(self, enable_debug: bool =False,  **kwargs):
         self.error_handlers = dict(default=self.default_handler_cls(self))
         self.enable_debug = enable_debug
 
-        self.site.addErrorHandler(self.processingFailed)
+        self.site.setErrorHandler(self.processingFailed)
 
 
     def handle_error(self, error_type: T.Union[HTTPCode, int, Exception, str], write_over=False) -> T.Callable:
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 40aacfd..78ab432 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -116,7 +116,7 @@ def processingFailed(self, request: StrRequest, reason: failure.Failure):
             self.my_log.error(f"Exception occurred while handling {reason!r}")
             raise
 
-    def addErrorHandler(self, func: ErrorHandler):
+    def setErrorHandler(self, func: ErrorHandler):
         self._errorHandler = func
         return func
 

From 32dc54435a72d776f245ffef820c5fae80cdeb06 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Thu, 3 Dec 2020 20:42:06 -0700
Subject: [PATCH 115/185] dead/duplicated code

---
 txweb/web_views.py | 46 +++++++++++++++++++++++-----------------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/txweb/web_views.py b/txweb/web_views.py
index 78ab432..3f0e2ee 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -101,8 +101,8 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
         self._errorHandler = siteErrorHandler
         self._lastError = None
 
-        self._before_request_render = None
-        self._after_request_render = None
+        # self._before_request_render = None
+        # self._after_request_render = None
 
     def processingFailed(self, request: StrRequest, reason: failure.Failure):
 
@@ -121,27 +121,27 @@ def setErrorHandler(self, func: ErrorHandler):
         return func
 
 
-    def call_before_request_render(self, func):
-        self._before_request_render = func
-        return func
-
-    def after_resource_fetch(self, func):
-        self._after_request_render = func
-        return func
-
-    def getResourceFor(self, request: StrRequest):
-        """
-        This is probably the least convoluted way to manipulate the
-        current http request.
-        """
-
-        if self._before_request_render is not None:
-            request.add_before_render(self._before_request_render)
-
-        if self._after_request_render is not None:
-            request.add_after_render(self._after_request_render)
-
-        return server.Site.getResourceFor(self, request)
+    # def call_before_request_render(self, func):
+    #     self._before_request_render = func
+    #     return func
+    #
+    # def after_resource_fetch(self, func):
+    #     self._after_request_render = func
+    #     return func
+    #
+    # def getResourceFor(self, request: StrRequest):
+    #     """
+    #     This is probably the least convoluted way to manipulate the
+    #     current http request.
+    #     """
+    #
+    #     if self._before_request_render is not None:
+    #         request.add_before_render(self._before_request_render)
+    #
+    #     if self._after_request_render is not None:
+    #         request.add_after_render(self._after_request_render)
+    #
+    #     return server.Site.getResourceFor(self, request)
 
 
 

From b119e2acbb21c2d2dfbb2e02b9bdf097bcb912b5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 07:49:42 -0700
Subject: [PATCH 116/185] Comment out and remove references to Directory/Simple
 file resource classes

---
 txweb/application.py                      |  18 +-
 txweb/resources/__init__.py               |   6 +-
 txweb/resources/directory.py              | 204 +++++++++---------
 txweb/resources/routing.py                |   6 +-
 txweb/resources/simple_file.py            | 188 ++++++++--------
 txweb/tests/test_resources_directory.py   | 252 +++++++++++-----------
 txweb/tests/test_resources_simple_file.py | 150 ++++++-------
 txweb/web_views.py                        |  28 +--
 8 files changed, 424 insertions(+), 428 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index c0d363c..7030d7a 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -25,6 +25,7 @@
 from twisted.python.compat import intToBytes
 from twisted.web.server import NOT_DONE_YET
 from twisted.web.static import File
+from twisted.web.static import File as StaticFile
 from twisted.python import failure
 
 try:
@@ -41,7 +42,8 @@
 
 # Application
 from .log import getLogger
-from .resources import RoutingResource, SimpleFile, Directory
+from .resources import RoutingResource
+# from .resources import SimpleFile, Directory
 from .lib import StrRequest, expose_method, set_prefilter, set_postfilter
 from .web_views import WebSite
 from .http_codes import HTTPCode
@@ -262,21 +264,11 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Si
         :param default_type: What content type should a file be served as
         :return: twisted.web.static.File
         """
-        from twisted.web.static import File as StaticFile
+
         assert Path(filePath).exists()
         file_resource = StaticFile(filePath)
         return self.router.add(route_str)(file_resource)
 
-    def add_staticdir(self, route_str: str, dirPath: T.Union[str, Path], recurse = False) -> Directory:
-
-        if route_str.endswith("/") is False:
-            route_str += "/"
-
-        directory_resource = Directory(dirPath, recurse)
-
-        self.router.add_directory(route_str, directory_resource)
-
-        return directory_resource
 
     def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path], recurse = False) -> File:
 
@@ -289,6 +281,8 @@ def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path], recurse =
 
         return directory_resource
 
+    add_staticdir = add_staticdir2
+
 
     def expose(self, route_str, **kwargs):
         # TODO make route_str optional somehow
diff --git a/txweb/resources/__init__.py b/txweb/resources/__init__.py
index badf661..6983168 100644
--- a/txweb/resources/__init__.py
+++ b/txweb/resources/__init__.py
@@ -7,9 +7,9 @@
 """
 from .view_class import ViewClassResource
 from .view_function import ViewFunctionResource
-from .simple_file import SimpleFile
+# from .simple_file import SimpleFile
 from .routing import RoutingResource
-from .directory import Directory
+# from .directory import Directory
 
-__ALL__ = ["ViewClassResource", "ViewFunctionResource", "SimpleFile", "Directory", "RoutingResource"]
+__ALL__ = ["ViewClassResource", "ViewFunctionResource", "RoutingResource"]
 
diff --git a/txweb/resources/directory.py b/txweb/resources/directory.py
index 3be990e..a1116b4 100644
--- a/txweb/resources/directory.py
+++ b/txweb/resources/directory.py
@@ -1,102 +1,102 @@
-"""
-    Clean slate reimplementation of the Directory resource.
-
-
-"""
-from txweb.lib.str_request import StrRequest
-from txweb.resources import SimpleFile
-from txweb.http_codes import HTTP404, HTTP405
-
-from twisted.web.resource import Resource
-
-from pathlib import Path
-import typing as T
-
-
-class Directory(Resource):
-    """
-        TODO - This still needs a lot of work
-
-        Non-recursive directory listing resource.  To clarify, this only serves the files in a given filepath and nothing
-        in child directories nor their contents.
-
-        #Goals
-
-        * allow hooking into the directory listing response so that it can be overloaded by user land code.
-        * Instead of trying to take the user supplied input and then scrubbing it for ".." or similar security breaking
-            injects, it uses a prebuilt (Path.iterdir) allowed list of files and compares that to the path info.
-
-
-
-    """
-
-    def __init__(self, path:T.Union[str, Path], recurse:bool=False):
-        """
-
-        :param path: str/Path an ABSOLUTE filepath to a directory to be exposed to a http client
-        :param recurse: bool should Directory exposed subdirectories
-        """
-        Resource.__init__(self)
-        self.path = Path(path) #  type: Path
-        self.isLeaf = False
-        self.recurse = False
-
-        #dynamic helpers
-
-        self._render_GET = self.render_GET
-
-    def handleGet(self, func):
-        self._render_GET = func
-
-        return func
-
-
-    def show_files(self, func):
-        self._render_GET = func
-        return func
-
-    def allowedFiles(self) -> T.List[str]:
-        """
-            Currently this mechanism is part of the basic security for Directory resource to ensure
-            only the files in a given directory can be served and or listed.
-
-
-        Returns:
-            list: an explicit list of what files can be served from this Directory resource
-        """
-        return [file for file in self.path.glob("*") if file.exists() and file.is_file()]
-
-
-
-    def getChild(self, path:bytes, request: StrRequest):
-
-        path = path.decode("utf-8")
-        if path.lower() in ["/", "", "index", "index.html"]:
-            return self
-
-        if path in [f.name for f in self.allowedFiles()]:
-            return SimpleFile(self.path / path, defaultType="text/blah")
-
-
-        raise HTTP404()
-
-
-    def render(self, request):
-        if request.method == b"GET":
-            return self._render_GET(self, request, self.allowedFiles())
-
-        elif request.method == b"HEAD":
-            # TODO fix this up
-            self._render_GET(self, request, self.allowedFiles())
-            return b""
-        else:
-            raise HTTP405()
-
-    @staticmethod
-    def render_GET(parent, request, files):
-        raise NotImplementedError("TODO")
-
-
-    def __repr__(self):
-        return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
-
+# """
+#     Clean slate reimplementation of the Directory resource.
+#
+#
+# """
+# from txweb.lib.str_request import StrRequest
+# from txweb.resources import SimpleFile
+# from txweb.http_codes import HTTP404, HTTP405
+#
+# from twisted.web.resource import Resource
+#
+# from pathlib import Path
+# import typing as T
+#
+#
+# class Directory(Resource):
+#     """
+#         TODO - This still needs a lot of work
+#
+#         Non-recursive directory listing resource.  To clarify, this only serves the files in a given filepath and nothing
+#         in child directories nor their contents.
+#
+#         #Goals
+#
+#         * allow hooking into the directory listing response so that it can be overloaded by user land code.
+#         * Instead of trying to take the user supplied input and then scrubbing it for ".." or similar security breaking
+#             injects, it uses a prebuilt (Path.iterdir) allowed list of files and compares that to the path info.
+#
+#
+#
+#     """
+#
+#     def __init__(self, path:T.Union[str, Path], recurse:bool=False):
+#         """
+#
+#         :param path: str/Path an ABSOLUTE filepath to a directory to be exposed to a http client
+#         :param recurse: bool should Directory exposed subdirectories
+#         """
+#         Resource.__init__(self)
+#         self.path = Path(path) #  type: Path
+#         self.isLeaf = False
+#         self.recurse = False
+#
+#         #dynamic helpers
+#
+#         self._render_GET = self.render_GET
+#
+#     def handleGet(self, func):
+#         self._render_GET = func
+#
+#         return func
+#
+#
+#     def show_files(self, func):
+#         self._render_GET = func
+#         return func
+#
+#     def allowedFiles(self) -> T.List[str]:
+#         """
+#             Currently this mechanism is part of the basic security for Directory resource to ensure
+#             only the files in a given directory can be served and or listed.
+#
+#
+#         Returns:
+#             list: an explicit list of what files can be served from this Directory resource
+#         """
+#         return [file for file in self.path.glob("*") if file.exists() and file.is_file()]
+#
+#
+#
+#     def getChild(self, path:bytes, request: StrRequest):
+#
+#         path = path.decode("utf-8")
+#         if path.lower() in ["/", "", "index", "index.html"]:
+#             return self
+#
+#         if path in [f.name for f in self.allowedFiles()]:
+#             return SimpleFile(self.path / path, defaultType="text/blah")
+#
+#
+#         raise HTTP404()
+#
+#
+#     def render(self, request):
+#         if request.method == b"GET":
+#             return self._render_GET(self, request, self.allowedFiles())
+#
+#         elif request.method == b"HEAD":
+#             # TODO fix this up
+#             self._render_GET(self, request, self.allowedFiles())
+#             return b""
+#         else:
+#             raise HTTP405()
+#
+#     @staticmethod
+#     def render_GET(parent, request, files):
+#         raise NotImplementedError("TODO")
+#
+#
+#     def __repr__(self):
+#         return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
+#
diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 80ba3cf..3546332 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -1,3 +1,5 @@
+from twisted.web.static import File
+
 from txweb.http_codes import UnrenderableException
 from txweb.util.url_converter import DirectoryPath
 from txweb.util.basic import get_thing_name
@@ -5,7 +7,7 @@
 
 from .view_function import ViewFunctionResource
 from .view_class import ViewClassResource
-from .directory import Directory
+# from .directory import Directory
 
 from ..lib import view_class_assembler as vca
 from txweb import http_codes as HTTP_Errors
@@ -157,7 +159,7 @@ def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None)
 
         self._route_map.add(new_rule)
 
-    def add_directory(self, route_str: str, directory_resource: Directory) -> Directory:
+    def add_directory(self, route_str: str, directory_resource: File) -> File:
 
         endpoint = repr(directory_resource)
         if endpoint not in self._endpoints:
diff --git a/txweb/resources/simple_file.py b/txweb/resources/simple_file.py
index 74c0a9f..30ab229 100644
--- a/txweb/resources/simple_file.py
+++ b/txweb/resources/simple_file.py
@@ -1,94 +1,94 @@
-import errno
-from pathlib import Path
-import typing as T
-
-from twisted.web.static import File, getTypeAndEncoding
-from twisted.web.server import NOT_DONE_YET
-from twisted.web import http
-from twisted.python import log
-
-from txweb.lib.str_request import StrRequest
-
-
-class SimpleFile(File):
-    """
-    Duplicates tx.web.static.File but splits it apart
-        from serving dual purposes of being a Resource Branch and a leaf.
-
-    Purpose: Serve only a specific file
-
-    Research:
-    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
-    https://tools.ietf.org/html/rfc7233#section-4.2
-
-    TODO There is a transient bug I am running into where the last few bytes
-    TODO - of the file is being truncated between server and client.
-    TODO - I feel like this might be part of a memory leak I am still trying to track down.
-
-    """
-    isLeaf = True # type: T.Union[bool, int]
-
-    def __init__(self, path:T.Union[str, Path], defaultType = "text/plain"):
-
-        if False in [Path(path).is_file(), Path(path).exists()]:
-            raise ValueError(f"{path} is not a file and or does not exist")
-
-        File.__init__(self, str(path), defaultType=defaultType)
-
-        self.isLeaf = True
-
-    def render(self, request:StrRequest):
-        method = request.method.lower().decode("utf-8")
-
-        if method == "get":
-            return self.render_GET(request)
-        elif method == "head":
-            return self.render_HEAD(request)
-        else:
-            log.err(request.method, "SimpleFile.render was given a bad HTTP method")
-            raise RuntimeError(f"{method} is not available for this resource")
-
-
-    def render_GET(self, request:StrRequest):
-
-        request.setHeader("accept-ranges", "bytes")
-
-        if self.type is None:
-            self.type, self.encoding = getTypeAndEncoding(self.basename(),
-                                                          self.contentTypes,
-                                                          self.contentEncodings,
-                                                          self.defaultType)
-
-        try:
-            fileForReading = self.openForReading()
-        except IOError as exc:
-            if exc.erno == errno.EACCESS:
-                # TODO Replace with a 500 exception
-                request.setResponseCode(500, f"Unable to read {self.path!r} due to permission error".encode("utf-8"))
-            else:
-                request.setResponseCode(500, f"Unknown error".encode("utf-8"))
-            return ""
-
-
-
-        if self.type == "text/html":
-            request.setHeader("content-type", f"{self.type}; charset=utf-8")
-        else:
-            request.setHeader("content-type", self.type)
-
-        if request.setLastModified(self.getModificationTime()) is http.CACHED:
-            # TODO research
-            return b""
-
-        producer = self.makeProducer(request, fileForReading)
-        producer.start()
-
-        return NOT_DONE_YET
-
-
-    def render_HEAD(self, request:StrRequest):
-        self._setContentHeaders(request)
-        return b""
-
-    def __repr__(self):
-        return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
\ No newline at end of file
+# import errno
+# from pathlib import Path
+# import typing as T
+#
+# from twisted.web.static import File, getTypeAndEncoding
+# from twisted.web.server import NOT_DONE_YET
+# from twisted.web import http
+# from twisted.python import log
+#
+# from txweb.lib.str_request import StrRequest
+#
+#
+# class SimpleFile(File):
+#     """
+#     Duplicates tx.web.static.File but splits it apart
+#         from serving dual purposes of being a Resource Branch and a leaf.
+#
+#     Purpose: Serve only a specific file
+#
+#     Research:
+#     https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
+#     https://tools.ietf.org/html/rfc7233#section-4.2
+#
+#     TODO There is a transient bug I am running into where the last few bytes
+#     TODO - of the file is being truncated between server and client.
+#     TODO - I feel like this might be part of a memory leak I am still trying to track down.
+#
+#     """
+#     isLeaf = True # type: T.Union[bool, int]
+#
+#     def __init__(self, path:T.Union[str, Path], defaultType = "text/plain"):
+#
+#         if False in [Path(path).is_file(), Path(path).exists()]:
+#             raise ValueError(f"{path} is not a file and or does not exist")
+#
+#         File.__init__(self, str(path), defaultType=defaultType)
+#
+#         self.isLeaf = True
+#
+#     def render(self, request:StrRequest):
+#         method = request.method.lower().decode("utf-8")
+#
+#         if method == "get":
+#             return self.render_GET(request)
+#         elif method == "head":
+#             return self.render_HEAD(request)
+#         else:
+#             log.err(request.method, "SimpleFile.render was given a bad HTTP method")
+#             raise RuntimeError(f"{method} is not available for this resource")
+#
+#
+#     def render_GET(self, request:StrRequest):
+#
+#         request.setHeader("accept-ranges", "bytes")
+#
+#         if self.type is None:
+#             self.type, self.encoding = getTypeAndEncoding(self.basename(),
+#                                                           self.contentTypes,
+#                                                           self.contentEncodings,
+#                                                           self.defaultType)
+#
+#         try:
+#             fileForReading = self.openForReading()
+#         except IOError as exc:
+#             if exc.erno == errno.EACCESS:
+#                 # TODO Replace with a 500 exception
+#                 request.setResponseCode(500, f"Unable to read {self.path!r} due to permission error".encode("utf-8"))
+#             else:
+#                 request.setResponseCode(500, f"Unknown error".encode("utf-8"))
+#             return ""
+#
+#
+#
+#         if self.type == "text/html":
+#             request.setHeader("content-type", f"{self.type}; charset=utf-8")
+#         else:
+#             request.setHeader("content-type", self.type)
+#
+#         if request.setLastModified(self.getModificationTime()) is http.CACHED:
+#             # TODO research
+#             return b""
+#
+#         producer = self.makeProducer(request, fileForReading)
+#         producer.start()
+#
+#         return NOT_DONE_YET
+#
+#
+#     def render_HEAD(self, request:StrRequest):
+#         self._setContentHeaders(request)
+#         return b""
+#
+#     def __repr__(self):
+#         return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
\ No newline at end of file
diff --git a/txweb/tests/test_resources_directory.py b/txweb/tests/test_resources_directory.py
index 11ed11f..78a5966 100644
--- a/txweb/tests/test_resources_directory.py
+++ b/txweb/tests/test_resources_directory.py
@@ -1,126 +1,126 @@
-from txweb.resources import Directory, SimpleFile
-from txweb import Application
-from txweb.http_codes import HTTP404
-
-from pathlib import Path
-import typing as T
-from txweb.lib.str_request import StrRequest
-import pytest
-
-from .helper import MockRequest
-
-
-
-
-
-def test_sketch_out(static_dir):
-    resource = Directory(static_dir, recurse=False)
-    expected = {
-        str(file.name): (file.stat().st_size,)
-        for file in static_dir.glob("*")
-        if file.exists() and file.is_file()
-    }
-
-    @resource.show_files
-    def render_FILES(self, request: StrRequest, files: T.List[str]):
-        buffer = {}
-        for file in files:  # type: str
-            file = Path(file)
-            buffer[file.name] = (file.stat().st_size,)
-
-        return buffer
-
-    request = MockRequest([], "/")
-    request.method = b"GET"
-    actual = resource.render(request)
-    assert actual == expected
-
-
-def test_hybrid_leaf_and_branch(static_dir):
-    request = MockRequest([], b"/")
-    request.method = b"GET"
-    resource = Directory(static_dir)
-
-    child = resource.getChild(b"", request)
-    assert child == resource
-
-    request = MockRequest([], "")
-    request.method = b"GET"
-    resource = Directory(static_dir)
-    child = resource.getChild(b"LICENSE.txt", request)  # type: SimpleFile
-
-    assert child.path == str(static_dir / "LICENSE.txt")
-    assert isinstance(child, SimpleFile)
-
-
-def test_full_suite_with_routed_site_to_added_directory(static_dir):
-
-    test_app = Application(__name__)
-
-
-
-    test_app.add_staticdir("/some/convoluted_path", static_dir)
-
-    request = MockRequest([], "/some/convoluted_path/LICENSE.txt")
-
-    resource = test_app.site.getResourceFor(request)
-
-    assert isinstance(resource, SimpleFile)
-
-
-def test_full_suite_with_empty_url(static_dir):
-    #  TODO relocate to static_dir/directory test suite
-    test_app = Application(namespace=__name__)
-    test_app.add_staticdir("/some/path", static_dir)
-
-    request = MockRequest([], "/some/path/")
-
-    resource = test_app.site.getResourceFor(request)
-    assert isinstance(resource, Directory)
-
-
-def test_full_suite_with_bad_url(static_dir):
-    test_app = Application(namespace=__name__)
-
-    test_app.add_staticdir("/some/path", static_dir)
-
-    request = MockRequest([], "/some/path/FOO.bar")
-
-    assert test_app.site._lastError is None
-    with pytest.raises(HTTP404):
-        resource = test_app.site.getResourceFor(request)
-
-
-def test_simple_security_check_ensure_allowedfiles_is_limited(static_dir):
-    resource = Directory(static_dir)
-    assert len(resource.allowedFiles()) == 2
-
-
-def test_render_Get_is_called_correctly(static_dir):
-    import json
-
-    directory = Directory(static_dir)
-    request = MockRequest([], "/")
-    with pytest.raises(NotImplementedError):
-        response = directory.render(request)
-
-    @directory.handleGet
-    def render(parent, request, allowedFiles):
-        response = {}
-        response['count'] = len(allowedFiles)
-        response['names'] = [x.name for x in allowedFiles]
-        return json.dumps(response)
-
-    response = directory.render(request)
-
-
-
-
-
-
-
-
-
-
-# TODO so many more tests needed
-# I already know that getChild is going to break
+# from txweb.resources import Directory, SimpleFile
+# from txweb import Application
+# from txweb.http_codes import HTTP404
+#
+# from pathlib import Path
+# import typing as T
+# from txweb.lib.str_request import StrRequest
+# import pytest
+#
+# from .helper import MockRequest
+#
+#
+#
+#
+#
+# def test_sketch_out(static_dir):
+#     resource = Directory(static_dir, recurse=False)
+#     expected = {
+#         str(file.name): (file.stat().st_size,)
+#         for file in static_dir.glob("*")
+#         if file.exists() and file.is_file()
+#     }
+#
+#     @resource.show_files
+#     def render_FILES(self, request: StrRequest, files: T.List[str]):
+#         buffer = {}
+#         for file in files:  # type: str
+#             file = Path(file)
+#             buffer[file.name] = (file.stat().st_size,)
+#
+#         return buffer
+#
+#     request = MockRequest([], "/")
+#     request.method = b"GET"
+#     actual = resource.render(request)
+#     assert actual == expected
+#
+#
+# def test_hybrid_leaf_and_branch(static_dir):
+#     request = MockRequest([], b"/")
+#     request.method = b"GET"
+#     resource = Directory(static_dir)
+#
+#     child = resource.getChild(b"", request)
+#     assert child == resource
+#
+#     request = MockRequest([], "")
+#     request.method = b"GET"
+#     resource = Directory(static_dir)
+#     child = resource.getChild(b"LICENSE.txt", request)  # type: SimpleFile
+#
+#     assert child.path == str(static_dir / "LICENSE.txt")
+#     assert isinstance(child, SimpleFile)
+#
+#
+# def test_full_suite_with_routed_site_to_added_directory(static_dir):
+#
+#     test_app = Application(__name__)
+#
+#
+#
+#     test_app.add_staticdir("/some/convoluted_path", static_dir)
+#
+#     request = MockRequest([], "/some/convoluted_path/LICENSE.txt")
+#
+#     resource = test_app.site.getResourceFor(request)
+#
+#     assert isinstance(resource, SimpleFile)
+#
+#
+# def test_full_suite_with_empty_url(static_dir):
+#     #  TODO relocate to static_dir/directory test suite
+#     test_app = Application(namespace=__name__)
+#     test_app.add_staticdir("/some/path", static_dir)
+#
+#     request = MockRequest([], "/some/path/")
+#
+#     resource = test_app.site.getResourceFor(request)
+#     assert isinstance(resource, Directory)
+#
+#
+# def test_full_suite_with_bad_url(static_dir):
+#     test_app = Application(namespace=__name__)
+#
+#     test_app.add_staticdir("/some/path", static_dir)
+#
+#     request = MockRequest([], "/some/path/FOO.bar")
+#
+#     assert test_app.site._lastError is None
+#     with pytest.raises(HTTP404):
+#         resource = test_app.site.getResourceFor(request)
+#
+#
+# def test_simple_security_check_ensure_allowedfiles_is_limited(static_dir):
+#     resource = Directory(static_dir)
+#     assert len(resource.allowedFiles()) == 2
+#
+#
+# def test_render_Get_is_called_correctly(static_dir):
+#     import json
+#
+#     directory = Directory(static_dir)
+#     request = MockRequest([], "/")
+#     with pytest.raises(NotImplementedError):
+#         response = directory.render(request)
+#
+#     @directory.handleGet
+#     def render(parent, request, allowedFiles):
+#         response = {}
+#         response['count'] = len(allowedFiles)
+#         response['names'] = [x.name for x in allowedFiles]
+#         return json.dumps(response)
+#
+#     response = directory.render(request)
+#
+#
+#
+#
+#
+#
+#
+#
+#
+#
+# # TODO so many more tests needed
+# # I already know that getChild is going to break
diff --git a/txweb/tests/test_resources_simple_file.py b/txweb/tests/test_resources_simple_file.py
index ded0914..679a2ee 100644
--- a/txweb/tests/test_resources_simple_file.py
+++ b/txweb/tests/test_resources_simple_file.py
@@ -1,75 +1,75 @@
-
-from .helper import MockRequest
-
-from txweb.resources.simple_file import SimpleFile
-from pathlib import Path
-
-from twisted.web.server import NOT_DONE_YET
-
-import pytest
-
-@pytest.fixture
-def license():
-    file_path = Path(__file__).parent / "fixture" / "static" / "LICENSE.txt" #  type: Path
-    assert file_path.exists()
-    return file_path
-
-
-def test_instantiates(license):
-
-
-    request = MockRequest([], "/foo")
-    resource = SimpleFile(license, defaultType="text/plain")
-    response = resource.render_GET(request)
-    assert response == NOT_DONE_YET
-    assert len(request.written) == 1 #  Expect only one thing to have been written here
-    assert request.written[0] == license.read_bytes()
-
-
-def test_supports_single_ranged_read(license):
-
-
-    request = MockRequest([], "/foo")
-    resource = SimpleFile(license, defaultType="text/plain")
-    request.requestHeaders.setRawHeaders(b"range", [b"bytes=100-200"])
-
-    response = resource.render_GET(request)
-    assert response == NOT_DONE_YET
-    assert len(request.written) == 1
-    expected = license.read_bytes()
-    expected = expected[100:201]
-    actual = request.written[0]
-
-    assert len(actual) == len(expected)
-    assert actual == expected
-
-
-def test_multiple_ranges(license):
-
-
-    request = MockRequest([], "/foo")
-    resource = SimpleFile(license, defaultType="text/plain")
-
-    request.requestHeaders.setRawHeaders(b"range", [b"bytes=0-499, -500"])
-
-    assert resource.render(request) == NOT_DONE_YET
-    assert len(request.written) == 1
-
-    raw_file = license.read_bytes()
-
-    return # TODO what was this test trying to do again?
-
-    content_type = request.responseHeaders.getRawHeaders("content-type")[0]
-    ct_type, ct_boundary = content_type.split(";")
-    _, dividier =ct_boundary.split("=")
-    dividier = dividier.strip().strip("\"")
-
-    parts = request.written[0].decode("utf-8").split(dividier)
-
-    assert len(parts) == 4
-
-    expected1 = raw_file[0:500]
-    expected2 = raw_file[-501]
-
-
-
+#
+# from .helper import MockRequest
+#
+# from txweb.resources.simple_file import SimpleFile
+# from pathlib import Path
+#
+# from twisted.web.server import NOT_DONE_YET
+#
+# import pytest
+#
+# @pytest.fixture
+# def license():
+#     file_path = Path(__file__).parent / "fixture" / "static" / "LICENSE.txt" #  type: Path
+#     assert file_path.exists()
+#     return file_path
+#
+#
+# def test_instantiates(license):
+#
+#
+#     request = MockRequest([], "/foo")
+#     resource = SimpleFile(license, defaultType="text/plain")
+#     response = resource.render_GET(request)
+#     assert response == NOT_DONE_YET
+#     assert len(request.written) == 1 #  Expect only one thing to have been written here
+#     assert request.written[0] == license.read_bytes()
+#
+#
+# def test_supports_single_ranged_read(license):
+#
+#
+#     request = MockRequest([], "/foo")
+#     resource = SimpleFile(license, defaultType="text/plain")
+#     request.requestHeaders.setRawHeaders(b"range", [b"bytes=100-200"])
+#
+#     response = resource.render_GET(request)
+#     assert response == NOT_DONE_YET
+#     assert len(request.written) == 1
+#     expected = license.read_bytes()
+#     expected = expected[100:201]
+#     actual = request.written[0]
+#
+#     assert len(actual) == len(expected)
+#     assert actual == expected
+#
+#
+# def test_multiple_ranges(license):
+#
+#
+#     request = MockRequest([], "/foo")
+#     resource = SimpleFile(license, defaultType="text/plain")
+#
+#     request.requestHeaders.setRawHeaders(b"range", [b"bytes=0-499, -500"])
+#
+#     assert resource.render(request) == NOT_DONE_YET
+#     assert len(request.written) == 1
+#
+#     raw_file = license.read_bytes()
+#
+#     return # TODO what was this test trying to do again?
+#
+#     content_type = request.responseHeaders.getRawHeaders("content-type")[0]
+#     ct_type, ct_boundary = content_type.split(";")
+#     _, dividier =ct_boundary.split("=")
+#     dividier = dividier.strip().strip("\"")
+#
+#     parts = request.written[0].decode("utf-8").split(dividier)
+#
+#     assert len(parts) == 4
+#
+#     expected1 = raw_file[0:500]
+#     expected2 = raw_file[-501]
+#
+#
+#
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 3f0e2ee..e92cba8 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -56,20 +56,20 @@ def add(self, route_str: str, **kwargs: T.Optional[T.Dict[str, T.Any]]) -> Resou
         """
         return self.resource.add(route_str, **kwargs)
 
-    def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> txw_resources.SimpleFile:
-        """
-        Just a simple helper for a common task of serving individual files
-
-        :param route_str: A valid URI route string
-        :param filepath: An absolute or relative path to a file to be served over HTTP
-        :param default_type: What content type should a file be served as
-        :return: twisted.web.static.File
-        """
-        return self.add_resource(route_str, txw_resources.SimpleFile(filePath, defaultType=defaultType))
-
-    def add_directory(self, route_str: str, dirPath: T.Union[str, pathlib.Path]) -> txw_resources.Directory:
-        # TODO pull add_directory OUT of RoutingResource
-        return self.resource.add_directory(route_str, dirPath)
+    # def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> txw_resources.SimpleFile:
+    #     """
+    #     Just a simple helper for a common task of serving individual files
+    #
+    #     :param route_str: A valid URI route string
+    #     :param filepath: An absolute or relative path to a file to be served over HTTP
+    #     :param default_type: What content type should a file be served as
+    #     :return: twisted.web.static.File
+    #     """
+    #     return self.add_resource(route_str, txw_resources.SimpleFile(filePath, defaultType=defaultType))
+    #
+    # def add_directory(self, route_str: str, dirPath: T.Union[str, pathlib.Path]) -> txw_resources.Directory:
+    #     # TODO pull add_directory OUT of RoutingResource
+    #     return self.resource.add_directory(route_str, dirPath)
 
 
 

From f35ffe6ffbc1cc7ae0f3e25a5083d8910a9bd6d6 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 07:50:13 -0700
Subject: [PATCH 117/185] Try and shut up coverage's concern over tracking
 these lines

---
 txweb/lib/at_wsprotocol.py | 12 +++++++-----
 txweb/util/templating.py   |  2 ++
 2 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/at_wsprotocol.py
index 74b6ac2..3ac10b6 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/at_wsprotocol.py
@@ -99,18 +99,19 @@ def ask(self, endpoint, **values):
             raise EnvironmentError("Maximum # of pending asks reached")
 
     def onMessage(self, payload, isBinary):
-        if isBinary:
+        if isBinary:  # pragma: no cover
+            warnings.warn("Received binary payload, don't know how to deal with this.")
             return None # I don't know how to deal with binary
 
-        try:
+        try:  # pragma: no cover
             payload = payload.decode("utf-8")
-        except UnicodeDecodeError:
+        except UnicodeDecodeError:  # pragma: no cover
             warnings.warn(f"Failed to decode {payload}")
             return
 
-        try:
+        try:  # pragma: no cover
             raw_message = json.loads(payload)
-        except json.JSONDecodeError:
+        except json.JSONDecodeError:  # pragma: no cover
             warnings.warn(f"Corrupt/bad payload: {payload}")
             return
 
@@ -126,6 +127,7 @@ def onMessage(self, payload, isBinary):
                 del self.deferred_asks[caller_id]
             else:
                 warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
+                return
 
         elif "endpoint" in message:
 
diff --git a/txweb/util/templating.py b/txweb/util/templating.py
index 73faf00..b6f25d5 100644
--- a/txweb/util/templating.py
+++ b/txweb/util/templating.py
@@ -1,3 +1,4 @@
+
 import json
 import typing as T
 from pathlib import Path
@@ -16,6 +17,7 @@
     TODO: look into jinja2 returning bytes by default to cut down on post-processing
 """
 
+# pragma: no cover
 JINJA2_ENV = None  # type: Environment
 
 

From ae28c1a169e2b8af238bb412a655e34e8e1521b5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:01:31 -0700
Subject: [PATCH 118/185] Renamed Websocket protocol class

---
 txweb/application.py                          | 2 +-
 txweb/lib/message_handler.py                  | 6 +++---
 txweb/lib/routed_factory.py                   | 4 ++--
 txweb/lib/{at_wsprotocol.py => wsprotocol.py} | 4 ++--
 txweb/tests/test_ws_at_wsprotocol.py          | 4 ++--
 5 files changed, 10 insertions(+), 10 deletions(-)
 rename txweb/lib/{at_wsprotocol.py => wsprotocol.py} (97%)

diff --git a/txweb/application.py b/txweb/application.py
index 7030d7a..4960eeb 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -35,7 +35,7 @@
     print("Unable to support websockets:  `pip install autobahn` to enable")
 else:
     AUTOBAHN_MISSING = False
-    from .lib.at_wsprotocol import AtWSProtocol
+    from .lib.wsprotocol import WSProtocol
     from .lib.message_handler import MessageHandler
     from .lib.routed_factory import RoutedWSFactory
     from autobahn.twisted.resource import WebSocketResource
diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 380868d..8706533 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -7,7 +7,7 @@
 
 if T.TYPE_CHECKING or False: # pragma: no cover
     # recursion import
-    from .at_wsprotocol import AtWSProtocol
+    from .wsprotocol import WSProtocol
 
 
 
@@ -15,9 +15,9 @@
 class MessageHandler(Mapping): #pragma: no cover
 
     raw_message: T.Dict[T.str, T.Any]
-    connection: AtWSProtocol
+    connection: WSProtocol
 
-    def __init__(self, raw_message:dict, connection:AtWSProtocol):
+    def __init__(self, raw_message:dict, connection:WSProtocol):
         self.raw_message = raw_message # type: dict
         self.connection = connection
 
diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index 91de76d..a3b9365 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -1,10 +1,10 @@
 from autobahn.twisted.websocket import WebSocketServerFactory
-from .at_wsprotocol import AtWSProtocol
+from .wsprotocol import WSProtocol
 
 
 class RoutedWSFactory(WebSocketServerFactory):  #pragma: no cover
 
-    def __init__(self, url, routes, protocol_cls=AtWSProtocol, application=None):
+    def __init__(self, url, routes, protocol_cls=WSProtocol, application=None):
         WebSocketServerFactory.__init__(self, url)
         self.protocol = protocol_cls
 
diff --git a/txweb/lib/at_wsprotocol.py b/txweb/lib/wsprotocol.py
similarity index 97%
rename from txweb/lib/at_wsprotocol.py
rename to txweb/lib/wsprotocol.py
index 3ac10b6..5086165 100644
--- a/txweb/lib/at_wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -16,7 +16,7 @@
 
 from txweb.log import getLogger
 
-class AtWSProtocol(WebSocketServerProtocol):
+class WSProtocol(WebSocketServerProtocol):
 
     my_log = getLogger()
     """
@@ -28,7 +28,7 @@ class AtWSProtocol(WebSocketServerProtocol):
     def __init__(self, *args, **kwargs):
         self.pending_responses = {}
 
-        super(AtWSProtocol, self).__init__(*args, **kwargs)
+        super(WSProtocol, self).__init__(*args, **kwargs)
         # WebSocketServerProtocol.__init__(self, *args, **kwargs)
 
         self.identity = None
diff --git a/txweb/tests/test_ws_at_wsprotocol.py b/txweb/tests/test_ws_at_wsprotocol.py
index 2044166..585af72 100644
--- a/txweb/tests/test_ws_at_wsprotocol.py
+++ b/txweb/tests/test_ws_at_wsprotocol.py
@@ -3,7 +3,7 @@
 
 from twisted.internet.defer import inlineCallbacks
 
-from txweb.lib.at_wsprotocol import AtWSProtocol
+from txweb.lib.wsprotocol import WSProtocol
 
 @dataclass(frozen=True)
 class CapturedMessage:
@@ -20,7 +20,7 @@ def __init__(self, endpoints):
     def get_endpoint(self, endpoint):
         return self.endpoints[endpoint]
 
-class TrackingProtocol(AtWSProtocol):
+class TrackingProtocol(WSProtocol):
 
         def __init__(self, *args, **kwargs):
             super(TrackingProtocol, self).__init__(*args, **kwargs)

From 8b05dc9c14a224f18f36579859a791a9298380e3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:01:43 -0700
Subject: [PATCH 119/185] documentation

---
 txweb/lib/errors/handler.py | 42 ++++++++++++++++++++++++++++++++-----
 txweb/lib/errors/html.py    |  3 +++
 txweb/util/templating.py    | 14 ++++++++++++-
 3 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index b0f6e6c..697674c 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -1,3 +1,8 @@
+"""
+    Base, Default, and generic Debug error handlers for txweb.
+
+
+"""
 from __future__ import annotations
 import typing as T
 from pathlib import Path
@@ -34,28 +39,39 @@ class StackFrame(T.NamedTuple):
 
 
 class BaseHandler(object):
+    """
+        TODO - Look into using twisted/zope's interface system
+
+        Base/example class of an error handler.
+    """
 
     def __init__(self, application):
         self.application = application
 
-    def __call__(self, request: StrRequest, reason: Failure) -> None:
+    def __call__(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:
+        """
+
+        :param request:  Provided to allow managing the connection (writing, http code, etc).
+        :param reason:
+        :return: Return False if the handler was unable to handle the error
+        """
         # noinspection PyBroadException
         try:
-            self.process(request, reason)
+            self.process(request, error)
         except Exception as exc:
             log.error("PANIC - There was an exception in the error handler.")
             request.ensureFinished()
             raise exc
 
-    def process(self, request: StrRequest, reason: Failure) -> None:  # pragma: no cover
+    def process(self, request: StrRequest, error: Failure) -> None:  # pragma: no cover
         raise NotImplementedError("Attempting to use Base error handler")
 
 
 # noinspection PyMissingConstructor
 class DefaultHandler(BaseHandler):
     """
-        Goal:  Delegate various errors to templates to make
-            a visual error system easier to view.
+        Primarily focused with handling 3xx HTTP exception/codes thrown by the application.
+
     """
 
     def __init__(self, enable_debug=False):
@@ -63,6 +79,22 @@ def __init__(self, enable_debug=False):
         self.enable_debug = enable_debug
 
     def process(self, request: StrRequest, reason: Failure) -> bool:
+        """
+            As mentioned in class docblock, primary focus is handling HTTPCode exceptions thrown by the application.
+
+            If the request/response factory has already started writing to the client, this halts all error processing
+             and throws the exception.
+
+            else if a HTTPCode exception/error it redirects for 3xx codes
+            OR it writes the code and message to the client
+            (Eg HTTPCode500 with Internal error would throw 500 "Internal server error")
+
+            else it sends a 500 HTTP response and then raises the exception back into the user application.
+
+        :param request:
+        :param reason:
+        :return:
+        """
 
         if request.startedWriting not in [0, False]:
             # There is nothing we can do, the out going stream is already tainted
diff --git a/txweb/lib/errors/html.py b/txweb/lib/errors/html.py
index c501b94..f1f6cef 100644
--- a/txweb/lib/errors/html.py
+++ b/txweb/lib/errors/html.py
@@ -1,3 +1,6 @@
+"""
+    TODO - Deprecate this
+"""
 
 DEFAULT_BODY = \
 b"""
diff --git a/txweb/util/templating.py b/txweb/util/templating.py
index b6f25d5..d60e608 100644
--- a/txweb/util/templating.py
+++ b/txweb/util/templating.py
@@ -22,6 +22,12 @@
 
 
 class MyCache(BytecodeCache):  # pragma: no cover
+    """
+        To help allievate some of the hanging/processing time for rendering templates, this
+         caches compiled byte cord versions of previously used templates.
+
+         https://jinja.palletsprojects.com/en/2.11.x/api/#bytecode-cache
+    """
 
     def __init__(self, directory: T.Union[str, Path]):
         self.directory = directory
@@ -38,7 +44,13 @@ def dump_bytecode(self, bucket: Bucket):
             bucket.write_bytecode(my_file)
 
 
-def initialize_jinja2(template_dir: T.Union[Path, str], cache_dir=None):  # pragma: no cover
+def initialize_jinja2(template_dir: T.Union[Path, str, T.List[T.Union[Path, str]]], cache_dir: T.Optional[str, Path] = None):  # pragma: no cover
+    """
+
+    :param template_dir:  Can be either a string, Path object, or a list of string/path objects pointing to template directories
+    :param cache_dir: optional absolute path to a cache dir intended for storing compiled templates
+    :return:
+    """
     global JINJA2_ENV
 
     env_kwargs = {}

From 1158e29763d369ec96253a726146f055d5382c99 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:19:59 -0700
Subject: [PATCH 120/185] pep8

---
 txweb/lib/errors/handler.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 697674c..48774e7 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -57,13 +57,13 @@ def __call__(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:
         """
         # noinspection PyBroadException
         try:
-            self.process(request, error)
+            return self.process(request, error)
         except Exception as exc:
             log.error("PANIC - There was an exception in the error handler.")
             request.ensureFinished()
             raise exc
 
-    def process(self, request: StrRequest, error: Failure) -> None:  # pragma: no cover
+    def process(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:  # pragma: no cover
         raise NotImplementedError("Attempting to use Base error handler")
 
 
@@ -126,7 +126,7 @@ def process(self, request: StrRequest, reason: Failure) -> bool:
         return True
 
 
-class DebugHandler(BaseHandler):  #  pragma: no cover
+class DebugHandler(BaseHandler):  # pragma: no cover
     """
         TODO - To finish
 
@@ -166,7 +166,8 @@ def process(self, request: StrRequest, reason: Failure) -> None:
         request.write(body)
         request.ensureFinished()
 
-    def format_stack(self, frames:T.List[StackFrame]) -> T.Generator[T.Dict[str, T.Any]]:
+    @staticmethod
+    def format_stack(frames: T.List[StackFrame]) -> T.Generator[T.Dict[str, T.Any]]:
         for frame in frames:
             yield FormattedFrame(
                 name=frame[0].encode("UTF-8"),

From 2d575c80404c6684f4400ab4b0f1297aac8e79fe Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:20:07 -0700
Subject: [PATCH 121/185] Update wsprotocol.py

---
 txweb/lib/wsprotocol.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 5086165..a8d6ada 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -36,9 +36,6 @@ def __init__(self, *args, **kwargs):
         # for tracking deferred `ask` calls
         self.deferred_asks = {}
 
-
-        # self._raw_message = {}
-
     @property
     def application(self):
         return self.factory.get_application()

From 419bf3da8ca7a5dc522c198de8b461507f235f61 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:20:21 -0700
Subject: [PATCH 122/185] removed dead files

---
 txweb/resources/directory.py | 102 -----------------------------------
 txweb/resources/error.py     |   0
 2 files changed, 102 deletions(-)
 delete mode 100644 txweb/resources/directory.py
 delete mode 100644 txweb/resources/error.py

diff --git a/txweb/resources/directory.py b/txweb/resources/directory.py
deleted file mode 100644
index a1116b4..0000000
--- a/txweb/resources/directory.py
+++ /dev/null
@@ -1,102 +0,0 @@
-# """
-#     Clean slate reimplementation of the Directory resource.
-#
-#
-# """
-# from txweb.lib.str_request import StrRequest
-# from txweb.resources import SimpleFile
-# from txweb.http_codes import HTTP404, HTTP405
-#
-# from twisted.web.resource import Resource
-#
-# from pathlib import Path
-# import typing as T
-#
-#
-# class Directory(Resource):
-#     """
-#         TODO - This still needs a lot of work
-#
-#         Non-recursive directory listing resource.  To clarify, this only serves the files in a given filepath and nothing
-#         in child directories nor their contents.
-#
-#         #Goals
-#
-#         * allow hooking into the directory listing response so that it can be overloaded by user land code.
-#         * Instead of trying to take the user supplied input and then scrubbing it for ".." or similar security breaking
-#             injects, it uses a prebuilt (Path.iterdir) allowed list of files and compares that to the path info.
-#
-#
-#
-#     """
-#
-#     def __init__(self, path:T.Union[str, Path], recurse:bool=False):
-#         """
-#
-#         :param path: str/Path an ABSOLUTE filepath to a directory to be exposed to a http client
-#         :param recurse: bool should Directory exposed subdirectories
-#         """
-#         Resource.__init__(self)
-#         self.path = Path(path) #  type: Path
-#         self.isLeaf = False
-#         self.recurse = False
-#
-#         #dynamic helpers
-#
-#         self._render_GET = self.render_GET
-#
-#     def handleGet(self, func):
-#         self._render_GET = func
-#
-#         return func
-#
-#
-#     def show_files(self, func):
-#         self._render_GET = func
-#         return func
-#
-#     def allowedFiles(self) -> T.List[str]:
-#         """
-#             Currently this mechanism is part of the basic security for Directory resource to ensure
-#             only the files in a given directory can be served and or listed.
-#
-#
-#         Returns:
-#             list: an explicit list of what files can be served from this Directory resource
-#         """
-#         return [file for file in self.path.glob("*") if file.exists() and file.is_file()]
-#
-#
-#
-#     def getChild(self, path:bytes, request: StrRequest):
-#
-#         path = path.decode("utf-8")
-#         if path.lower() in ["/", "", "index", "index.html"]:
-#             return self
-#
-#         if path in [f.name for f in self.allowedFiles()]:
-#             return SimpleFile(self.path / path, defaultType="text/blah")
-#
-#
-#         raise HTTP404()
-#
-#
-#     def render(self, request):
-#         if request.method == b"GET":
-#             return self._render_GET(self, request, self.allowedFiles())
-#
-#         elif request.method == b"HEAD":
-#             # TODO fix this up
-#             self._render_GET(self, request, self.allowedFiles())
-#             return b""
-#         else:
-#             raise HTTP405()
-#
-#     @staticmethod
-#     def render_GET(parent, request, files):
-#         raise NotImplementedError("TODO")
-#
-#
-#     def __repr__(self):
-#         return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
-#
diff --git a/txweb/resources/error.py b/txweb/resources/error.py
deleted file mode 100644
index e69de29..0000000

From 7a1ee79a7af3d97eea663fd30b04e504d7135730 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:29:49 -0700
Subject: [PATCH 123/185] PEP8

---
 txweb/resources/routing.py | 62 +++++++++++++++-----------------------
 1 file changed, 24 insertions(+), 38 deletions(-)

diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 3546332..b8b9b40 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -32,27 +32,21 @@
 #    view_function(request, foo, bar)
 # EndPointCallable should match `view_function`
 EndpointCallable = T.NewType("InstanceCallable",
-                                  T.Callable[
-                                      [StrRequest,
-                                       T.Optional[T.Iterable],
-                                       T.Optional[T.Dict],
-                                       ], T.Union[str, int]])
-
+                             T.Callable[[StrRequest, T.Optional[T.Iterable], T.Optional[T.Dict], ], T.Union[str, int]])
 
 
 class RoutingResource(resource.Resource):
 
-
-    def __init__(self, on_error: T.Optional[resource.Resource] = None):
-
+    def __init__(self, script_name: bytes = None):
 
         resource.Resource.__init__(self)
 
         self._site = None
-        self._endpoints = OrderedDict() # type: typing.Dict[str, resource.Resource]
-        self._instances = OrderedDict() # type: typing.Dict[str, object]
-        self._route_map = wz_routing.Map() # type: wz_routing.Map
+        self._endpoints = OrderedDict()  # type: typing.Dict[str, resource.Resource]
+        self._instances = OrderedDict()  # type: typing.Dict[str, object]
+        self._route_map = wz_routing.Map()  # type: wz_routing.Map
         self._route_map.converters['directory'] = DirectoryPath
+        self._script_name = script_name
 
     @property
     def site(self):   # pragma: no cover
@@ -61,13 +55,11 @@ def site(self):   # pragma: no cover
     @site.setter
     def site(self, site):  # pragma: no cover
         self._site = site
-        return self._site
-
 
     def iter_rules(self) -> T.Generator:
         return self._route_map.iter_rules()
 
-    def add(self, route_str:str, **kwargs:T.Dict[str, T.Any]):
+    def add(self, route_str: str, **kwargs: T.Dict[str, T.Any]):
 
         assert "endpoint" not in kwargs, \
             "Undefined behavior to use RoutingResource.add('/some/route/', endpoint='something', ...)"
@@ -78,7 +70,7 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
 
             endpoint_name = get_thing_name(original_thing)
 
-            common_kwargs = {"endpoint":endpoint_name, "thing":original_thing, "route_kwargs":kwargs}
+            common_kwargs = {"endpoint": endpoint_name, "thing": original_thing, "route_kwargs": kwargs}
 
             if inspect.isclass(original_thing) and issubclass(original_thing, resource.Resource):
 
@@ -97,7 +89,8 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
                 self._add_callable(route_str, **common_kwargs)
 
             else:
-                raise ValueError(f"Received {original_thing} but expected callable|Object|twisted.web.resource.Resource")
+                raise ValueError(
+                    f"Received {original_thing} but expected callable|Object|twisted.web.resource.Resource")
 
             # return whatever was decorated unchanged
             # the Resource.getChildForRequest is completely shortcircuited so
@@ -106,10 +99,10 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
 
         return processor
 
-    def _add_callable(self, route_str:str,
-                      endpoint:str=None,
-                      thing:T.Union[EndpointCallable, object]=None,
-                      route_kwargs:T.Dict[str,T.Any]=None):
+    def _add_callable(self, route_str: str,
+                      endpoint: str = None,
+                      thing: T.Union[EndpointCallable, object] = None,
+                      route_kwargs: T.Dict[str, T.Any] = None):
         """
 
         :param route_str: a valid path for werkzeug routing
@@ -127,7 +120,7 @@ def _add_callable(self, route_str:str,
 
     def _add_class(self, route_str: T.AnyStr,
                    endpoint: T.AnyStr = None,
-                   thing:T.Union[object,T.Callable] = None,
+                   thing: T.Union[object, T.Callable] = None,
                    route_kwargs: T.Dict[str, T.Any] = None):
 
         if vca.is_renderable(thing) is False:
@@ -139,7 +132,7 @@ def _add_class(self, route_str: T.AnyStr,
             self._endpoints.update(result.endpoints)
             self._route_map.add(result.rule)
         else:
-            instance = self._instances[endpoint] = thing(**route_kwargs.get("inits_kwargs",{}))
+            instance = self._instances[endpoint] = thing(**route_kwargs.get("inits_kwargs", {}))
             self._route_map.add(wz_routing.Rule(route_str, endpoint=endpoint))
             self._endpoints[endpoint] = ViewClassResource(thing, instance)
 
@@ -149,7 +142,6 @@ def _add_resource_cls(self, route_str, endpoint=None, thing=None, route_kwargs=N
             self._instances[endpoint] = thing()
         self._add_resource(route_str, endpoint=endpoint, thing=self._instances[endpoint], route_kwargs=route_kwargs)
 
-
     def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None):
         route_kwargs = route_kwargs if route_kwargs is not None else {}
         endpoint = endpoint or get_thing_name(thing)
@@ -165,23 +157,20 @@ def add_directory(self, route_str: str, directory_resource: File) -> File:
         if endpoint not in self._endpoints:
             self._endpoints[endpoint] = directory_resource
 
-
         fixed_rule = wz_routing.Rule(route_str,
                                      endpoint=endpoint,
                                      methods=["GET", "HEAD"],
-                                     defaults={"postpath":""})
+                                     defaults={"postpath": ""})
 
         instrumented_rule = wz_routing.Rule(route_str + "",
                                             endpoint=endpoint,
-                                            methods=["GET","HEAD"])
+                                            methods=["GET", "HEAD"])
 
         self._route_map.add(fixed_rule)
         self._route_map.add(instrumented_rule)
 
         return directory_resource
 
-
-
     def _build_map(self, pathEl, request):
 
         map_bind_kwargs = {}
@@ -193,9 +182,12 @@ def _build_map(self, pathEl, request):
         else:
             map_bind_kwargs["server_name"] = request.getRequestHostname()
 
-        map_bind_kwargs["script_name"] = b"/"  # b"/".join(request.prepath) if request.prepath else b"/"
+        if self._script_name is None:
+            map_bind_kwargs["script_name"] = b"/"  # b"/".join(request.prepath) if request.prepath else b"/"
+        else:
+            map_bind_kwargs["script_name"] = self._script_name
 
-        #TODO add strict slash check flag to here or to website.add
+        # TODO add strict slash check flag to here or to website.add
         if map_bind_kwargs["script_name"].startswith(b"/") is False:
             map_bind_kwargs["script_name"] = b"/" + map_bind_kwargs["script_name"]
 
@@ -207,9 +199,7 @@ def _build_map(self, pathEl, request):
 
         return self._route_map.bind(**map_bind_kwargs)
 
-
-
-    def getChildWithDefault(self, pathEl: T.Union[bytes,str], request: StrRequest):
+    def getChildWithDefault(self, pathEl: T.Union[bytes, str], request: StrRequest):
         """
             Routing resource is mostly ignorant of the larger ecosystem so it either
             returns a resource OR it throws up an errors.HTTPCode
@@ -241,7 +231,3 @@ def getChildWithDefault(self, pathEl: T.Union[bytes,str], request: StrRequest):
             request.postpath = [el.encode("utf-8") for el in kwargs['postpath']]
         return self._endpoints[rule.endpoint]
 
-        # if rule:
-        #
-        # else:
-        #     raise HTTP_Errors.HTTP404()
\ No newline at end of file

From 896a50ece8d4bd3050b56e30f19920ffbc0fad66 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:47:05 -0700
Subject: [PATCH 124/185] PEP8

---
 txweb/lib/errors/__init__.py |  3 +--
 txweb/lib/message_handler.py | 17 +++++++----------
 2 files changed, 8 insertions(+), 12 deletions(-)

diff --git a/txweb/lib/errors/__init__.py b/txweb/lib/errors/__init__.py
index cd133fc..ca731ec 100644
--- a/txweb/lib/errors/__init__.py
+++ b/txweb/lib/errors/__init__.py
@@ -1,2 +1 @@
-
-from twisted.python.failure import Failure
\ No newline at end of file
+from twisted.python.failure import Failure
diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 8706533..862fbc1 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -5,20 +5,18 @@
 
 from collections.abc import Mapping
 
-if T.TYPE_CHECKING or False: # pragma: no cover
+if T.TYPE_CHECKING or False:  # pragma: no cover
     # recursion import
     from .wsprotocol import WSProtocol
 
 
-
-
-class MessageHandler(Mapping): #pragma: no cover
+class MessageHandler(Mapping):  # pragma: no cover
 
     raw_message: T.Dict[T.str, T.Any]
     connection: WSProtocol
 
-    def __init__(self, raw_message:dict, connection:WSProtocol):
-        self.raw_message = raw_message # type: dict
+    def __init__(self, raw_message: dict, connection: WSProtocol):
+        self.raw_message = raw_message  # type: dict
         self.connection = connection
 
     def __getitem__(self, item):  # pragma: no cover
@@ -30,7 +28,7 @@ def __iter__(self):  # pragma: no cover
     def __len__(self):  # pragma: no cover
         return len(self.raw_message)
 
-    def __contains__(self, item): # pragma: no cover
+    def __contains__(self, item):  # pragma: no cover
         return item in self.raw_message
 
     def keys(self):  # pragma: no cover
@@ -58,8 +56,7 @@ def get(self, key, default=None, type=None):
         return value
 
     def args(self, key, default=None, type=None):
-        args = None
-        value = default
+
         try:
             args = self['args']
         except KeyError:
@@ -90,4 +87,4 @@ def ask(self, endpoint, **kwargs):
         return self.connection.ask(endpoint, type="ask", args=kwargs)
 
     def get_session(self, get_key=None):
-        return self.connection.application.get_session(self.connection, get_key=get_key)
\ No newline at end of file
+        return self.connection.application.get_session(self.connection, get_key=get_key)

From 4b86cd39cbb621d61bd0ce687a6bd683a39182c2 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 11:47:41 -0700
Subject: [PATCH 125/185] pep8

---
 txweb/lib/routed_factory.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index a3b9365..21fbb72 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -2,7 +2,7 @@
 from .wsprotocol import WSProtocol
 
 
-class RoutedWSFactory(WebSocketServerFactory):  #pragma: no cover
+class RoutedWSFactory(WebSocketServerFactory):  # pragma: no cover
 
     def __init__(self, url, routes, protocol_cls=WSProtocol, application=None):
         WebSocketServerFactory.__init__(self, url)
@@ -14,6 +14,5 @@ def __init__(self, url, routes, protocol_cls=WSProtocol, application=None):
     def get_endpoint(self, name):
         return self.routes.get(name, None)
 
-
     def get_application(self):
-        return self._application
\ No newline at end of file
+        return self._application

From 2995671d6fe6aedfe95f78e4cc0694f13d170e44 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 12:40:40 -0700
Subject: [PATCH 126/185] pep8

---
 txweb/lib/str_request.py | 19 +++++++++++--------
 txweb/util/reloader.py   |  1 +
 txweb/util/templating.py | 17 +++++++++--------
 3 files changed, 21 insertions(+), 16 deletions(-)

diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py
index d3c4a14..6be47de 100644
--- a/txweb/lib/str_request.py
+++ b/txweb/lib/str_request.py
@@ -19,6 +19,7 @@
 import typing as T
 
 from twisted.python import reflect
+# noinspection PyProtectedMember
 from twisted.web.error import UnsupportedMethod
 from twisted.web.server import Request, NOT_DONE_YET, supportedMethods
 from twisted.web.http import FOUND
@@ -88,7 +89,7 @@ def write(self, data: T.Union[bytes, str]):
         return Request.write(self, data)
 
     def writeTotal(self, response_body: T.Union[bytes, str], code: T.Union[int, str, bytes] = None,
-                   message:T.Union[bytes, str] = None) -> T.NoReturn:
+                   message: T.Union[bytes, str] = None) -> T.NoReturn:
         """
         
         :param response_body: Content intended for after headers 
@@ -106,7 +107,7 @@ def writeTotal(self, response_body: T.Union[bytes, str], code: T.Union[int, str,
         self.write(response_body)
         self.ensureFinished()
 
-    def writeJSON(self, data:T.Dict):
+    def writeJSON(self, data: T.Dict):
         """
             Utility to take a dictionary and convert it to a JSON string
         """
@@ -116,7 +117,7 @@ def writeJSON(self, data:T.Dict):
         self.setHeader("Content-Length", content_length)
         return self.write(payload)
 
-    def setHeader(self, name:T.Union[str, bytes], value:T.Union[str, bytes]):
+    def setHeader(self, name: T.Union[str, bytes], value: T.Union[str, bytes]):
         """
             A quick wrapper to convert unicode inputs to utf-8 bytes
 
@@ -132,7 +133,9 @@ def setHeader(self, name:T.Union[str, bytes], value:T.Union[str, bytes]):
 
         return Request.setHeader(self, name, value)
 
-    def setResponseCode(self, code: int = 500, message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"):
+    def setResponseCode(self,
+                        code: int = 500,
+                        message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"):
         if message and not isinstance(message, bytes):
             message = message.encode("utf-8")
 
@@ -192,7 +195,6 @@ def query_iter(arguments):
 
         self.process()
 
-
     @property
     def methodIsPost(self):
         return self.method == b"POST"
@@ -225,11 +227,12 @@ def render(self, resrc: resource.Resource) -> None:
 
         # TODO deal with HEAD requests or leave it to the Application developer to deal with?
 
-        if body is NOT_DONE_YET:  #TODO replace NOT_DONE_YET with a sentinel versus integer
+        if body is NOT_DONE_YET:  # TODO replace NOT_DONE_YET with a sentinel versus integer
             return
 
         if not isinstance(body, bytes):
-            log.error(f"<{type(resrc)} {resrc}> - uri={self.uri} returned {type(body)}:{len(body)} but MUST return a byte string")
+            log.error(
+                f"<{type(resrc)}{resrc!r}> - uri={self.uri} returned {type(body)}:{len(body)} but MUST return a byte string")
             raise HTTP500()
 
         if self.method == b"HEAD":
@@ -288,7 +291,7 @@ def processingFailed(self, reason):
 
     @property
     def json(self):
-        if self.getHeader("Content-Type") not in ["application/json", "text/json"]:
+        if self.getHeader("Content-Type") in ["application/json", "text/json"]:
             return json.loads(self.content.read())
         else:
             return None
diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index 988d210..6a883b1 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -87,6 +87,7 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
 
     :param root_dir: pathlib.Path current working dir to search
     :param watch_self: bool Watch all of txweb for changes
+    :param ignore_prefix: simple check that if provided compares file names to the prefix and skips if they match
     :return: None
     """
 
diff --git a/txweb/util/templating.py b/txweb/util/templating.py
index d60e608..86d4301 100644
--- a/txweb/util/templating.py
+++ b/txweb/util/templating.py
@@ -23,7 +23,7 @@
 
 class MyCache(BytecodeCache):  # pragma: no cover
     """
-        To help allievate some of the hanging/processing time for rendering templates, this
+        To help alleviate some of the hanging/processing time for rendering templates, this
          caches compiled byte cord versions of previously used templates.
 
          https://jinja.palletsprojects.com/en/2.11.x/api/#bytecode-cache
@@ -44,10 +44,15 @@ def dump_bytecode(self, bucket: Bucket):
             bucket.write_bytecode(my_file)
 
 
-def initialize_jinja2(template_dir: T.Union[Path, str, T.List[T.Union[Path, str]]], cache_dir: T.Optional[str, Path] = None):  # pragma: no cover
+def initialize_jinja2(
+        template_dir: T.Union[Path,
+                              str,
+                              T.List[T.Union[Path, str]]],
+        cache_dir: T.Optional[str, Path] = None):  # pragma: no cover
     """
 
-    :param template_dir:  Can be either a string, Path object, or a list of string/path objects pointing to template directories
+    :param template_dir:  Can be either a string, Path object, or a list of string/path objects pointing
+    to template directories
     :param cache_dir: optional absolute path to a cache dir intended for storing compiled templates
     :return:
     """
@@ -63,12 +68,8 @@ def initialize_jinja2(template_dir: T.Union[Path, str, T.List[T.Union[Path, str]
     JINJA2_ENV = Environment(**env_kwargs)
 
 
-def render(template_pathname, **template_args): # pragma: no cover
+def render(template_pathname, **template_args):  # pragma: no cover
     if JINJA2_ENV is None:
         raise EnvironmentError("Jinja2 environment not initialized, call initialize_jinja2 first")
 
     return JINJA2_ENV.get_template(template_pathname).render(**template_args)
-
-
-
-

From 05606be6f9c562a796e6c75b3a789543af226c44 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 12:42:46 -0700
Subject: [PATCH 127/185] pep8

---
 txweb/lib/str_request.py          | 3 ++-
 txweb/lib/view_class_assembler.py | 8 ++++++--
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py
index 6be47de..694c393 100644
--- a/txweb/lib/str_request.py
+++ b/txweb/lib/str_request.py
@@ -232,7 +232,8 @@ def render(self, resrc: resource.Resource) -> None:
 
         if not isinstance(body, bytes):
             log.error(
-                f"<{type(resrc)}{resrc!r}> - uri={self.uri} returned {type(body)}:{len(body)} but MUST return a byte string")
+                f"<{type(resrc)}{resrc!r}>" 
+                f"- uri={self.uri} returned {type(body)}:{len(body)} but MUST return a byte string")
             raise HTTP500()
 
         if self.method == b"HEAD":
diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index a65a14e..29f8f9d 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -36,6 +36,7 @@ def handle_show(self, request):
 PREFILTER_ID = "__PREFILTER_ID__"
 POSTFILTER_ID = "__POSTFILTER_ID__"
 
+
 def has_exposed(obj):
     return any([
         True
@@ -43,11 +44,11 @@ def has_exposed(obj):
         if inspect.isfunction(m) and hasattr(m, EXPOSED_STR)
     ])
 
-def is_exposed(attribute):
-
 
+def is_exposed(attribute):
     return has_exposed(attribute, EXPOSED_STR) and is_valid_callable
 
+
 def is_viewable(attribute):
     is_valid_callable = inspect.ismethod(attribute) \
                         or inspect.isfunction(attribute) \
@@ -79,10 +80,12 @@ def processor(func):
 
     return processor
 
+
 def set_prefilter(func):
     setattr(func, PREFILTER_ID, True)
     return func
 
+
 def set_postfilter(func):
     setattr(func, POSTFILTER_ID, True)
     return func
@@ -90,6 +93,7 @@ def set_postfilter(func):
 
 ViewAssemblerResult = namedtuple("ViewAssemblerResult", "instance,rule,endpoints")
 
+
 def find_member(thing, identifier) -> T.Union[T.Callable, bool]:
 
     for name, member in inspect.getmembers(thing, lambda v: hasattr(v, identifier)):

From 1b110074278449b545f7812b115748ee4606819c Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 12:44:05 -0700
Subject: [PATCH 128/185] Update wsprotocol.py

---
 txweb/lib/wsprotocol.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index a8d6ada..68189f2 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -16,6 +16,7 @@
 
 from txweb.log import getLogger
 
+
 class WSProtocol(WebSocketServerProtocol):
 
     my_log = getLogger()
@@ -28,7 +29,7 @@ class WSProtocol(WebSocketServerProtocol):
     def __init__(self, *args, **kwargs):
         self.pending_responses = {}
 
-        super(WSProtocol, self).__init__(*args, **kwargs)
+        super(WSProtocol, self).__init__()
         # WebSocketServerProtocol.__init__(self, *args, **kwargs)
 
         self.identity = None
@@ -42,7 +43,6 @@ def application(self):
 
     def getCookie(self, cookie_name, default=None):
         raw_cookies = self.http_headers.get('cookie', "")
-        cookies = {}
         for params in raw_cookies.split(";"):
             str_name, value = params.split("=")
             if str_name.strip() == cookie_name:
@@ -114,6 +114,7 @@ def onMessage(self, payload, isBinary):
 
         message = MessageHandler(raw_message, self)
         result = None
+        endpoint_func = None
 
         if message.get("type") == "response":
             caller_id = message.get("caller_id", None)

From 4f51106a0effc27ce0002a26deb774699876ccc8 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 12:52:33 -0700
Subject: [PATCH 129/185] Delete simple_file.py

---
 txweb/resources/simple_file.py | 94 ----------------------------------
 1 file changed, 94 deletions(-)
 delete mode 100644 txweb/resources/simple_file.py

diff --git a/txweb/resources/simple_file.py b/txweb/resources/simple_file.py
deleted file mode 100644
index 30ab229..0000000
--- a/txweb/resources/simple_file.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# import errno
-# from pathlib import Path
-# import typing as T
-#
-# from twisted.web.static import File, getTypeAndEncoding
-# from twisted.web.server import NOT_DONE_YET
-# from twisted.web import http
-# from twisted.python import log
-#
-# from txweb.lib.str_request import StrRequest
-#
-#
-# class SimpleFile(File):
-#     """
-#     Duplicates tx.web.static.File but splits it apart
-#         from serving dual purposes of being a Resource Branch and a leaf.
-#
-#     Purpose: Serve only a specific file
-#
-#     Research:
-#     https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
-#     https://tools.ietf.org/html/rfc7233#section-4.2
-#
-#     TODO There is a transient bug I am running into where the last few bytes
-#     TODO - of the file is being truncated between server and client.
-#     TODO - I feel like this might be part of a memory leak I am still trying to track down.
-#
-#     """
-#     isLeaf = True # type: T.Union[bool, int]
-#
-#     def __init__(self, path:T.Union[str, Path], defaultType = "text/plain"):
-#
-#         if False in [Path(path).is_file(), Path(path).exists()]:
-#             raise ValueError(f"{path} is not a file and or does not exist")
-#
-#         File.__init__(self, str(path), defaultType=defaultType)
-#
-#         self.isLeaf = True
-#
-#     def render(self, request:StrRequest):
-#         method = request.method.lower().decode("utf-8")
-#
-#         if method == "get":
-#             return self.render_GET(request)
-#         elif method == "head":
-#             return self.render_HEAD(request)
-#         else:
-#             log.err(request.method, "SimpleFile.render was given a bad HTTP method")
-#             raise RuntimeError(f"{method} is not available for this resource")
-#
-#
-#     def render_GET(self, request:StrRequest):
-#
-#         request.setHeader("accept-ranges", "bytes")
-#
-#         if self.type is None:
-#             self.type, self.encoding = getTypeAndEncoding(self.basename(),
-#                                                           self.contentTypes,
-#                                                           self.contentEncodings,
-#                                                           self.defaultType)
-#
-#         try:
-#             fileForReading = self.openForReading()
-#         except IOError as exc:
-#             if exc.erno == errno.EACCESS:
-#                 # TODO Replace with a 500 exception
-#                 request.setResponseCode(500, f"Unable to read {self.path!r} due to permission error".encode("utf-8"))
-#             else:
-#                 request.setResponseCode(500, f"Unknown error".encode("utf-8"))
-#             return ""
-#
-#
-#
-#         if self.type == "text/html":
-#             request.setHeader("content-type", f"{self.type}; charset=utf-8")
-#         else:
-#             request.setHeader("content-type", self.type)
-#
-#         if request.setLastModified(self.getModificationTime()) is http.CACHED:
-#             # TODO research
-#             return b""
-#
-#         producer = self.makeProducer(request, fileForReading)
-#         producer.start()
-#
-#         return NOT_DONE_YET
-#
-#
-#     def render_HEAD(self, request:StrRequest):
-#         self._setContentHeaders(request)
-#         return b""
-#
-#     def __repr__(self):
-#         return f"<{self.__class__.__name__} at {id(self)!r} path={self.path!r}/>"
\ No newline at end of file

From 0555d6b81fe56b0a3a19c66f60d60f4eff611a4f Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 12:52:40 -0700
Subject: [PATCH 130/185] pep8

---
 txweb/lib/wsprotocol.py    | 20 ++++++++++----------
 txweb/resources/routing.py | 13 ++++++-------
 2 files changed, 16 insertions(+), 17 deletions(-)

diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 68189f2..af07ca5 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
 try:
     import ujson as json
 except ImportError:
@@ -25,6 +26,7 @@ class WSProtocol(WebSocketServerProtocol):
         pending asks to 100
     """
     MAX_ASKS = 100  # Need to make this tunable
+    factory: RoutedFactory
 
     def __init__(self, *args, **kwargs):
         self.pending_responses = {}
@@ -54,7 +56,7 @@ def onConnect(self, request):
         self.identity = uuid4().hex
         self.my_log.debug("Client connecting: {request.peer}", request=request)
 
-    def onClose(self, wasClean, code, reason):
+    def onClose(self, was_clean, code, reason):
         self.on_disconnect.addErrback(self.my_log.error)
         self.on_disconnect.callback(self.identity)
         del self.on_disconnect
@@ -87,18 +89,18 @@ def ask(self, endpoint, **values):
         :return:
         """
         if len(self.deferred_asks) < self.MAX_ASKS:
-            requestToken = uuid4().hex
+            request_token = uuid4().hex
             d = Deferred()
-            self.deferred_asks[requestToken] = d
-            self.sendDict(endpoint=endpoint, type="ask", caller_id=requestToken, args=values)
+            self.deferred_asks[request_token] = d
+            self.sendDict(endpoint=endpoint, type="ask", caller_id=request_token, args=values)
             return d
         else:
             raise EnvironmentError("Maximum # of pending asks reached")
 
-    def onMessage(self, payload, isBinary):
-        if isBinary:  # pragma: no cover
+    def onMessage(self, payload, is_binary):
+        if is_binary:  # pragma: no cover
             warnings.warn("Received binary payload, don't know how to deal with this.")
-            return None # I don't know how to deal with binary
+            return None  # I don't know how to deal with binary
 
         try:  # pragma: no cover
             payload = payload.decode("utf-8")
@@ -120,7 +122,7 @@ def onMessage(self, payload, isBinary):
             caller_id = message.get("caller_id", None)
 
             if caller_id is not None and caller_id in self.deferred_asks:
-                d = self.deferred_asks[caller_id] # type: Deferred
+                d = self.deferred_asks[caller_id]  # type: Deferred
                 d.callback(message.get("result"))
                 del self.deferred_asks[caller_id]
             else:
@@ -150,5 +152,3 @@ def onMessage(self, payload, isBinary):
         else:
             warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
             pass
-
-
diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index b8b9b40..2fcb945 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -93,7 +93,7 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
                     f"Received {original_thing} but expected callable|Object|twisted.web.resource.Resource")
 
             # return whatever was decorated unchanged
-            # the Resource.getChildForRequest is completely shortcircuited so
+            # the Resource.getChildForRequest is completely short circuited so
             # that a viewable class could be inherited in userland
             return original_thing
 
@@ -171,7 +171,7 @@ def add_directory(self, route_str: str, directory_resource: File) -> File:
 
         return directory_resource
 
-    def _build_map(self, pathEl, request):
+    def _build_map(self, path_element, request):
 
         map_bind_kwargs = {}
 
@@ -183,7 +183,7 @@ def _build_map(self, pathEl, request):
             map_bind_kwargs["server_name"] = request.getRequestHostname()
 
         if self._script_name is None:
-            map_bind_kwargs["script_name"] = b"/"  # b"/".join(request.prepath) if request.prepath else b"/"
+            map_bind_kwargs["script_name"] = b"/"
         else:
             map_bind_kwargs["script_name"] = self._script_name
 
@@ -199,18 +199,18 @@ def _build_map(self, pathEl, request):
 
         return self._route_map.bind(**map_bind_kwargs)
 
-    def getChildWithDefault(self, pathEl: T.Union[bytes, str], request: StrRequest):
+    def getChildWithDefault(self, path_element: T.Union[bytes, str], request: StrRequest):
         """
             Routing resource is mostly ignorant of the larger ecosystem so it either
             returns a resource OR it throws up an errors.HTTPCode
         """
 
-        map = self._build_map(pathEl, request)
+        routing = self._build_map(path_element, request)
 
         try:
             # TODO refactor to handle HEAD requests when the only valid match support GET
             # - one bad idea is to hack on werkzeug to append the URI matching rule to MethodNotAllowed
-            (rule, kwargs) = map.match(return_rule=True)
+            (rule, kwargs) = routing.match(return_rule=True)
         except wz_routing.RequestRedirect as redirect:
             log.debug(f"Werkzeug threw a redirect")
             raise HTTP_Errors.HTTP3xx(redirect.code, redirect.new_url, redirect.name)
@@ -230,4 +230,3 @@ def getChildWithDefault(self, pathEl: T.Union[bytes, str], request: StrRequest):
             # Intended to help with nested Directory resources
             request.postpath = [el.encode("utf-8") for el in kwargs['postpath']]
         return self._endpoints[rule.endpoint]
-

From f746b1105867bd799f27e5335532157c3096d4b5 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 13:44:01 -0700
Subject: [PATCH 131/185] PEP 8

---
 txweb/resources/view_class.py    |  10 +--
 txweb/resources/view_function.py |   2 +-
 txweb/util/basic.py              |   4 +
 txweb/util/reloader.py           | 125 ++++++++++++++++++-------------
 4 files changed, 82 insertions(+), 59 deletions(-)

diff --git a/txweb/resources/view_class.py b/txweb/resources/view_class.py
index f9ed924..cfcbb47 100644
--- a/txweb/resources/view_class.py
+++ b/txweb/resources/view_class.py
@@ -8,7 +8,8 @@
 if T.TYPE_CHECKING:  # pragma: no cover
     from txweb.lib.str_request import StrRequest
 
-NotDoneYet = T.TypeVar(int)
+NotDoneYet = int
+
 
 class ViewClassResource(resource.Resource):
 
@@ -19,7 +20,7 @@ def __init__(self, kls_view, instance=None):
         self.kls_view = kls_view
         self.instance = instance
 
-    def render(self, request:StrRequest) -> T.Union[bytes, NotDoneYet]:
+    def render(self, request: StrRequest) -> T.Union[bytes, NotDoneYet]:
         """
             Relays the request to the wrapped class instance with some caveats.
 
@@ -67,7 +68,6 @@ def render(self, request:StrRequest) -> T.Union[bytes, NotDoneYet]:
 
         return sanitize_render_output(result)
 
-
-    def __repr__(self): # pragma: no cover
+    def __repr__(self):  # pragma: no cover
         instance_repr = f"<{self.instance.__class__.__name__} {self.instance!r}/>"
-        return f"<{self.__class__.__name__} at {id(self)!r} instance={instance_repr}/>"
\ No newline at end of file
+        return f"<{self.__class__.__name__} at {id(self)!r} instance={instance_repr}/>"
diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py
index 8cf5ed3..cfd12a3 100644
--- a/txweb/resources/view_function.py
+++ b/txweb/resources/view_function.py
@@ -8,7 +8,7 @@
 
 # Prefilter takes StrRequest as first argument and second argument is the actual wrapped view function
 PrefilterFunc = T.NewType("PrefilterFunc", T.Callable[["StrRequest"], None])
-# Postfilter takes StrRequest, wrapped view func, and the output of the view func
+# Post-filter takes StrRequest, wrapped view func, and the output of the view func
 PostFilterFunc = T.NewType("PostFilterFunc", T.Callable[["StrRequest", T.Union[str, bytes]], T.Union[str, bytes]])
 
 
diff --git a/txweb/util/basic.py b/txweb/util/basic.py
index b8f9fda..0da679a 100644
--- a/txweb/util/basic.py
+++ b/txweb/util/basic.py
@@ -1,3 +1,7 @@
+"""
+    Basic and common utility functions
+
+"""
 import inspect
 import typing as T
 
diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index 6a883b1..75b90e0 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -30,6 +30,7 @@ def main():
     recopied from https://gist.github.com/devdave/05de2ed2fa2aa0a09ba931db36314e3e
 
 """
+import typing as T
 import pathlib
 import os
 import sys
@@ -48,37 +49,35 @@ def main():
     except ImportError:
         try:
             import dummy_thread as thread
-        except ImportError:
-            try:
-                import _dummy_thread as thread
-            except ImportError:
-                print("Alright... so I tried importing thread, that failed, so I tried _thread, that failed too")
-                print("..so then I tried dummy_thread, then _dummy_thread.  All failed")
-                print(", at this point I am out of ideas here")
-                raise RuntimeError("Failed to import threading library")
-                sys.exit(-1)
+        except ImportError as missing_import:
+            print("Alright... so I tried importing thread, that failed, so I tried _thread, that failed too")
+            print("..so then I tried dummy_thread, then _dummy_thread.  All failed")
+            print(", at this point I am out of ideas here")
+            raise RuntimeError("Failed to import threading library") from missing_import
+
 
 RUN_RELOADER = True
 SENTINEL_CODE = 7211
 SENTINEL_NAME = "RELOADER_ACTIVE"
 SENTINEL_OS_EXIT = True
 
+"""
+   "Reason" is here https://code.djangoproject.com/ticket/2330
+   TODO - Figure out why threading needs to be imported as this feels like a problem within stdlib.
+"""
 try:
-    """
-       "Reason" is here https://code.djangoproject.com/ticket/2330
-
-       TODO - Figure out why threading needs to be imported as this feels like a problem 
-       within stdlib.
-    """
     import threading
 except ImportError:
     pass
 
-_watch_list = {}
+WATCH_LIST = {}
 _win = (sys.platform == "win32")
 
 
-def build_list(root_dir, watch_self=False, ignore_prefix=None):
+def build_list(
+        root_dir: T.Union[pathlib.Path, str],
+        watch_self: bool = False,
+        ignore_prefix: T.Optional[T.List[str]] = None):
     """
         Walk from root_dir down, collecting all files that end with ^*.py$ to watch
 
@@ -91,14 +90,15 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
     :return: None
     """
 
-    global _watch_list
+    global WATCH_LIST
 
     if watch_self is True:
         import txweb
         log.info("RELOADER: Watching self")
         build_list(pathlib.Path(txweb.__file__).parent.absolute(), ignore_prefix=ignore_prefix)
 
-    is_list = lambda obj: obj is not None and isinstance(obj, list)
+    def is_list(obj):
+        return obj is not None and isinstance(obj, list)
 
     for pathobj in root_dir.iterdir():
         if pathobj.is_dir():
@@ -106,7 +106,7 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
         elif pathobj.name.endswith(".py") and not (pathobj.name.endswith(".pyc") or pathobj.name.endswith(".pyo")):
             stat = pathobj.stat()
             if is_list(ignore_prefix) and any([pathobj.name.startswith(prefix) for prefix in ignore_prefix]) is False:
-                _watch_list[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,)
+                WATCH_LIST[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,)
             else:
                 # print("Ignoring", pathobj.name)
                 pass
@@ -114,15 +114,20 @@ def build_list(root_dir, watch_self=False, ignore_prefix=None):
             pass
 
 
-def file_changed():
-    global _watch_list
+def file_changed() -> bool:
+    """
+        Scans the watched list of files for change in size, created & modified timestamps
+    :return:
+    """
+    global WATCH_LIST
     change_detected = False
-    for pathname, (st_size, st_ctime, st_mtime) in _watch_list.items():
+    for pathname, (st_size, st_ctime, st_mtime) in WATCH_LIST.items():
         pathobj = pathlib.Path(pathname)
         stat = pathobj.stat()
         if pathobj.exists() is False:
             raise Exception(f"Lost track of {pathname!r}")
-        elif stat.st_size != st_size:
+
+        if stat.st_size != st_size:
             change_detected = True
         elif stat.st_ctime != st_ctime:
             change_detected = True
@@ -136,7 +141,14 @@ def file_changed():
     return change_detected
 
 
-def watch_thread(os_exit=SENTINEL_OS_EXIT, watch_self=False, ignore_prefix=None):
+def watch_thread(os_exit: bool = SENTINEL_OS_EXIT, watch_self: bool = False, ignore_prefix=None):
+    """
+
+    :param os_exit: flag to decide whether to use sys.exit or os._exit
+    :param watch_self: Should reloader watch its own source code
+    :param ignore_prefix: Ignore files starting with this prefix
+    :return:
+    """
     exit_func = os._exit if os_exit is True else sys.exit
 
     build_list(pathlib.Path(os.getcwd()), watch_self=watch_self, ignore_prefix=ignore_prefix)
@@ -149,34 +161,49 @@ def watch_thread(os_exit=SENTINEL_OS_EXIT, watch_self=False, ignore_prefix=None)
 
 
 def run_reloader():
+    """
+        Respawns the user's program/script and hangs until it returns.
+
+    :return:
+    """
+    args = [sys.executable] + sys.argv
+    if _win:
+        args = ['"%s"' % arg for arg in args]
+
+    new_env = os.environ.copy()
+    new_env[SENTINEL_NAME] = "true"
+    log.info("Starting reloader process")
+
     while True:
-        args = [sys.executable] + sys.argv
-        if _win:
-            args = ['"%s"' % arg for arg in args]
-
-        new_env = os.environ.copy()
-        new_env[SENTINEL_NAME] = "true"
-        log.info("Starting reloader process")
-        # print("Running reloader process")
         exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_env)
         if exit_code != SENTINEL_CODE:
             return exit_code
 
 
-def reloader_main(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs):
+def reloader_main(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs) -> None:
     """
 
-    :param main_func:
-    :param args:
-    :param kwargs:
+        Right...  This checks to see if it is running as a child process with the sentinel flag set in its
+        process environment and if it isn't, it spawns a child process that run's the user's provided entry point
+        "main function".
+
+        Meanwhile in the child process, a watcher thread scans the user application source code files for changes.
+
+    :param main_func:  The entry point to the user application
+    :param watch_self: Should reloader also watch it's own source code for changes?
+    :param args: arguments for the main_func
+    :param ignore_prefix: Files to ignore by their prefix/starts with.
+    :param kwargs: keyword arguments for the main func
     :return:
     """
 
-
     # If it is, start watcher thread and then run the main_func in the parent process as thread 0
     if os.environ.get(SENTINEL_NAME) == "true":
 
-        thread.start_new_thread(watch_thread, (), {"os_exit": SENTINEL_OS_EXIT, "watch_self": watch_self, "ignore_prefix": ignore_prefix})
+        thread.start_new_thread(
+            watch_thread,
+            (),
+            {"os_exit": SENTINEL_OS_EXIT, "watch_self": watch_self, "ignore_prefix": ignore_prefix})
         try:
             main_func(*args, **kwargs)
         except KeyboardInterrupt:
@@ -193,13 +220,16 @@ def reloader_main(main_func, *args, watch_self=False, ignore_prefix=None, **kwar
 
 def reloader(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs):
     """
-        To avoid messing with twisted as much as possible, the watcher logic is shunted into
-        a thread while the main (twisted) reactor runs in the main thread.
+        See Reloader main for how this works.
+
+        WARNING - This assumes that the cwd (current working directory) is the base directory
+        of the project to be watched.
 
     :param main_func: The function to run in the main/primary thread
     :param args: list of arguments
+    :param watch_self: Should reloader also watch it's own src code for changes?
+    :param ignore_prefix: Files to ignore based on their prefix (eg test_ files)
     :param kwargs: dictionary of arguments
-    :param more_options: var trash currently
     :return: None
     """
     if args is None:
@@ -208,14 +238,3 @@ def reloader(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs):
         kwargs = {}
 
     reloader_main(main_func, *args, watch_self=watch_self, ignore_prefix=ignore_prefix, **kwargs)
-
-
-"""
-
-def main():
-  #startup twisted here
-
-if __name__ == "__main__":
-  reloader(main)
-
-"""
\ No newline at end of file

From fa0faf61e850dd833175451db27ab419cbb6bfb3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:34:25 -0700
Subject: [PATCH 132/185] Work on annotating/inline documenting code

---
 txweb/application.py | 187 ++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 174 insertions(+), 13 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 4960eeb..47036fc 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -65,7 +65,7 @@
 ErrorHandler = T.NewType("ErrorHandler", T.Callable[[StrRequest, failure.Failure], T.Union[bool, None]])
 
 
-class ApplicationWebsocketMixin(object):
+class ApplicationWebsocketMixin:
 
     WS_EXPOSED_FUNC = "WS_EXPOSED_FUNC"
 
@@ -77,6 +77,13 @@ def __init__(self, *args, **kwargs):
         self.ws_instances = {}
 
     def enable_websockets(self, url, route):
+        """
+            Setup websocket support for txweb
+
+        :param url:
+        :param route:
+        :return:
+        """
         if AUTOBAHN_MISSING is True:
             raise EnvironmentError("Unable to provide websocket support without autobahn installed/present")
 
@@ -85,6 +92,19 @@ def enable_websockets(self, url, route):
         self.add_resource(route, self.ws_resource)
 
     def ws_add(self, name, assign_args=False) -> T.Callable[[WSEndpoint], WSEndpoint]:
+        """
+        Add a new endpoint for use with the connected websocket.
+
+        Example usage
+        ```
+            @ws_add
+            def my_websocket_endpoint(message):
+                ...
+        ```
+        :param name:
+        :param assign_args:
+        :return:
+        """
 
         def processor(func: WSEndpoint) -> WSEndpoint:
 
@@ -98,10 +118,31 @@ def processor(func: WSEndpoint) -> WSEndpoint:
 
     # noinspection SpellCheckingInspection
     def ws_sharelib(self, route_str="/lib"):
+        """
+            Adds utility libraries to the provided route str for use by client facing html javascript.
+        :param route_str:
+        :return:
+        """
         self.add_staticdir2(route_str, WS_STATIC_LIB)
 
     def ws_class(self, kls=None, name: str = None):
-
+        """
+        Add an entire class of @ws_expose'd class methods as endpoints for the websocket.
+
+        Example
+        ```
+            @app.ws_class
+            class Foo:
+                @app.expose
+                def bar(message):
+                    ...
+        ```
+            That will create a "foo.bar" endpoint
+
+        :param kls:
+        :param name: Override the default behavior of using the class.__name__ property for the base endpoint.
+        :return:
+        """
 
         def processor(kls, name = None):
             kls_name = name if name is not None else kls.__name__.lower()
@@ -132,6 +173,11 @@ def processor(kls, name = None):
 
     @staticmethod
     def websocket_class_arguments_decorator(func):
+        """
+            Internal method not intended for users
+        :param func:
+        :return:
+        """
         params = inspect.signature(func).parameters
         arg_keys = {}
         converter_keys = {}
@@ -174,6 +220,11 @@ def method_argument_decorator(parent, message):
 
     @staticmethod
     def websocket_function_arguments_decorator(func):
+        """
+            Internal method not intended for users
+        :param func:
+        :return:
+        """
         params = inspect.signature(func).parameters
         arg_keys = {}
         converter_keys= {}
@@ -215,6 +266,12 @@ def argument_decorator(message: MessageHandler):
 
 
     def ws_expose(self, func: callable = None, assign_args=False):
+        """
+        See ws_class for use
+        :param func:
+        :param assign_args:
+        :return:
+        """
 
         if func is None and assign_args is True:
             def processor(real_func):
@@ -228,7 +285,7 @@ def processor(real_func):
             return func
 
 
-class ApplicationRoutingHelperMixin(object):
+class ApplicationRoutingHelperMixin:
     """
         Provides a wrapping interface around :ref: `RoutingResource`
 
@@ -248,10 +305,63 @@ def add(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResour
     route = add
 
     def add_class(self, route_str:str, **kwargs: ArbitraryKWArguments) ->CallableToResourceDecorator:
+        """
+
+        example usage
+        ```
+        @app.add_class("/my_foo")
+        class Foo:
+            @app.expose("/bar")
+            def some_endpoint(self, request):
+                ...
+
+        ```
+        would connect an instance of Foo and it's method `some_endpoint` to the url `/my_foo/bar`
+
+
+        :param route_str:
+        :param kwargs:
+        :return:
+        """
         return self.router.add(route_str, **kwargs)
 
 
+    def expose(self, route_str, **kwargs):
+        """
+            Refer to add_class for usage
+        :param route_str:
+        :param kwargs:
+        :return:
+        """
+
+        return expose_method(route_str, **kwargs)
+
+    def set_view_prefilter(self, func):
+        """
+            Experimental, sets a view class method to be called before any `expose`'d method.
+        :param func:
+        :return:
+        """
+        return set_prefilter(func)
+
+    def set_view_postfilter(self, func):
+        """
+            Experimental, sets a view class method to be called after any `expose`'d method.
+        :param func:
+        :return:
+        """
+        return set_postfilter(func)
+
+
     def add_resource(self, route_str:str, resource, **kwargs):
+        """
+        Add's a native/vanilla twisted.web.Resource object to the provided route_str
+
+        :param route_str:
+        :param resource:
+        :param kwargs:
+        :return:
+        """
         return self.router._add_resource(route_str, thing=resource, route_kwargs=kwargs )
 
 
@@ -271,6 +381,13 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Si
 
 
     def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path], recurse = False) -> File:
+        """
+        TODO - remove calls to `add_staticdir2` and just use `add_staticdir`
+        :param route_str:
+        :param dirPath:
+        :param recurse:
+        :return:
+        """
 
         if route_str.endswith("/") is False:
             route_str += "/"
@@ -284,18 +401,10 @@ def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path], recurse =
     add_staticdir = add_staticdir2
 
 
-    def expose(self, route_str, **kwargs):
-        # TODO make route_str optional somehow
-        return expose_method(route_str, **kwargs)
 
-    def set_view_prefilter(self, func):
-        return set_prefilter(func)
-
-    def set_view_postfilter(self, func):
-        return set_postfilter(func)
 
 
-class ApplicationErrorHandlingMixin(object):
+class ApplicationErrorHandlingMixin:
     """
     The Error processing and handling aspect of Application
 
@@ -341,6 +450,15 @@ def processor(func: ErrorHandler) -> ErrorHandler:
         return processor
 
     def add_error_handler(self, handler:T.Callable, error_type: T.Union[HTTPCode, int, Exception, str], override=False):
+        """
+        TODO cull this out or justify it's existence.
+
+        Used similar to @handle_error but instead the first argument is a callable/reference to a function.
+        :param handler:
+        :param error_type:
+        :param override:
+        :return:
+        """
 
         if error_type in self.error_handlers and override is False:
             old_func = self.error_handlers[error_type]
@@ -349,6 +467,17 @@ def add_error_handler(self, handler:T.Callable, error_type: T.Union[HTTPCode, in
         self.error_handlers[error_type] = handler
 
     def processingFailed(self, request:StrRequest, reason: failure.Failure):
+        """
+        Internal method
+
+        Called as part of the pipeline started when an exception occurs at Request.render and bubbles its
+        way up to this part.
+
+
+        :param request:
+        :param reason:
+        :return:
+        """
 
         default_handler = self.error_handlers['default']
 
@@ -375,7 +504,11 @@ def processingFailed(self, request:StrRequest, reason: failure.Failure):
 
 
 class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin, ApplicationWebsocketMixin):
+    """
+        Grand unified god module of txweb.
 
+        TODO rename to TXWeb versus application to avoid confusion.
+    """
 
     NOT_DONE_YET = NOT_DONE_YET
 
@@ -444,14 +577,26 @@ def partial(*args, **kwargs):
 
     @property
     def router(self) -> RoutingResource:
+        """
+        Provide access to the routing resource object.
+        :return:
+        """
         return self._router
 
     @property
     def site(self) -> WebSite:
+        """
+        Provides access to the server.Site instance
+        :return:
+        """
         return self._site
 
     @property
     def reactor(self) -> PosixReactorBase:
+        """
+        Provides access to the currently used reactor, used specifically to make testing easier.
+        :return:
+        """
         return self._reactor
 
     @reactor.setter
@@ -485,6 +630,14 @@ def after_render(self, func: T.Callable[[StrRequest], None]):
         return func
 
     def _call_before_render(self, request: StrRequest):
+        """
+        Internal method that iterates over all appended pre filter functions
+
+        TODO - add logic to abort the loop if a post filter returns anything besides None
+
+        :param request:
+        :return:
+        """
         for func in self._before_render_handlers:
             try:
                 func(request)
@@ -492,12 +645,20 @@ def _call_before_render(self, request: StrRequest):
                 log.error(f"Before render failed {func}")
 
     def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]):
+        """
+            Internal method that iterates over all post filters.
+
+        :param request:
+        :param body:
+        :return:
+        """
         for func in self._after_render_handlers:
             try:
-                func(request)
+                body = func(request, body)
             except Exception:
                 log.error(f"After render failed {func}")
 
+        return body
 
 
 

From dabad03612adbd1b4b7f7d467668b82bed2059f3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:34:50 -0700
Subject: [PATCH 133/185] Current pylint behavior being used to make code
 kosher

---
 lint.bat | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 lint.bat

diff --git a/lint.bat b/lint.bat
new file mode 100644
index 0000000..c172d42
--- /dev/null
+++ b/lint.bat
@@ -0,0 +1 @@
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb 

From f1f7e86263fd8eb4cec96f8408988d3cbc69ecfd Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:35:30 -0700
Subject: [PATCH 134/185] PEP8 and documentation

---
 txweb/resources/routing.py | 96 ++++++++++++++++++++++++++++++++++----
 1 file changed, 86 insertions(+), 10 deletions(-)

diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 2fcb945..abe9f32 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -1,5 +1,17 @@
+from collections import OrderedDict
+import typing as T
+import inspect
+import warnings
+
 from twisted.web.static import File
 
+from twisted.web import resource
+from twisted.python import compat
+
+# Werkzeug routing import
+from werkzeug import routing as wz_routing
+
+
 from txweb.http_codes import UnrenderableException
 from txweb.util.url_converter import DirectoryPath
 from txweb.util.basic import get_thing_name
@@ -12,18 +24,10 @@
 from ..lib import view_class_assembler as vca
 from txweb import http_codes as HTTP_Errors
 
-from twisted.web import resource
-from twisted.python import compat
-
-# Werkzeug routing import
-from werkzeug import routing as wz_routing
 
 from ..log import getLogger
 
-from collections import OrderedDict
-import typing as T
-import inspect
-import warnings
+
 
 log = getLogger(__name__)
 
@@ -36,8 +40,17 @@
 
 
 class RoutingResource(resource.Resource):
+    """
+        The bridge between twisted's object graph routing system and werkzeug's url pattern
+        recognition routing system.
+    """
 
     def __init__(self, script_name: bytes = None):
+        """
+
+        :param script_name: Optional ability to add a prefix to all routed URL's in case this application
+            is nested inside another web app.   See CGI's SCRIPT_NAME variable.
+        """
 
         resource.Resource.__init__(self)
 
@@ -50,16 +63,36 @@ def __init__(self, script_name: bytes = None):
 
     @property
     def site(self):   # pragma: no cover
+        """
+            Return a reference to the parent site of this resource
+        :return:
+        """
         return self._site
 
     @site.setter
     def site(self, site):  # pragma: no cover
+        """
+        TODO - Verify this is still being used.
+        :param site:
+        :return:
+        """
         self._site = site
 
     def iter_rules(self) -> T.Generator:
+        """
+        Debug method for iterating over all of the currently set URL Rule's for the werkzeug routing map.
+        :return:
+        """
         return self._route_map.iter_rules()
 
     def add(self, route_str: str, **kwargs: T.Dict[str, T.Any]):
+        """
+            Possibly super overloaded
+
+        :param route_str:
+        :param kwargs:
+        :return:
+        """
 
         assert "endpoint" not in kwargs, \
             "Undefined behavior to use RoutingResource.add('/some/route/', endpoint='something', ...)"
@@ -72,11 +105,15 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
 
             common_kwargs = {"endpoint": endpoint_name, "thing": original_thing, "route_kwargs": kwargs}
 
+            """
+                Is the thing to be added to the router a twisted Resource class?
+            """
             if inspect.isclass(original_thing) and issubclass(original_thing, resource.Resource):
 
                 self._add_resource_cls(route_str, **common_kwargs)
 
             elif isinstance(original_thing, resource.Resource):
+
                 self._add_resource(route_str, **common_kwargs)
 
             elif inspect.isclass(original_thing):
@@ -122,6 +159,9 @@ def _add_class(self, route_str: T.AnyStr,
                    endpoint: T.AnyStr = None,
                    thing: T.Union[object, T.Callable] = None,
                    route_kwargs: T.Dict[str, T.Any] = None):
+        """
+            A view class has been provided, decorate and process it into the router.
+        """
 
         if vca.is_renderable(thing) is False:
             raise UnrenderableException(f"{thing.__name__!r} is missing exposed methods or a render method")
@@ -137,12 +177,32 @@ def _add_class(self, route_str: T.AnyStr,
             self._endpoints[endpoint] = ViewClassResource(thing, instance)
 
     def _add_resource_cls(self, route_str, endpoint=None, thing=None, route_kwargs=None):
+        """
+            Give the class definition of a resource, instantiate it, add it to the instances list, and then
+            add the instance to the routing map.
+
+        :param self:
+        :param route_str:
+        :param endpoint:
+        :param thing:
+        :param route_kwargs:
+        :return:
+        """
         route_kwargs = route_kwargs if route_kwargs is not None else {}
         if endpoint not in self._instances:
             self._instances[endpoint] = thing()
         self._add_resource(route_str, endpoint=endpoint, thing=self._instances[endpoint], route_kwargs=route_kwargs)
 
     def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None):
+        """
+
+        :param self:
+        :param route_str:
+        :param endpoint:
+        :param thing:
+        :param route_kwargs:
+        :return:
+        """
         route_kwargs = route_kwargs if route_kwargs is not None else {}
         endpoint = endpoint or get_thing_name(thing)
 
@@ -152,6 +212,13 @@ def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None)
         self._route_map.add(new_rule)
 
     def add_directory(self, route_str: str, directory_resource: File) -> File:
+        """
+            TODO refactor.   Since I've dropped my own custom file and directory resources
+             this method isn't as relevant.
+        :param route_str:
+        :param directory_resource:
+        :return:
+        """
 
         endpoint = repr(directory_resource)
         if endpoint not in self._endpoints:
@@ -172,6 +239,16 @@ def add_directory(self, route_str: str, directory_resource: File) -> File:
         return directory_resource
 
     def _build_map(self, path_element, request):
+        """
+            Takes all of the information provided by the request object and adapts them to match the wsgi environment
+            dictionary so that werkzeug can provide a routing map.
+
+            It feels unfortunate/excessive that I have to build this map on each request instead of just reporting
+            the changes in URL.
+        :param path_element:
+        :param request:
+        :return:
+        """
 
         map_bind_kwargs = {}
 
@@ -187,7 +264,6 @@ def _build_map(self, path_element, request):
         else:
             map_bind_kwargs["script_name"] = self._script_name
 
-        # TODO add strict slash check flag to here or to website.add
         if map_bind_kwargs["script_name"].startswith(b"/") is False:
             map_bind_kwargs["script_name"] = b"/" + map_bind_kwargs["script_name"]
 

From afa1d501d62cd13b9b171e1c93530a824226a0e7 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:35:41 -0700
Subject: [PATCH 135/185] pep 8

---
 txweb/lib/errors/handler.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
index 48774e7..6b436da 100644
--- a/txweb/lib/errors/handler.py
+++ b/txweb/lib/errors/handler.py
@@ -20,14 +20,14 @@
 
 
 @dataclass
-class FormattedFrame(object):
+class FormattedFrame:
     name: bytes
     file: Path
     line_no: int
     line: bytes
 
-
-class StackFrame(T.NamedTuple):
+@dataclass(frozen=True, unsafe_hash=True)
+class StackFrame:
     funcName: str
     fileName: str
     lineNumber: int
@@ -38,7 +38,7 @@ class StackFrame(T.NamedTuple):
 log = getLogger(__name__)
 
 
-class BaseHandler(object):
+class BaseHandler:
     """
         TODO - Look into using twisted/zope's interface system
 

From f950f329b365118483a0f8e0437635cd7e920d7a Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:35:48 -0700
Subject: [PATCH 136/185] pep8

---
 txweb/util/reloader.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index 75b90e0..01ca647 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -36,6 +36,7 @@ def main():
 import sys
 import time
 
+import txweb
 from logging import getLogger
 
 log = getLogger(__name__)
@@ -93,7 +94,6 @@ def build_list(
     global WATCH_LIST
 
     if watch_self is True:
-        import txweb
         log.info("RELOADER: Watching self")
         build_list(pathlib.Path(txweb.__file__).parent.absolute(), ignore_prefix=ignore_prefix)
 

From 4e8c1b9c0b195070e4ff8891cdaa6e9dc2154217 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:35:57 -0700
Subject: [PATCH 137/185] pep8

---
 txweb/util/url_converter.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/util/url_converter.py b/txweb/util/url_converter.py
index f8876ac..9772eb2 100644
--- a/txweb/util/url_converter.py
+++ b/txweb/util/url_converter.py
@@ -10,7 +10,7 @@ class DirectoryPath(BaseConverter): # pragma: no cover
     regex = r"(.*)" #  Got to catch them all
 
     def __init__(self, url_map):
-        super(DirectoryPath, self).__init__(url_map)
+        super().__init__(url_map)
 
     def to_python(self, value):
         return value.split("/")

From dcf4170fdd72b7582f489d215ca7c16c03c5b3e3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:36:03 -0700
Subject: [PATCH 138/185] pep8

---
 txweb/web_views.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/web_views.py b/txweb/web_views.py
index e92cba8..6a1f223 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -82,7 +82,7 @@ def expose(self, route_str, **route_kwargs) -> T.Callable:
         return vca.expose(route_str, **route_kwargs)
 
 
-class WebSite(_RoutingSiteConnectors, object):
+class WebSite(_RoutingSiteConnectors):
     """
         Public side of the web_views class collection.
 

From 95f4798154b5940a1149a9747c637447af19707e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:53:23 -0700
Subject: [PATCH 139/185] Pep8

---
 txweb/log.py       |  8 ++++++++
 txweb/web_views.py | 25 ++++++++++---------------
 2 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/txweb/log.py b/txweb/log.py
index 518f474..61d68d3 100644
--- a/txweb/log.py
+++ b/txweb/log.py
@@ -1,7 +1,15 @@
+"""
+    Currently just a stupid bridge between twisted.logger and user applications
 
+"""
 # from logging import getLogger as stdlib_getLogger, Logger
 from twisted import logger
 
 
 def getLogger(namespace: str = None) -> logger.Logger:
+    """
+        Just an adapted to mimic python's logging getLogger to twisted's Logger()
+    :param namespace:
+    :return:
+    """
     return logger.Logger(namespace) if namespace else logger.Logger()
\ No newline at end of file
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 6a1f223..2eb997b 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -1,14 +1,18 @@
+"""
+    Currently acts as a bridge for error handling.
+
+"""
+
 from __future__ import annotations
 
 #stdlib
+import typing as T
 import pathlib
 import copy
 
-#Third party
-############
-# TODO remove this as a hardwired requirement?
-import jinja2
-
+# twisted imports
+from twisted.python import failure
+from twisted.web import server
 
 # txweb imports
 import txweb
@@ -20,18 +24,9 @@
 from txweb.log import getLogger
 
 
-
-
-# twisted imports
-from twisted.python import failure
-from twisted.web import server
-
-
-
-
 log = getLogger(__name__)
 
-import typing as T
+
 if T.TYPE_CHECKING: # pragma: no cover
     # No executable intended for type hints only
     import pathlib

From 163dfd316dfa7271dfc2fe48209fb4566032af6f Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 14:54:06 -0700
Subject: [PATCH 140/185] Murdered off and simplified WebSite class as it
 doesn't need add and expose routing methods/helpers

---
 txweb/tests/test_routable_classes.py | 166 +++++++++++++--------------
 txweb/tests/test_views.py            | 130 ++++++++++-----------
 txweb/web_views.py                   | 112 +++++-------------
 3 files changed, 175 insertions(+), 233 deletions(-)

diff --git a/txweb/tests/test_routable_classes.py b/txweb/tests/test_routable_classes.py
index 38fa27a..5a4b390 100644
--- a/txweb/tests/test_routable_classes.py
+++ b/txweb/tests/test_routable_classes.py
@@ -1,83 +1,83 @@
-
-import pytest
-
-from twisted.web.resource import NoResource
-
-from txweb.web_views import WebSite
-from txweb.http_codes import UnrenderableException
-from .helper import MockRequest
-
-def test_basic_idea():
-
-    app = WebSite()
-
-    @app.add("/nexus")
-    class PersistentObject(object):
-
-        def __init__(self):
-            self._number = 0
-
-
-        @app.expose("/number")
-        def respond_number(self, request):
-            return 1234
-
-        @app.expose("/greeting")
-        def render_response_says_hello(self, request):
-            return "Hello"
-
-
-        @app.expose("/add_one")
-        def adds_to_passed_get_argument(self, request):
-            """
-                subviews do not need to start with render_
-            """
-            input = int(request.args[b'number'][0])
-
-            return input + 1
-
-        @app.expose("/counter")
-        def increments_persistant_value(self, request):
-            self._number += 1
-            return self._number
-
-
-    assert len(app.resource._route_map._rules) == 4
-
-    number_request = MockRequest([], "/nexus/number")
-    number_resource = app.getResourceFor(number_request)
-
-    assert isinstance(number_resource, NoResource) is False
-    expected = b"1234"
-    actual = number_resource.render(number_request)
-    assert actual == expected
-
-    add_request = MockRequest([], "/nexus/add_one", {b"number":5})
-    resource = app.getResourceFor(add_request)
-    expected = b"6"
-    actual = resource.render(add_request)
-    assert actual == expected
-
-    incrementer = MockRequest([], "/nexus/counter")
-    assert app.getResourceFor(incrementer).render(incrementer) == 1 #This is a bug because NOT_DONE_YET =='s 1
-    assert app.getResourceFor(incrementer).render(incrementer) == b"2"
-    assert app.getResourceFor(incrementer).render(incrementer) == b"3"
-
-
-
-
-def test_throws_exception_on_inaccessible_view_class():
-
-
-    app = WebSite()
-
-    with pytest.raises(UnrenderableException):
-        @app.add("/base")
-        class Foo:
-            pass
-
-
-
-
-
-
+#
+# import pytest
+#
+# from twisted.web.resource import NoResource
+#
+# from txweb.web_views import WebSite
+# from txweb.http_codes import UnrenderableException
+# from .helper import MockRequest
+#
+# def test_basic_idea():
+#
+#     app = WebSite()
+#
+#     @app.add("/nexus")
+#     class PersistentObject(object):
+#
+#         def __init__(self):
+#             self._number = 0
+#
+#
+#         @app.expose("/number")
+#         def respond_number(self, request):
+#             return 1234
+#
+#         @app.expose("/greeting")
+#         def render_response_says_hello(self, request):
+#             return "Hello"
+#
+#
+#         @app.expose("/add_one")
+#         def adds_to_passed_get_argument(self, request):
+#             """
+#                 subviews do not need to start with render_
+#             """
+#             input = int(request.args[b'number'][0])
+#
+#             return input + 1
+#
+#         @app.expose("/counter")
+#         def increments_persistant_value(self, request):
+#             self._number += 1
+#             return self._number
+#
+#
+#     assert len(app.resource._route_map._rules) == 4
+#
+#     number_request = MockRequest([], "/nexus/number")
+#     number_resource = app.getResourceFor(number_request)
+#
+#     assert isinstance(number_resource, NoResource) is False
+#     expected = b"1234"
+#     actual = number_resource.render(number_request)
+#     assert actual == expected
+#
+#     add_request = MockRequest([], "/nexus/add_one", {b"number":5})
+#     resource = app.getResourceFor(add_request)
+#     expected = b"6"
+#     actual = resource.render(add_request)
+#     assert actual == expected
+#
+#     incrementer = MockRequest([], "/nexus/counter")
+#     assert app.getResourceFor(incrementer).render(incrementer) == 1 #This is a bug because NOT_DONE_YET =='s 1
+#     assert app.getResourceFor(incrementer).render(incrementer) == b"2"
+#     assert app.getResourceFor(incrementer).render(incrementer) == b"3"
+#
+#
+#
+#
+# def test_throws_exception_on_inaccessible_view_class():
+#
+#
+#     app = WebSite()
+#
+#     with pytest.raises(UnrenderableException):
+#         @app.add("/base")
+#         class Foo:
+#             pass
+#
+#
+#
+#
+#
+#
diff --git a/txweb/tests/test_views.py b/txweb/tests/test_views.py
index bb3b0e6..051a6e2 100644
--- a/txweb/tests/test_views.py
+++ b/txweb/tests/test_views.py
@@ -17,71 +17,71 @@
 #     return resource
 
 
-def test__website_add__works():
-
-    test_website = web_views.WebSite()
-    expected_number_value = 890
-    request = MockRequest([], f"/foo/bar/{expected_number_value}")
-
-    @test_website.add("/foo/bar/")
-    def stub(request, a_number):
-        assert expected_number_value == a_number
-        return a_number
-
-    rsrc = test_website.getResourceFor(request)
-
-    assert rsrc.func == stub
-
-
-def test_website_add__handles_native_resources():
-
-    test_website = web_views.WebSite()
-    expected_number_value = 890
-    request = MockRequest([], f"/foo/bar/{expected_number_value}")
-
-    @test_website.add("/foo/bar/")
-    class TestResource(tw_resource.Resource):
-        isLeaf = True
-
-        def render(self, request):
-            assert "number" in request.route_args
-            assert request.route_args["number"] == expected_number_value
-            return b"Rendered TestResource"
-
-    rsrc = test_website.getResourceFor(request)
-    assert isinstance(rsrc, TestResource)
-    assert rsrc.render(request) == b"Rendered TestResource"
-
-
-def test_website__adds_resource_class():
-
-    test_website = web_views.WebSite()
-
-    test_website.add("/foo")(tw_resource.NoResource)
-
-
-    request = MockRequest([], "/foo")
-    rsrc = test_website.getResourceFor(request)
-
-    assert isinstance(rsrc, tw_resource.NoResource)
-
-
-def test_website__able_to_access_routing_rules():
-
-    site = web_views.WebSite()
-
-    @site.add("/foo")
-    def foo_view(request):
-        pass
-
-    @site.add("/bar")
-    def bar_view(request):
-        pass
-
-    rules = list(site.resource.iter_rules())
-
-    assert len(rules) == 2
-    assert rules[0].rule == "/foo"
+# def test__website_add__works():
+#
+#     test_website = web_views.WebSite()
+#     expected_number_value = 890
+#     request = MockRequest([], f"/foo/bar/{expected_number_value}")
+#
+#     @test_website.add("/foo/bar/")
+#     def stub(request, a_number):
+#         assert expected_number_value == a_number
+#         return a_number
+#
+#     rsrc = test_website.getResourceFor(request)
+#
+#     assert rsrc.func == stub
+
+
+# def test_website_add__handles_native_resources():
+#
+#     test_website = web_views.WebSite()
+#     expected_number_value = 890
+#     request = MockRequest([], f"/foo/bar/{expected_number_value}")
+#
+#     @test_website.add("/foo/bar/")
+#     class TestResource(tw_resource.Resource):
+#         isLeaf = True
+#
+#         def render(self, request):
+#             assert "number" in request.route_args
+#             assert request.route_args["number"] == expected_number_value
+#             return b"Rendered TestResource"
+#
+#     rsrc = test_website.getResourceFor(request)
+#     assert isinstance(rsrc, TestResource)
+#     assert rsrc.render(request) == b"Rendered TestResource"
+
+
+# def test_website__adds_resource_class():
+#
+#     test_website = web_views.WebSite()
+#
+#     test_website.add("/foo")(tw_resource.NoResource)
+#
+#
+#     request = MockRequest([], "/foo")
+#     rsrc = test_website.getResourceFor(request)
+#
+#     assert isinstance(rsrc, tw_resource.NoResource)
+
+
+# def test_website__able_to_access_routing_rules():
+#
+#     site = web_views.WebSite()
+#
+#     @site.add("/foo")
+#     def foo_view(request):
+#         pass
+#
+#     @site.add("/bar")
+#     def bar_view(request):
+#         pass
+#
+#     rules = list(site.resource.iter_rules())
+#
+#     assert len(rules) == 2
+#     assert rules[0].rule == "/foo"
 
 
 
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 2eb997b..3c60b66 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -36,48 +36,32 @@
 
 LIBRARY_TEMPLATE_PATH = pathlib.Path(txweb.__file__).parent / "templates"
 
-class _RoutingSiteConnectors(server.Site):
-    """
-        Purpose: provide hooks to the RoutingResource assigned to self.resource
-    """
-    resource: RoutingResource
-
-    def add(self, route_str: str, **kwargs: T.Optional[T.Dict[str, T.Any]]) -> ResourceView:
-        """
-
-        :param route_str: A valid werkzeug routing url
-        :param kwargs: optional keyword arguments for werkzeug routing
-        :return:
-        """
-        return self.resource.add(route_str, **kwargs)
-
-    # def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> txw_resources.SimpleFile:
+# class _RoutingSiteConnectors(server.Site):
+#     """
+#         Purpose: provide hooks to the RoutingResource assigned to self.resource
+#     """
+#     resource: RoutingResource
+#
+    # def add(self, route_str: str, **kwargs: T.Optional[T.Dict[str, T.Any]]) -> ResourceView:
     #     """
-    #     Just a simple helper for a common task of serving individual files
-    #
-    #     :param route_str: A valid URI route string
-    #     :param filepath: An absolute or relative path to a file to be served over HTTP
-    #     :param default_type: What content type should a file be served as
-    #     :return: twisted.web.static.File
+    #         :param route_str: A valid werkzeug routing url
+    #         :param kwargs: optional keyword arguments for werkzeug routing
+    #         :return:
     #     """
-    #     return self.add_resource(route_str, txw_resources.SimpleFile(filePath, defaultType=defaultType))
-    #
-    # def add_directory(self, route_str: str, dirPath: T.Union[str, pathlib.Path]) -> txw_resources.Directory:
-    #     # TODO pull add_directory OUT of RoutingResource
-    #     return self.resource.add_directory(route_str, dirPath)
-
-
-
-    def add_resource(self, route_str: str,
-                     rsrc: resource.Resource,
-                     **kwargs: T.Dict[str, T.Any]) -> ResourceView:
-        return self.resource.add(route_str, **kwargs)(rsrc)
-
-    def expose(self, route_str, **route_kwargs) -> T.Callable:
-        return vca.expose(route_str, **route_kwargs)
-
-
-class WebSite(_RoutingSiteConnectors):
+    #     return self.resource.add(route_str, **kwargs)
+#
+#
+#
+#     def add_resource(self, route_str: str,
+#                      rsrc: resource.Resource,
+#                      **kwargs: T.Dict[str, T.Any]) -> ResourceView:
+#         return self.resource.add(route_str, **kwargs)(rsrc)
+#
+#     def expose(self, route_str, **route_kwargs) -> T.Callable:
+#         return vca.expose(route_str, **route_kwargs)
+
+
+class WebSite(server.Site):
     """
         Public side of the web_views class collection.
 
@@ -91,7 +75,8 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
         routing_resource = routing_resource or RoutingResource()
         request_factory = request_factory or StrRequest
 
-        _RoutingSiteConnectors.__init__(self, routing_resource, requestFactory=request_factory)
+        # _RoutingSiteConnectors.__init__(self, routing_resource, requestFactory=request_factory)
+        super().__init__(routing_resource, requestFactory=request_factory)
 
         self._errorHandler = siteErrorHandler
         self._lastError = None
@@ -99,6 +84,7 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
         # self._before_request_render = None
         # self._after_request_render = None
 
+
     def processingFailed(self, request: StrRequest, reason: failure.Failure):
 
         self._lastError = reason
@@ -114,48 +100,4 @@ def processingFailed(self, request: StrRequest, reason: failure.Failure):
     def setErrorHandler(self, func: ErrorHandler):
         self._errorHandler = func
         return func
-
-
-    # def call_before_request_render(self, func):
-    #     self._before_request_render = func
-    #     return func
-    #
-    # def after_resource_fetch(self, func):
-    #     self._after_request_render = func
-    #     return func
-    #
-    # def getResourceFor(self, request: StrRequest):
-    #     """
-    #     This is probably the least convoluted way to manipulate the
-    #     current http request.
-    #     """
-    #
-    #     if self._before_request_render is not None:
-    #         request.add_before_render(self._before_request_render)
-    #
-    #     if self._after_request_render is not None:
-    #         request.add_after_render(self._after_request_render)
-    #
-    #     return server.Site.getResourceFor(self, request)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 

From e761436adec2fdc0bb20af3f10377a50a71ce44e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 15:54:43 -0700
Subject: [PATCH 141/185] Update lint.bat

---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index c172d42..e5d739b 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb 
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From a5664349643ca6d01c73671dea6d96d84fc72f06 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 15:55:05 -0700
Subject: [PATCH 142/185] More documentation/annotations

---
 txweb/__init__.py            | 17 ++++++++++
 txweb/lib/message_handler.py | 37 +++++++++++++++++++++
 txweb/lib/routed_factory.py  | 14 ++++++++
 txweb/lib/str_request.py     | 64 ++++++++++++++++++++++++++++++++----
 txweb/web_views.py           | 13 ++++++++
 5 files changed, 138 insertions(+), 7 deletions(-)

diff --git a/txweb/__init__.py b/txweb/__init__.py
index 0b33444..5da66b4 100644
--- a/txweb/__init__.py
+++ b/txweb/__init__.py
@@ -1,4 +1,21 @@
+"""
+    Texas web (txweb).
 
+    ```python
+        app = Application(__name__)
+
+        @app.route("/foo")
+        def bar(request):
+            return "Hello World!"
+
+        app.listenTCP(7070)
+
+        reactor.run()
+    ```
+
+    curl http://127.0.0.1/foo
+    Hello World!
+"""
 from twisted.web.server import NOT_DONE_YET
 from txweb.application import Application
 
diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 862fbc1..0e27bae 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -1,3 +1,10 @@
+"""
+    A wrapper around the message sent from the client
+
+    Breaks ideal style guides as it also wraps around the connection object to make
+    responding and interacting with the client easier.
+
+"""
 from __future__ import annotations
 import typing as T
 
@@ -56,6 +63,14 @@ def get(self, key, default=None, type=None):
         return value
 
     def args(self, key, default=None, type=None):
+        """
+            A more explicit/direct getter that looks for an `args` dictionary in the client message and
+            if it exists, returns the requested key.
+        :param key:
+        :param default:
+        :param type:  What type to cast the arg value as.  (eg type=int would cast a str to int if possible)
+        :return:
+        """
 
         try:
             args = self['args']
@@ -78,13 +93,35 @@ def args(self, key, default=None, type=None):
         return value
 
     def respond(self, result):
+        """
+            If the message was an request/ask for response, send back a result.
+        :param result:
+        :return:
+        """
         return self.connection.respond(self.raw_message, result=result)
 
     def tell(self, endpoint, **kwargs):
+        """
+        Tell the client to do something if it provides the requested end point.
+        :param endpoint:
+        :param kwargs:
+        :return:
+        """
         return self.connection.tell(endpoint, **kwargs)
 
     def ask(self, endpoint, **kwargs):
+        """
+            Ask the client for information or acknowledgement of success/failure for an action.
+        :param endpoint:
+        :param kwargs:
+        :return:
+        """
         return self.connection.ask(endpoint, type="ask", args=kwargs)
 
     def get_session(self, get_key=None):
+        """
+            see WSProtocol's get_session
+        :param get_key:
+        :return:
+        """
         return self.connection.application.get_session(self.connection, get_key=get_key)
diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py
index 21fbb72..b61236b 100644
--- a/txweb/lib/routed_factory.py
+++ b/txweb/lib/routed_factory.py
@@ -1,3 +1,8 @@
+"""
+    Just a simple implementation of WebSocketServerFactory to provide a basic dict based router
+    for server side websocket endpoints.
+
+"""
 from autobahn.twisted.websocket import WebSocketServerFactory
 from .wsprotocol import WSProtocol
 
@@ -12,7 +17,16 @@ def __init__(self, url, routes, protocol_cls=WSProtocol, application=None):
         self._application = application
 
     def get_endpoint(self, name):
+        """
+
+        :param name:
+        :return:
+        """
         return self.routes.get(name, None)
 
     def get_application(self):
+        """
+
+        :return:
+        """
         return self._application
diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py
index 694c393..d964df7 100644
--- a/txweb/lib/str_request.py
+++ b/txweb/lib/str_request.py
@@ -58,6 +58,13 @@ def __init__(self, *args, **kwargs):
         self._call_after_render = None
 
     def getCookie(self, cookie_name: T.Union[str, bytes]):
+        """
+        Wrapper around Request's getCookie to convert to and from byte strings
+        to unicode/str's
+
+        :param cookie_name:
+        :return:
+        """
         expect_bytes = isinstance(cookie_name, bytes)
 
         if expect_bytes:
@@ -71,15 +78,27 @@ def getCookie(self, cookie_name: T.Union[str, bytes]):
                 return None
 
     def add_before_render(self, func):
+        """
+        Utility intended solely to make testing easier
+        :param func:
+        :return:
+        """
         self._call_before_render = func
         return func
 
     def add_after_render(self, func):
+        """
+        Utility intended solely to make testing easier
+        :param func:
+        :return:
+        """
         self._call_after_render = func
         return func
 
     def write(self, data: T.Union[bytes, str]):
-
+        """
+            Wrapper to prevent unicode/str's from going to Request's write method
+        """
         if isinstance(data, str):
             data = data.encode("utf-8")
         elif isinstance(data, bytes) is False:
@@ -91,7 +110,10 @@ def write(self, data: T.Union[bytes, str]):
     def writeTotal(self, response_body: T.Union[bytes, str], code: T.Union[int, str, bytes] = None,
                    message: T.Union[bytes, str] = None) -> T.NoReturn:
         """
-        
+            Utility to write and then close the connection in one go.
+            Especially useful for error handling events.
+
+
         :param response_body: Content intended for after headers 
         :param code: Optional HTTP Code to use
         :param message: Optional HTTP response message to use
@@ -136,12 +158,22 @@ def setHeader(self, name: T.Union[str, bytes], value: T.Union[str, bytes]):
     def setResponseCode(self,
                         code: int = 500,
                         message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"):
+        """
+            Str wrapper
+        :param code:
+        :param message:
+        :return:
+        """
         if message and not isinstance(message, bytes):
             message = message.encode("utf-8")
 
         return Request.setResponseCode(self, code, message)
 
     def ensureFinished(self):
+        """
+            Ensure's the connection has been flushed and closed without throwing an error.
+        :return:
+        """
         if self.finished not in [1, True]:
             self.finish()
 
@@ -196,11 +228,19 @@ def query_iter(arguments):
         self.process()
 
     @property
-    def methodIsPost(self):
+    def methodIsPost(self) -> bool:
+        """
+            Utility method
+        :return:
+        """
         return self.method == b"POST"
 
     @property
-    def methodIsGet(self):
+    def methodIsGet(self) -> bool:
+        """
+            Utility method
+        :return:
+        """
         return self.method == b"GET"
 
     def render(self, resrc: resource.Resource) -> None:
@@ -217,7 +257,8 @@ def render(self, resrc: resource.Resource) -> None:
         """
         try:
             if self._call_before_render is not None:
-                self._call_before_render(self)
+                body = self._call_before_render(self)
+            # TODO halt rendering resource if call before render provides a response
             body = resrc.render(self)
             if self._call_after_render is not None:
                 self._call_after_render(self, body)
@@ -227,7 +268,7 @@ def render(self, resrc: resource.Resource) -> None:
 
         # TODO deal with HEAD requests or leave it to the Application developer to deal with?
 
-        if body is NOT_DONE_YET:  # TODO replace NOT_DONE_YET with a sentinel versus integer
+        if body is NOT_DONE_YET:
             return
 
         if not isinstance(body, bytes):
@@ -288,10 +329,19 @@ def _processFormData(self, content_type, content_length):
         self.content.seek(0, 0)
 
     def processingFailed(self, reason):
+        """
+            Start of the error handling chain that leads from here all the way up to Application.processingFailed
+        :param reason:
+        :return:
+        """
         self.site.processingFailed(self, reason)
 
     @property
-    def json(self):
+    def json(self) -> bool:
+        """
+        Is this a JSON posted request?
+        :return:
+        """
         if self.getHeader("Content-Type") in ["application/json", "text/json"]:
             return json.loads(self.content.read())
         else:
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 3c60b66..0c0201e 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -86,6 +86,13 @@ def __init__(self, routing_resource=None, request_factory=StrRequest, siteErrorH
 
 
     def processingFailed(self, request: StrRequest, reason: failure.Failure):
+        """
+            The crucial bridge that connects a request.processingFailed exception chain upward
+                towards the Txweb application instance for errorhandling.
+        :param request:
+        :param reason:
+        :return:
+        """
 
         self._lastError = reason
         # self.my_log.error("Handling exception: {reason!r}", reason=reason)
@@ -98,6 +105,12 @@ def processingFailed(self, request: StrRequest, reason: failure.Failure):
             raise
 
     def setErrorHandler(self, func: ErrorHandler):
+        """
+            Kind of goofy, this should only be called by the Txweb application error handler so it can
+            snag the failure/exception that occurred in request.render
+        :param func:
+        :return:
+        """
         self._errorHandler = func
         return func
 

From f8bae2ddc1452d697c9717ef2e18f63e069710af Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 16:48:17 -0700
Subject: [PATCH 143/185] Kind of stupid to have an Exception suffix attached.

---
 txweb/http_codes.py               | 2 +-
 txweb/lib/view_class_assembler.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/txweb/http_codes.py b/txweb/http_codes.py
index 7d4d43d..5b79252 100644
--- a/txweb/http_codes.py
+++ b/txweb/http_codes.py
@@ -91,6 +91,6 @@ def __init__(self, message="Internal Server Error"):
         super().__init__(message)
 
 
-class UnrenderableException(HTTP5xx):
+class Unrenderable(HTTP5xx):
     def __init__(self, message):
         super().__init__(500, message)
diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index 29f8f9d..51de1a6 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -140,4 +140,4 @@ def view_assembler(prefix, kls, route_args):
         return ViewAssemblerResult(instance, rule, endpoints)
 
     else:
-        raise UnrenderableException(f"{kls.__name__!r} is missing exposed method(s) or a render method")
+        raise Unrenderable(f"{kls.__name__!r} is missing exposed method(s) or a render method")

From b45481369cfdac15275fd0460d63355a6e8a34cb Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 16:48:38 -0700
Subject: [PATCH 144/185] First wave of annotation/documentation.

---
 txweb/lib/view_class_assembler.py | 63 +++++++++++++++++++++++++++++--
 txweb/lib/wsprotocol.py           | 59 ++++++++++++++++++++++++++++-
 2 files changed, 117 insertions(+), 5 deletions(-)

diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index 51de1a6..71da072 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -26,7 +26,7 @@ def handle_show(self, request):
 from werkzeug.routing import Rule, Submount
 
 from ..resources import ViewFunctionResource, ViewClassResource
-from txweb.http_codes import UnrenderableException
+from txweb.http_codes import Unrenderable
 from txweb.util.basic import get_thing_name
 
 
@@ -38,6 +38,11 @@ def handle_show(self, request):
 
 
 def has_exposed(obj):
+    """
+        Does the provided object have an exposed endpoint?
+    :param obj:
+    :return:
+    """
     return any([
         True
         for m in getattr(obj, "__dict__", {}).values()
@@ -46,10 +51,20 @@ def has_exposed(obj):
 
 
 def is_exposed(attribute):
+    """
+        Is the provided callable/thing set as exposed?
+    :param attribute:
+    :return:
+    """
     return has_exposed(attribute, EXPOSED_STR) and is_valid_callable
 
 
 def is_viewable(attribute):
+    """
+        Check if whatever this is, it can behave as a web endpoint when called.
+    :param attribute:
+    :return:
+    """
     is_valid_callable = inspect.ismethod(attribute) \
                         or inspect.isfunction(attribute) \
                         or inspect.isgenerator(attribute) \
@@ -60,6 +75,12 @@ def is_viewable(attribute):
 
 
 def is_renderable(kls):
+    """
+        Does a class definition have a valid render method/function
+        Generally checked if it has no exposed methods.
+    :param kls:
+    :return:
+    """
     return \
         any([
             hasattr(kls, render_name)
@@ -72,7 +93,12 @@ def is_renderable(kls):
 
 
 def expose(route, **route_kwargs):
-
+    """
+        Decorator to set the exposed method's routing url and tag it with the exposed sentinel attribute
+    :param route:
+    :param route_kwargs:
+    :return:
+    """
     def processor(func):
         setattr(func, EXPOSED_STR, True)
         setattr(func, EXPOSED_RULE, ExposeSubRule(func.__name__, route, route_kwargs))
@@ -82,11 +108,21 @@ def processor(func):
 
 
 def set_prefilter(func):
+    """
+    decorator used to mark a class method as a prefilter for the class.
+    :param func:
+    :return:
+    """
     setattr(func, PREFILTER_ID, True)
     return func
 
 
 def set_postfilter(func):
+    """
+    decorator used to mark a class method as a post filter for a class.
+    :param func:
+    :return:
+    """
     setattr(func, POSTFILTER_ID, True)
     return func
 
@@ -95,7 +131,12 @@ def set_postfilter(func):
 
 
 def find_member(thing, identifier) -> T.Union[T.Callable, bool]:
-
+    """
+        Utility to search every member of an object for the provided `identifier` attribute
+    :param thing:
+    :param identifier:
+    :return:
+    """
     for name, member in inspect.getmembers(thing, lambda v: hasattr(v, identifier)):
         return member
 
@@ -103,6 +144,22 @@ def find_member(thing, identifier) -> T.Union[T.Callable, bool]:
 
 
 def view_assembler(prefix, kls, route_args):
+    """
+        Given a class definition, this instantiates the class, searches it for exposed
+        methods and pre/post filters
+
+            if it has no exposed methods,
+                and builds a ViewAssemblerResult which contains the instance, submount rules,
+                and references to the endpoints.
+            else it checks if the class def has a render/render_METHOD method.
+            else it throws UnrenderableException
+
+
+    :param prefix:
+    :param kls:
+    :param route_args:
+    :return:
+    """
     endpoints = {}
 
     rules = []
diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index af07ca5..48ab3cd 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -1,9 +1,18 @@
+"""
+    Bridge/interface between the client and server.
+
+    Provides a deferred to notify interested listeners if the connection closes.
+    Provides ask, tell, and respond helpers that match the same verbage/methods on the javascript ResilientSocket class.
+
+
+"""
 from __future__ import annotations
 try:
     import ujson as json
 except ImportError:
     import json
 
+import typing as T
 from uuid import uuid4
 import warnings
 
@@ -28,7 +37,7 @@ class WSProtocol(WebSocketServerProtocol):
     MAX_ASKS = 100  # Need to make this tunable
     factory: RoutedFactory
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self):
         self.pending_responses = {}
 
         super(WSProtocol, self).__init__()
@@ -41,9 +50,20 @@ def __init__(self, *args, **kwargs):
 
     @property
     def application(self):
+        """
+            Utility intended mostly for unit-testing
+        :return:
+        """
         return self.factory.get_application()
 
     def getCookie(self, cookie_name, default=None):
+        """
+            Mirror's the behavior of StrRequest.getCookie
+
+        :param cookie_name:
+        :param default:
+        :return:
+        """
         raw_cookies = self.http_headers.get('cookie', "")
         for params in raw_cookies.split(";"):
             str_name, value = params.split("=")
@@ -53,21 +73,47 @@ def getCookie(self, cookie_name, default=None):
         return default
 
     def onConnect(self, request):
+        """
+            Set's a unique identifier for the connection.
+
+
+        :param request:
+        :return:
+        """
         self.identity = uuid4().hex
         self.my_log.debug("Client connecting: {request.peer}", request=request)
 
     def onClose(self, was_clean, code, reason):
+        """
+            Connection was lost, currently I don't care why but I likely should.
+        :param was_clean:
+        :param code:
+        :param reason:
+        :return:
+        """
         self.on_disconnect.addErrback(self.my_log.error)
         self.on_disconnect.callback(self.identity)
         del self.on_disconnect
         self.my_log.debug("WebSocket connection closed: {reason!r}", reason=reason)
 
     def sendDict(self, **values):
+        """
+            Utility used by every other method that follows
+        :param values:
+        :return:
+        """
         response = json.dumps(values)
         # Always send synchronously for now
         self.sendMessage(response.encode("utf-8"), isBinary=False, sync=True)
 
     def respond(self, original_message, result):
+        """
+            The client asked for a result/response.
+
+        :param original_message:
+        :param result:
+        :return:
+        """
         self.sendDict(caller_id=original_message['caller_id'], type="response", result=result)
 
     def tell(self, endpoint, **values):
@@ -98,6 +144,14 @@ def ask(self, endpoint, **values):
             raise EnvironmentError("Maximum # of pending asks reached")
 
     def onMessage(self, payload, is_binary):
+        """
+            Could be broken apart into smaller pieces perhaps but this handles routing and
+
+        :param payload:
+        :param is_binary:
+        :return:
+        """
+
         if is_binary:  # pragma: no cover
             warnings.warn("Received binary payload, don't know how to deal with this.")
             return None  # I don't know how to deal with binary
@@ -127,7 +181,8 @@ def onMessage(self, payload, is_binary):
                 del self.deferred_asks[caller_id]
             else:
                 warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
-                return
+
+            return
 
         elif "endpoint" in message:
 

From 0392b9cb1c7b56e28c806b0b66d88868a2cf22c9 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 16:50:22 -0700
Subject: [PATCH 145/185] Getting to many false positives with R1705
 unnecessary else/elif after return

---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index e5d739b..9dfae95 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From 4da69f4bb938cfd3557b882d81297a3fb7734cc7 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:21:05 -0700
Subject: [PATCH 146/185] suppress R0901: Too many ancestors (8/7)
 (too-many-ancestors)

Nothing I can do about Autobahn's class inheritance tree so suppressing this error.
---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index 9dfae95..f5adcb5 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0901 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From d25e237e208b71e991c4968983ef7938c34d8c96 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:22:35 -0700
Subject: [PATCH 147/185] Split the error handlers apart into their own files

---
 txweb/lib/errors/base.py    | 46 ++++++++++++++++++
 txweb/lib/errors/debug.py   | 97 +++++++++++++++++++++++++++++++++++++
 txweb/lib/errors/default.py | 83 +++++++++++++++++++++++++++++++
 3 files changed, 226 insertions(+)
 create mode 100644 txweb/lib/errors/base.py
 create mode 100644 txweb/lib/errors/debug.py
 create mode 100644 txweb/lib/errors/default.py

diff --git a/txweb/lib/errors/base.py b/txweb/lib/errors/base.py
new file mode 100644
index 0000000..102bd30
--- /dev/null
+++ b/txweb/lib/errors/base.py
@@ -0,0 +1,46 @@
+"""
+    Reference example for a error handler
+"""
+from __future__ import annotations
+import typing as T
+
+from twisted.python.failure import Failure
+
+from txweb.log import getLogger
+from txweb.lib.str_request import StrRequest
+
+log = getLogger(__name__)
+
+class BaseHandler:
+    """
+
+        Base/example class of an error handler.
+    """
+
+    def __init__(self, application):
+        self.application = application
+
+    def __call__(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:
+        """
+
+        :param request:  Provided to allow managing the connection (writing, http code, etc).
+        :param reason:
+        :return: Return False if the handler was unable to handle the error
+        """
+        # noinspection PyBroadException
+        try:
+            return self.process(request, error)
+        except Exception as exc:
+            log.error("PANIC - There was an exception in the error handler.")
+            request.ensureFinished()
+            raise exc
+
+    def process(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:  # pragma: no cover
+        """
+        Just a stub
+
+        :param request:
+        :param error:
+        :return:
+        """
+        raise NotImplementedError("Attempting to use Base error handler")
\ No newline at end of file
diff --git a/txweb/lib/errors/debug.py b/txweb/lib/errors/debug.py
new file mode 100644
index 0000000..5648a38
--- /dev/null
+++ b/txweb/lib/errors/debug.py
@@ -0,0 +1,97 @@
+"""
+    Base, Default, and generic Debug error handlers for txweb.
+
+
+"""
+from __future__ import annotations
+import typing as T
+from pathlib import Path
+import linecache
+from dataclasses import dataclass
+
+from twisted.python.failure import Failure
+from twisted.python.compat import intToBytes
+
+from txweb.lib.str_request import StrRequest
+from txweb.log import getLogger
+from ... import http_codes
+from ..str_request import StrRequest
+from . import html
+from .base import BaseHandler
+
+
+@dataclass
+class FormattedFrame:
+    name: bytes
+    file: Path
+    line_no: int
+    line: bytes
+
+@dataclass(frozen=True, unsafe_hash=True)
+class StackFrame:
+    funcName: str
+    fileName: str
+    lineNumber: int
+    localsItems: T.Dict[str, T.Any]
+    globalsItems: T.Dict[str, T.Any]
+
+
+log = getLogger(__name__)
+
+class DebugHandler(BaseHandler):  # pragma: no cover
+    """
+        TODO - To finish
+
+        Mimic flask's exception rendering system with some minor caveats.
+
+        Errors are held in resident/session memory if possible.
+        Errors can be reaccessed as necessary
+        Errors time out after 5 minutes of activity
+        No more than 3 errors can be held in memory at one time
+        The same error is not stored more than once to avoid repeats pushing out the other
+        saved errors.
+
+        ##Cool to have
+        * stack trace local/global evaluations similar to flask
+        * pycharm integration (no idea if this is possible)
+
+        ## Great to have
+        * Trim errors down to JUST application code, so tracebacks don't dive all the way into twisted and
+        * txweb beyond the first or second frame.
+
+    """
+    def process(self, request: StrRequest, reason: Failure) -> T.Union[None, bool]:
+        error_items = []
+        for formatted_frame in self.format_stack(reason.frames):
+            error_items.append(html.ERROR_ITEM.format(**formatted_frame))
+
+        error_list = html.ERROR_LIST.format(error_items="\n".join(error_items))
+
+        content = html.ERROR_CONTENT.format(digest=repr(reason.value), error_list=error_list)
+        body = html.ERROR_BODY.format(content=content)
+
+        if issubclass(reason.type, HTTPCode):
+            request.setResponseCode(reason.value.code, reason.value.message)
+        else:
+            request.setResponseCode(500, "Internal server error")
+
+        request.write(body)
+        request.ensureFinished()
+
+    @staticmethod
+    def format_stack(frames: T.List[StackFrame]) -> T.Generator[T.Dict[str, T.Any]]:
+        """
+        Given a twisted Failure frames list, create a generator that yields FormattedFrame's for easier
+         consumption by a jinja2 template.
+        :param frames:
+        :return:
+        """
+        for frame in frames:
+            yield FormattedFrame(
+                name=frame[0].encode("UTF-8"),
+                file=Path(frame[1]),
+                line_no=frame[2],
+                line=linecache.get(frame[2])
+            )
+
+        linecache.clearcache()
diff --git a/txweb/lib/errors/default.py b/txweb/lib/errors/default.py
new file mode 100644
index 0000000..cb2d5a9
--- /dev/null
+++ b/txweb/lib/errors/default.py
@@ -0,0 +1,83 @@
+"""
+    Base, Default, and generic Debug error handlers for txweb.
+
+
+"""
+from __future__ import annotations
+import typing as T
+from pathlib import Path
+import linecache
+from dataclasses import dataclass
+
+from twisted.python.failure import Failure
+from twisted.python.compat import intToBytes
+
+from txweb.lib.str_request import StrRequest
+from txweb.log import getLogger
+from ... import http_codes
+from . import html
+from ..str_request import StrRequest
+from .base import BaseHandler
+
+
+log = getLogger(__name__)
+
+
+
+
+
+# noinspection PyMissingConstructor
+class DefaultHandler(BaseHandler):
+    """
+        Primarily focused with handling 3xx HTTP exception/codes thrown by the application.
+
+    """
+
+    def process(self, request: StrRequest, reason: Failure) -> T.Union[None, bool]:
+        """
+            As mentioned in class docblock, primary focus is handling HTTPCode exceptions thrown by the application.
+
+            If the request/response factory has already started writing to the client, this halts all error processing
+             and throws the exception.
+
+            else if a HTTPCode exception/error it redirects for 3xx codes
+            OR it writes the code and message to the client
+            (Eg HTTPCode500 with Internal error would throw 500 "Internal server error")
+
+            else it sends a 500 HTTP response and then raises the exception back into the user application.
+
+        :param request:
+        :param reason:
+        :return:
+        """
+
+        if request.startedWriting not in [0, False]:
+            # There is nothing we can do, the out going stream is already tainted
+            # noinspection PyBroadException
+            log.error("Failed writing error message to an active stream")
+            request.ensureFinished()
+            reason.raiseException()
+
+        elif isinstance(http_codes.HTTPCode, reason.type) or issubclass(reason.type, http_codes.HTTPCode):
+
+            if issubclass(reason.type, http_codes.HTTP3xx):
+                exc = reason.value
+                request.redirect(exc.redirect, exc.code)
+                response = html.REDIRECT_BODY.format(url=exc.redirect)
+                request.writeTotal(response, code=exc.code, message=exc.message)
+            else:
+                exc = reason.value  # type: HTTPCode
+                request.setResponseCode(exc.code, exc.message)
+                request.setHeader("Content-length", intToBytes(len(exc.message)))
+                request.write(exc.message)
+
+        else:
+            request.setResponseCode(500, b"Internal server error")
+            log.debug(f"Non-HTTPCode error was caught: {reason.type} - {reason.value}")
+            request.ensureFinished()
+            reason.raiseException()
+
+        request.ensureFinished()
+        return True
+
+

From a3955e66eb554ba88b0e72f25f6bacd4af88213d Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:22:52 -0700
Subject: [PATCH 148/185] Delete handler.py

---
 txweb/lib/errors/handler.py | 179 ------------------------------------
 1 file changed, 179 deletions(-)
 delete mode 100644 txweb/lib/errors/handler.py

diff --git a/txweb/lib/errors/handler.py b/txweb/lib/errors/handler.py
deleted file mode 100644
index 6b436da..0000000
--- a/txweb/lib/errors/handler.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""
-    Base, Default, and generic Debug error handlers for txweb.
-
-
-"""
-from __future__ import annotations
-import typing as T
-from pathlib import Path
-import linecache
-from dataclasses import dataclass
-
-from twisted.python.failure import Failure
-from twisted.python.compat import intToBytes
-
-from txweb.log import getLogger
-from ... import http_codes
-from . import html
-from ..str_request import StrRequest
-from txweb.lib.str_request import StrRequest
-
-
-@dataclass
-class FormattedFrame:
-    name: bytes
-    file: Path
-    line_no: int
-    line: bytes
-
-@dataclass(frozen=True, unsafe_hash=True)
-class StackFrame:
-    funcName: str
-    fileName: str
-    lineNumber: int
-    localsItems: T.Dict[str, T.Any]
-    globalsItems: T.Dict[str, T.Any]
-
-
-log = getLogger(__name__)
-
-
-class BaseHandler:
-    """
-        TODO - Look into using twisted/zope's interface system
-
-        Base/example class of an error handler.
-    """
-
-    def __init__(self, application):
-        self.application = application
-
-    def __call__(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:
-        """
-
-        :param request:  Provided to allow managing the connection (writing, http code, etc).
-        :param reason:
-        :return: Return False if the handler was unable to handle the error
-        """
-        # noinspection PyBroadException
-        try:
-            return self.process(request, error)
-        except Exception as exc:
-            log.error("PANIC - There was an exception in the error handler.")
-            request.ensureFinished()
-            raise exc
-
-    def process(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:  # pragma: no cover
-        raise NotImplementedError("Attempting to use Base error handler")
-
-
-# noinspection PyMissingConstructor
-class DefaultHandler(BaseHandler):
-    """
-        Primarily focused with handling 3xx HTTP exception/codes thrown by the application.
-
-    """
-
-    def __init__(self, enable_debug=False):
-
-        self.enable_debug = enable_debug
-
-    def process(self, request: StrRequest, reason: Failure) -> bool:
-        """
-            As mentioned in class docblock, primary focus is handling HTTPCode exceptions thrown by the application.
-
-            If the request/response factory has already started writing to the client, this halts all error processing
-             and throws the exception.
-
-            else if a HTTPCode exception/error it redirects for 3xx codes
-            OR it writes the code and message to the client
-            (Eg HTTPCode500 with Internal error would throw 500 "Internal server error")
-
-            else it sends a 500 HTTP response and then raises the exception back into the user application.
-
-        :param request:
-        :param reason:
-        :return:
-        """
-
-        if request.startedWriting not in [0, False]:
-            # There is nothing we can do, the out going stream is already tainted
-            # noinspection PyBroadException
-            log.error("Failed writing error message to an active stream")
-            request.ensureFinished()
-            reason.raiseException()
-
-        elif isinstance(http_codes.HTTPCode, reason.type) or issubclass(reason.type, http_codes.HTTPCode):
-
-            if issubclass(reason.type, http_codes.HTTP3xx):
-                exc = reason.value
-                request.redirect(exc.redirect, exc.code)
-                response = html.REDIRECT_BODY.format(url=exc.redirect)
-                request.writeTotal(response, code=exc.code, message=exc.message)
-            else:
-                exc = reason.value  # type: HTTPCode
-                request.setResponseCode(exc.code, exc.message)
-                request.setHeader("Content-length", intToBytes(len(exc.message)))
-                request.write(exc.message)
-
-        else:
-            request.setResponseCode(500, b"Internal server error")
-            log.debug(f"Non-HTTPCode error was caught: {reason.type} - {reason.value}")
-            request.ensureFinished()
-            reason.raiseException()
-
-        request.ensureFinished()
-        return True
-
-
-class DebugHandler(BaseHandler):  # pragma: no cover
-    """
-        TODO - To finish
-
-        Mimic flask's exception rendering system with some minor caveats.
-
-        Errors are held in resident/session memory if possible.
-        Errors can be reaccessed as necessary
-        Errors time out after 5 minutes of activity
-        No more than 3 errors can be held in memory at one time
-        The same error is not stored more than once to avoid repeats pushing out the other
-        saved errors.
-
-        ##Cool to have
-        * stack trace local/global evaluations similar to flask
-        * pycharm integration (no idea if this is possible)
-
-        ## Great to have
-        * Trim errors down to JUST application code, so tracebacks don't dive all the way into twisted and
-        * txweb beyond the first or second frame.
-
-    """
-    def process(self, request: StrRequest, reason: Failure) -> None:
-        error_items = []
-        for formatted_frame in self.format_stack(reason.frames):
-            error_items.append(html.ERROR_ITEM.format(**formatted_frame))
-
-        error_list = html.ERROR_LIST.format(error_items="\n".join(error_items))
-
-        content = html.ERROR_CONTENT.format(digest=repr(reason.value), error_list=error_list)
-        body = html.ERROR_BODY.format(content=content)
-
-        if issubclass(reason.type, HTTPCode):
-            request.setResponseCode(reason.value.code, reason.value.message)
-        else:
-            request.setResponseCode(500, "Internal server error")
-
-        request.write(body)
-        request.ensureFinished()
-
-    @staticmethod
-    def format_stack(frames: T.List[StackFrame]) -> T.Generator[T.Dict[str, T.Any]]:
-        for frame in frames:
-            yield FormattedFrame(
-                name=frame[0].encode("UTF-8"),
-                file=Path(frame[1]),
-                line_no=frame[2],
-                line=linecache.get(frame[2])
-            )
-
-        linecache.clearcache()

From 7077e78ca13b495b1e351f8fcb850c9fb9f73ef0 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:24:11 -0700
Subject: [PATCH 149/185] Working on replacing the need for eval

to make it easier, consolidated into one staticmethod which required the calling functions to become classmethods so they could reference it.
---
 txweb/application.py | 28 +++++++++++++++-------------
 1 file changed, 15 insertions(+), 13 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 47036fc..4bdaebc 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -169,10 +169,19 @@ def processor(kls, name = None):
             return processor(kls, name)
 
 
+    @staticmethod
+    def _eval_annotation(statement, func):
+        """
+        TODO - Figure out if there is a way to the type of an annotation without eval
+        :param statement:
+        :param func:
+        :return:
+        """
+        return statement if not isinstance(statement, str) else eval(statement, vars(sys.modules[func.__module__]))
 
 
-    @staticmethod
-    def websocket_class_arguments_decorator(func):
+    @classmethod
+    def websocket_class_arguments_decorator(cls, func):
         """
             Internal method not intended for users
         :param func:
@@ -181,10 +190,6 @@ def websocket_class_arguments_decorator(func):
         params = inspect.signature(func).parameters
         arg_keys = {}
         converter_keys = {}
-        positional_count = 0
-
-        def eval_type(st):
-            return st if not isinstance(st, str) else eval(st, vars(sys.modules[func.__module__]))
 
         for param_name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
@@ -193,7 +198,7 @@ def eval_type(st):
                 arg_keys[param.name] = param.default
 
                 if param.annotation is not inspect.Parameter.empty:
-                    converter_keys[param.name] = eval_type(param.annotation)
+                    converter_keys[param.name] = cls._eval_annotation(param.annotation, func)
 
         if "message" not in params:
             raise TypeError("ws_expose convention expects (self, message, **kwargs)")
@@ -218,8 +223,8 @@ def method_argument_decorator(parent, message):
 
         return method_argument_decorator
 
-    @staticmethod
-    def websocket_function_arguments_decorator(func):
+    @classmethod
+    def websocket_function_arguments_decorator(cls, func):
         """
             Internal method not intended for users
         :param func:
@@ -229,9 +234,6 @@ def websocket_function_arguments_decorator(func):
         arg_keys = {}
         converter_keys= {}
 
-        def eval_type(st):
-            return st if not isinstance(st, str) else eval(st, vars(sys.modules[func.__module__]))
-
         for name, param in params.items():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
@@ -240,7 +242,7 @@ def eval_type(st):
                 arg_keys[name] = param.default
 
                 if param.annotation is not inspect.Parameter.empty:
-                    converter_keys[name] = eval_type(param.annotation)
+                    converter_keys[name] = cls._eval_annotation(param.annotation, func)
 
 
         @functools.wraps(func)

From a36e554484cacf4ac2ac838406f09291d7e79351 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:24:35 -0700
Subject: [PATCH 150/185] massive work on making pylint happy

---
 txweb/application.py                          | 38 ++++++-----
 txweb/http_codes.py                           |  7 +-
 txweb/lib/__init__.py                         |  3 +
 txweb/lib/errors/__init__.py                  |  5 ++
 txweb/lib/message_handler.py                  |  8 +--
 txweb/lib/str_request.py                      | 43 ++++++------
 txweb/lib/view_class_assembler.py             |  4 +-
 txweb/lib/wsprotocol.py                       | 68 +++++++++----------
 txweb/log.py                                  |  2 +-
 txweb/resources/routing.py                    | 30 ++++----
 txweb/resources/view_class.py                 | 13 ++++
 txweb/resources/view_function.py              |  6 +-
 .../test_errors_handlers_DebugHandler.py      |  2 +-
 txweb/tests/test_resources_routing.py         |  4 +-
 txweb/util/reloader.py                        |  2 +-
 txweb/util/templating.py                      | 22 +++---
 txweb/util/url_converter.py                   | 26 +++++--
 txweb/web_views.py                            |  8 +--
 18 files changed, 161 insertions(+), 130 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 4bdaebc..a99cf0a 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -22,10 +22,9 @@
 from twisted.internet.tcp import Port
 from twisted.internet.posixbase import PosixReactorBase
 from twisted.internet import reactor  # type: PosixReactorBase
-from twisted.python.compat import intToBytes
+# from twisted.python.compat import intToBytes
 from twisted.web.server import NOT_DONE_YET
 from twisted.web.static import File
-from twisted.web.static import File as StaticFile
 from twisted.python import failure
 
 try:
@@ -47,7 +46,7 @@
 from .lib import StrRequest, expose_method, set_prefilter, set_postfilter
 from .web_views import WebSite
 from .http_codes import HTTPCode
-from .lib.errors.handler import DefaultHandler, DebugHandler, BaseHandler
+from .lib.errors.default import DefaultHandler, BaseHandler
 
 
 HERE = Path(__file__).parent
@@ -191,7 +190,7 @@ def websocket_class_arguments_decorator(cls, func):
         arg_keys = {}
         converter_keys = {}
 
-        for param_name, param in params.items():  # type: inspect.Parameter
+        for param in params.values():  # type: inspect.Parameter
             if param.default is not inspect.Parameter.empty:
                 if param.name in ["message"]:
                     raise TypeError(f"assign_args error: argument `message` cannot be a keyword argument: {param.name}")
@@ -306,7 +305,7 @@ def add(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResour
     # mimic flask's API
     route = add
 
-    def add_class(self, route_str:str, **kwargs: ArbitraryKWArguments) ->CallableToResourceDecorator:
+    def add_class(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator:
         """
 
         example usage
@@ -328,7 +327,8 @@ def some_endpoint(self, request):
         return self.router.add(route_str, **kwargs)
 
 
-    def expose(self, route_str, **kwargs):
+    @staticmethod
+    def expose(route_str, **kwargs):
         """
             Refer to add_class for usage
         :param route_str:
@@ -338,7 +338,8 @@ def expose(self, route_str, **kwargs):
 
         return expose_method(route_str, **kwargs)
 
-    def set_view_prefilter(self, func):
+    @staticmethod
+    def set_view_prefilter(func):
         """
             Experimental, sets a view class method to be called before any `expose`'d method.
         :param func:
@@ -346,7 +347,8 @@ def set_view_prefilter(self, func):
         """
         return set_prefilter(func)
 
-    def set_view_postfilter(self, func):
+    @staticmethod
+    def set_view_postfilter(func):
         """
             Experimental, sets a view class method to be called after any `expose`'d method.
         :param func:
@@ -367,7 +369,7 @@ def add_resource(self, route_str:str, resource, **kwargs):
         return self.router._add_resource(route_str, thing=resource, route_kwargs=kwargs )
 
 
-    def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> SimpleFile:
+    def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> File:
         """
         Just a simple helper for a common task of serving individual files
 
@@ -378,7 +380,7 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Si
         """
 
         assert Path(filePath).exists()
-        file_resource = StaticFile(filePath)
+        file_resource = File(filePath, defaultType=defaultType)
         return self.router.add(route_str)(file_resource)
 
 
@@ -418,13 +420,13 @@ class ApplicationErrorHandlingMixin:
     enable_debug: bool
     site: WebSite
 
-    def __init__(self, enable_debug: bool =False,  **kwargs):
+    def __init__(self, enable_debug: bool =False):
         """
         :param enable_debug: Flag to decide if to use debugging tools
         """
-        self.error_handlers = dict(default=self.default_handler_cls(self))
         self.enable_debug = enable_debug
 
+        self.error_handlers = dict(default=self.default_handler_cls(self))
         self.site.setErrorHandler(self.processingFailed)
 
 
@@ -445,9 +447,9 @@ def processor(func: ErrorHandler) -> ErrorHandler:
             if error_type in self.error_handlers and write_over is False:
                 old_func = self.error_handlers[error_type]
                 raise ValueError(f"handle_error called twice to handle {error_type} with old {old_func} vs {func}")
-
-            self.error_handlers[error_type] = func
-            return func
+            else:
+                self.error_handlers[error_type] = func
+                return func
 
         return processor
 
@@ -493,8 +495,8 @@ def processingFailed(self, request:StrRequest, reason: failure.Failure):
         if handler(request, reason) is False:
             if handler == default_handler:
                 raise RuntimeError(f"Default error handler {handler} returned False but it should return True")
-            else:
-                default_handler(request, reason)
+
+            default_handler(request, reason)
 
         request.ensureFinished()
 
@@ -610,7 +612,7 @@ def listenTCP(self, port:int, interface:str= "127.0.0.1") -> Port:
             Convenience helper which adds the HTTP protocol factory to the reactor and set it to listen to the provided
             interface and port
         """
-        self._listening_port = self.reactor.listenTCP(port, self.site, interface=interface)
+        self._listening_port = self.reactor.listenTCP(port, self._site, interface=interface)
         return self._listening_port
 
     def before_render(self, func: T.Callable[[StrRequest], None]):
diff --git a/txweb/http_codes.py b/txweb/http_codes.py
index 5b79252..b6c1952 100644
--- a/txweb/http_codes.py
+++ b/txweb/http_codes.py
@@ -16,6 +16,8 @@ class HTTPCode(RuntimeError):
         errors
     """
     def __init__(self, code:int, message:str, exc:T.Optional[Exception]=None):
+        super().__init__(message)
+
         self.code = code
         self.message = message
         self.exc = exc
@@ -39,7 +41,7 @@ def __init__(self, redirect):
 
 class HTTP303(HTTP3xx):
     def __init__(self, redirect):
-        super(HTTP303, self).__init__(303, redirect, message="See Other")
+        super().__init__(303, redirect, message="See Other")
 
 class HTTP304(HTTP3xx):
     def __init__(self, redirect):
@@ -88,8 +90,7 @@ class HTTP5xx(HTTPCode):
 
 class HTTP500(HTTP5xx):
     def __init__(self, message="Internal Server Error"):
-        super().__init__(message)
-
+        super().__init__(500, message)
 
 class Unrenderable(HTTP5xx):
     def __init__(self, message):
diff --git a/txweb/lib/__init__.py b/txweb/lib/__init__.py
index a1537d4..4aca959 100644
--- a/txweb/lib/__init__.py
+++ b/txweb/lib/__init__.py
@@ -1,3 +1,6 @@
+"""
+    TXWeb's library of fundamental classes
+"""
 from .str_request import StrRequest
 from .view_class_assembler import expose as expose_method
 from .view_class_assembler import ViewClassResource
diff --git a/txweb/lib/errors/__init__.py b/txweb/lib/errors/__init__.py
index ca731ec..3be09d4 100644
--- a/txweb/lib/errors/__init__.py
+++ b/txweb/lib/errors/__init__.py
@@ -1 +1,6 @@
+"""
+    Error handlers for the txweb
+"""
 from twisted.python.failure import Failure
+from .default import DefaultHandler
+from .debug import DebugHandler
diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 0e27bae..f1f973e 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -7,10 +7,10 @@
 """
 from __future__ import annotations
 import typing as T
+from collections.abc import Mapping
 
-from twisted.web.server import NOT_DONE_YET
+# from twisted.web.server import NOT_DONE_YET
 
-from collections.abc import Mapping
 
 if T.TYPE_CHECKING or False:  # pragma: no cover
     # recursion import
@@ -19,7 +19,7 @@
 
 class MessageHandler(Mapping):  # pragma: no cover
 
-    raw_message: T.Dict[T.str, T.Any]
+    raw_message: T.Dict[T.AnyStr, T.Any]
     connection: WSProtocol
 
     def __init__(self, raw_message: dict, connection: WSProtocol):
@@ -39,7 +39,7 @@ def __contains__(self, item):  # pragma: no cover
         return item in self.raw_message
 
     def keys(self):  # pragma: no cover
-        return self.raw_message1.keys()
+        return self.raw_message.keys()
 
     def items(self):  # pragma: no cover
         return self.raw_message.items()
diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py
index d964df7..6963efd 100644
--- a/txweb/lib/str_request.py
+++ b/txweb/lib/str_request.py
@@ -13,35 +13,38 @@
     and into the twisted library.   Unfortunately this is a doozy of a sub-project as its not just Request but also
     headers logic.
 """
-import cgi
+from __future__ import annotations
+
+# import cgi
 import json
 from urllib.parse import parse_qs
 import typing as T
 
-from twisted.python import reflect
+# from twisted.python import reflect
 # noinspection PyProtectedMember
-from twisted.web.error import UnsupportedMethod
-from twisted.web.server import Request, NOT_DONE_YET, supportedMethods
+# from twisted.web.error import UnsupportedMethod
+from twisted.web.server import Request, NOT_DONE_YET
+# from twisted.web.server import supportedMethods
 from twisted.web.http import FOUND
 from twisted.web import resource
-from twisted.web import http
+# from twisted.web import http
 # noinspection PyProtectedMember
-from twisted.web.http import _parseHeader
+# from twisted.web.http import _parseHeader
 # noinspection PyProtectedMember
-from twisted.python.compat import _PY3, _PY37PLUS, nativeString, escape, intToBytes
+# from twisted.python.compat import _PY3, _PY37PLUS
+# from twisted.python.compat import nativeString
+# from twisted.python.compat import escape
+from twisted.python.compat import intToBytes
 
 from werkzeug.formparser import FormDataParser
 from werkzeug.datastructures import MultiDict
+from werkzeug import FileStorage
 
 from ..log import getLogger
 from ..http_codes import HTTP500
 
 log = getLogger(__name__)
 
-if T.TYPE_CHECKING:  # pragma: no cover
-    from werkzeug import FileStorage
-
-
 class StrRequest(Request):
 
     NOT_DONE_YET: T.Union[int, bool] = NOT_DONE_YET
@@ -221,7 +224,7 @@ def query_iter(arguments):
                 key = key.decode("utf-8") if isinstance(key, bytes) else key
                 for val in values:
                     val = val.decode("utf-8") if isinstance(val, bytes) else val
-                    yield key, val,
+                    yield key, val
 
         self.args = MultiDict(list(query_iter(query_args)))
 
@@ -255,16 +258,12 @@ def render(self, resrc: resource.Resource) -> None:
 
         @see: L{IResource.render()}
         """
-        try:
-            if self._call_before_render is not None:
-                body = self._call_before_render(self)
-            # TODO halt rendering resource if call before render provides a response
-            body = resrc.render(self)
-            if self._call_after_render is not None:
-                self._call_after_render(self, body)
-        except:
-            # log.exception(f"While processing {self.method!r} {self.uri}")
-            raise
+        if self._call_before_render is not None:
+            body = self._call_before_render(self)
+        # TODO halt rendering resource if call before render provides a response
+        body = resrc.render(self)
+        if self._call_after_render is not None:
+            self._call_after_render(self, body)
 
         # TODO deal with HEAD requests or leave it to the Application developer to deal with?
 
diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index 71da072..ec3b06c 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -26,7 +26,7 @@ def handle_show(self, request):
 from werkzeug.routing import Rule, Submount
 
 from ..resources import ViewFunctionResource, ViewClassResource
-from txweb.http_codes import Unrenderable
+# from txweb.http_codes import Unrenderable
 from txweb.util.basic import get_thing_name
 
 
@@ -197,4 +197,4 @@ def view_assembler(prefix, kls, route_args):
         return ViewAssemblerResult(instance, rule, endpoints)
 
     else:
-        raise Unrenderable(f"{kls.__name__!r} is missing exposed method(s) or a render method")
+        raise EnvironmentError(f"{kls.__name__!r} is missing exposed method(s) or a render method")
diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 48ab3cd..c915c06 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -154,56 +154,50 @@ def onMessage(self, payload, is_binary):
 
         if is_binary:  # pragma: no cover
             warnings.warn("Received binary payload, don't know how to deal with this.")
-            return None  # I don't know how to deal with binary
+            return
 
         try:  # pragma: no cover
             payload = payload.decode("utf-8")
+            raw_message = json.loads(payload)
         except UnicodeDecodeError:  # pragma: no cover
             warnings.warn(f"Failed to decode {payload}")
-            return
-
-        try:  # pragma: no cover
-            raw_message = json.loads(payload)
         except json.JSONDecodeError:  # pragma: no cover
             warnings.warn(f"Corrupt/bad payload: {payload}")
-            return
+        else:
 
-        message = MessageHandler(raw_message, self)
-        result = None
-        endpoint_func = None
 
-        if message.get("type") == "response":
-            caller_id = message.get("caller_id", None)
+            message = MessageHandler(raw_message, self)
+            result = None
+            endpoint_func = None
 
-            if caller_id is not None and caller_id in self.deferred_asks:
-                d = self.deferred_asks[caller_id]  # type: Deferred
-                d.callback(message.get("result"))
-                del self.deferred_asks[caller_id]
-            else:
-                warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
+            if message.get("type") == "response":
+                caller_id = message.get("caller_id", None)
 
-            return
+                if caller_id is not None and caller_id in self.deferred_asks:
+                    d = self.deferred_asks[caller_id]  # type: Deferred
+                    d.callback(message.get("result"))
+                    del self.deferred_asks[caller_id]
+                else:
+                    warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
 
-        elif "endpoint" in message:
+            elif "endpoint" in message:
 
-            endpoint_func = self.factory.get_endpoint(message['endpoint'])
-            self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
+                endpoint_func = self.factory.get_endpoint(message['endpoint'])
+                self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
 
-            if endpoint_func is None:
-                self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])
-                return
+                if endpoint_func is None:
+                    self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])
+                else:
+                    result = endpoint_func(message)
             else:
-                result = endpoint_func(message)
-        else:
-            self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message)
-            # raise Exception("Got message without a endpoint")
+                self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message)
 
-        if result in [NOT_DONE_YET, None]:
-            return
-        elif isinstance(result, Deferred):
-            return
-        elif message.get("type", default=None) == "ask":
-            self.respond(message, result=result)
-        else:
-            warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
-            pass
+            if result in [NOT_DONE_YET, None]:
+                return
+            elif isinstance(result, Deferred):
+                return
+            elif message.get("type", default=None) == "ask":
+                self.respond(message, result=result)
+            else:
+                warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
+                pass
diff --git a/txweb/log.py b/txweb/log.py
index 61d68d3..230f570 100644
--- a/txweb/log.py
+++ b/txweb/log.py
@@ -12,4 +12,4 @@ def getLogger(namespace: str = None) -> logger.Logger:
     :param namespace:
     :return:
     """
-    return logger.Logger(namespace) if namespace else logger.Logger()
\ No newline at end of file
+    return logger.Logger(namespace) if namespace else logger.Logger()
diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index abe9f32..9862f0d 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -1,3 +1,7 @@
+"""
+    The centerpiece of TxWeb is the Routing Resource which wraps around werkzeug's routing system.
+
+"""
 from collections import OrderedDict
 import typing as T
 import inspect
@@ -12,7 +16,7 @@
 from werkzeug import routing as wz_routing
 
 
-from txweb.http_codes import UnrenderableException
+from txweb.http_codes import Unrenderable
 from txweb.util.url_converter import DirectoryPath
 from txweb.util.basic import get_thing_name
 from txweb.lib.str_request import StrRequest
@@ -21,10 +25,8 @@
 from .view_class import ViewClassResource
 # from .directory import Directory
 
-from ..lib import view_class_assembler as vca
 from txweb import http_codes as HTTP_Errors
-
-
+from ..lib import view_class_assembler as vca
 from ..log import getLogger
 
 
@@ -105,9 +107,8 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
 
             common_kwargs = {"endpoint": endpoint_name, "thing": original_thing, "route_kwargs": kwargs}
 
-            """
-                Is the thing to be added to the router a twisted Resource class?
-            """
+
+            # Is the thing to be added to the router a twisted Resource class?
             if inspect.isclass(original_thing) and issubclass(original_thing, resource.Resource):
 
                 self._add_resource_cls(route_str, **common_kwargs)
@@ -164,7 +165,7 @@ def _add_class(self, route_str: T.AnyStr,
         """
 
         if vca.is_renderable(thing) is False:
-            raise UnrenderableException(f"{thing.__name__!r} is missing exposed methods or a render method")
+            raise Unrenderable(f"{thing.__name__!r} is missing exposed methods or a render method")
 
         if vca.has_exposed(thing):
             result = vca.view_assembler(route_str, thing, route_kwargs)
@@ -238,7 +239,7 @@ def add_directory(self, route_str: str, directory_resource: File) -> File:
 
         return directory_resource
 
-    def _build_map(self, path_element, request):
+    def _build_map(self, request):
         """
             Takes all of the information provided by the request object and adapts them to match the wsgi environment
             dictionary so that werkzeug can provide a routing map.
@@ -281,24 +282,23 @@ def getChildWithDefault(self, path_element: T.Union[bytes, str], request: StrReq
             returns a resource OR it throws up an errors.HTTPCode
         """
 
-        routing = self._build_map(path_element, request)
+        routing = self._build_map(request)
 
         try:
             # TODO refactor to handle HEAD requests when the only valid match support GET
             # - one bad idea is to hack on werkzeug to append the URI matching rule to MethodNotAllowed
             (rule, kwargs) = routing.match(return_rule=True)
         except wz_routing.RequestRedirect as redirect:
-            log.debug(f"Werkzeug threw a redirect")
-            raise HTTP_Errors.HTTP3xx(redirect.code, redirect.new_url, redirect.name)
+            log.debug("Werkzeug threw a redirect")
+            raise HTTP_Errors.HTTP3xx(redirect.code, redirect.new_url, redirect.name) from redirect
         except wz_routing.NotFound as exc:
-            # TODO remove print
             log.debug(f"Failed to find match for: {request.path!r}")
-            raise HTTP_Errors.HTTP404(exc)
+            raise HTTP_Errors.HTTP404(exc) from exc
 
         except wz_routing.MethodNotAllowed as exc:
             # TODO finish error handling
             log.debug(f"Unable to find a valid match for {request.path!r} with {request.method!r}")
-            raise HTTP_Errors.HTTP405(exc)
+            raise HTTP_Errors.HTTP405(exc) from exc
 
         request.rule = rule
         request.route_args = kwargs
diff --git a/txweb/resources/view_class.py b/txweb/resources/view_class.py
index cfcbb47..b73d3ae 100644
--- a/txweb/resources/view_class.py
+++ b/txweb/resources/view_class.py
@@ -1,3 +1,16 @@
+"""
+    A wrapper that takes a vanilla class and turns it into a compatible Resource.
+        Not intended to be used directly but instead by using
+
+        ```python
+        @app.route("/my_thing")
+        class Thing:
+            def render(request):
+                return "I am thing!"
+
+        ```
+
+"""
 from __future__ import annotations
 import typing as T
 from twisted.web import resource
diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py
index cfd12a3..01e0aa7 100644
--- a/txweb/resources/view_function.py
+++ b/txweb/resources/view_function.py
@@ -1,9 +1,11 @@
 from __future__ import annotations
+import typing as T
+
+from twisted.web import resource
+
 from txweb.util.basic import sanitize_render_output
 
 
-from twisted.web import resource
-import typing as T
 
 
 # Prefilter takes StrRequest as first argument and second argument is the actual wrapped view function
diff --git a/txweb/tests/test_errors_handlers_DebugHandler.py b/txweb/tests/test_errors_handlers_DebugHandler.py
index a3aca6e..225383c 100644
--- a/txweb/tests/test_errors_handlers_DebugHandler.py
+++ b/txweb/tests/test_errors_handlers_DebugHandler.py
@@ -3,7 +3,7 @@
 from unittest.mock import MagicMock
 
 from txweb import Application
-from txweb.lib.errors.handler import DebugHandler
+from txweb.lib.errors import DebugHandler
 from txweb.lib.str_request import StrRequest
 
 from .helper import MockRequest
diff --git a/txweb/tests/test_resources_routing.py b/txweb/tests/test_resources_routing.py
index 97d8418..f701bbb 100644
--- a/txweb/tests/test_resources_routing.py
+++ b/txweb/tests/test_resources_routing.py
@@ -4,7 +4,7 @@
 
 from txweb.resources import RoutingResource
 from txweb import App
-from txweb.http_codes import UnrenderableException
+from txweb.http_codes import Unrenderable
 from txweb.resources import ViewClassResource
 
 from unittest.mock import sentinel
@@ -57,7 +57,7 @@ def test_ensure_blowsup_with_a_class_that_has_no_way_to_render():
 
     app = App(__name__)
 
-    with pytest.raises(UnrenderableException):
+    with pytest.raises(Unrenderable):
         @app.add("/trash")
         class BaseClass(object):
             pass
diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index 01ca647..dc5b588 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -36,8 +36,8 @@ def main():
 import sys
 import time
 
-import txweb
 from logging import getLogger
+import txweb
 
 log = getLogger(__name__)
 
diff --git a/txweb/util/templating.py b/txweb/util/templating.py
index 86d4301..3c4191e 100644
--- a/txweb/util/templating.py
+++ b/txweb/util/templating.py
@@ -1,22 +1,20 @@
+"""
+    Utility to provide Jinja2 support for txweb enabled applications
 
-import json
+    TODO: look into jinja2 returning bytes by default to cut down on post-processing
+"""
+# import json
 import typing as T
 from pathlib import Path
 try:  # pragma: no cover
     import jinja2
-except ImportError:  # pragma: no cover
-    raise EnvironmentError("Jinja2 is not install: pip install jinja2 to use the templating utility")
+except ImportError as failed_import:  # pragma: no cover
+    raise EnvironmentError("Jinja2 is not install: pip install jinja2 to use the templating utility") from failed_import
 
 
 from jinja2 import FileSystemLoader, Environment, BytecodeCache
 from jinja2.bccache import Bucket
 
-"""
-    Utility to provide Jinja2 support for txweb enabled applications
-    
-    TODO: look into jinja2 returning bytes by default to cut down on post-processing
-"""
-
 # pragma: no cover
 JINJA2_ENV = None  # type: Environment
 
@@ -69,6 +67,12 @@ def initialize_jinja2(
 
 
 def render(template_pathname, **template_args):  # pragma: no cover
+    """
+    Utility that merges fetching a Jinja2 template and rendering it into one call.
+    :param template_pathname:
+    :param template_args:
+    :return:
+    """
     if JINJA2_ENV is None:
         raise EnvironmentError("Jinja2 environment not initialized, call initialize_jinja2 first")
 
diff --git a/txweb/util/url_converter.py b/txweb/util/url_converter.py
index 9772eb2..69dfe07 100644
--- a/txweb/util/url_converter.py
+++ b/txweb/util/url_converter.py
@@ -1,21 +1,35 @@
+"""
+    URL converter for adding twisted.web.static.File resources into the routing map.
+
+"""
 import typing as T
 
 from werkzeug.routing import BaseConverter
 
-class DirectoryPath(BaseConverter): # pragma: no cover
+
+class DirectoryPath(BaseConverter):  # pragma: no cover
     """
         Rule("/whatever/") is a black hole rule in that
         whatever starts with /whatever will end up being caught by this rule.
     """
-    regex = r"(.*)" #  Got to catch them all
+    regex = r"(.*)"  # Got to catch them all
 
-    def __init__(self, url_map):
-        super().__init__(url_map)
+    def to_python(self, value) -> T.List[str]:
+        """
+            Convert the url segment to a list object
 
-    def to_python(self, value):
+        :param value:
+        :return:
+        """
         return value.split("/")
 
-    def to_url(self, value: T.List[str]):
+    def to_url(self, value: T.List[str]) -> str:
+        """
+        Join a list of strs into a directory path string
+
+        :param value:
+        :return:
+        """
         if isinstance(value, str):
             value = [value]
         return "/".join(value)
diff --git a/txweb/web_views.py b/txweb/web_views.py
index 0c0201e..539fcf0 100644
--- a/txweb/web_views.py
+++ b/txweb/web_views.py
@@ -26,14 +26,8 @@
 
 log = getLogger(__name__)
 
-
-if T.TYPE_CHECKING: # pragma: no cover
-    # No executable intended for type hints only
-    import pathlib
-
 ResourceView = T.Type["_ResourceThing"]
 ErrorHandler = T.NewType("ErrorHandler", T.Callable[['Website', StrRequest, failure.Failure], None])
-
 LIBRARY_TEMPLATE_PATH = pathlib.Path(txweb.__file__).parent / "templates"
 
 # class _RoutingSiteConnectors(server.Site):
@@ -101,7 +95,7 @@ def processingFailed(self, request: StrRequest, reason: failure.Failure):
             self._errorHandler(request, reason)
         except Exception as exc:
             #Dear god wtf went wrong?
-            self.my_log.error(f"Exception occurred while handling {reason!r}")
+            self.my_log.error("Exception {exc!r} occurred while handling {reason!r}", exc=exc, reason=reason)
             raise
 
     def setErrorHandler(self, func: ErrorHandler):

From e283c088a3c07bda70b3d46742b5c512fcef443f Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:25:42 -0700
Subject: [PATCH 151/185] R0903 - Too few public methods

Currently I only need one public method.
---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index f5adcb5..aa29660 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0901 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0903 -d R0901 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From ed78d09d4cdf9c3bfe61ef22d89dcb44b197d697 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 20:39:45 -0700
Subject: [PATCH 152/185]  R0902: Too many instance attributes

Nothing I can do or want to do about this as Application is an intentional god module.
---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index aa29660..08ddeda 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0903 -d R0901 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0903 -d R0901 -d R0902 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From d05dbcce7f3ea76417a37e1700eb9c91d52aa89b Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:07:20 -0700
Subject: [PATCH 153/185] W0221: Parameters differ from overridden

There are multiple places where I added more arguments/parameters to the underlying logic so this warning doesn't make much sense to me.
---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index 08ddeda..1aae1ac 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d R0903 -d R0901 -d R0902 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -W0221 -d R0903 -d R0901 -d R0902 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From 2fd37ff5f4c574f2f20db8a81b116ec46c30f47d Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:07:44 -0700
Subject: [PATCH 154/185] Massive pylint hunt

---
 txweb/application.py              | 25 ++++++-----
 txweb/lib/__init__.py             |  2 +-
 txweb/lib/errors/base.py          |  3 +-
 txweb/lib/errors/debug.py         |  2 +-
 txweb/lib/errors/default.py       |  3 --
 txweb/lib/errors/html.py          |  2 +-
 txweb/lib/view_class_assembler.py | 23 +++++-----
 txweb/lib/wsprotocol.py           | 74 +++++++++++++++++++------------
 txweb/resources/routing.py        | 14 +++---
 txweb/resources/view_function.py  |  3 ++
 txweb/util/templating.py          |  6 +--
 11 files changed, 89 insertions(+), 68 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index a99cf0a..58ec6ac 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -28,16 +28,16 @@
 from twisted.python import failure
 
 try:
-    import autobahn
+    from autobahn.twisted.resource import WebSocketResource
 except ImportError:
     AUTOBAHN_MISSING = True
     print("Unable to support websockets:  `pip install autobahn` to enable")
 else:
     AUTOBAHN_MISSING = False
-    from .lib.wsprotocol import WSProtocol
+    # from .lib.wsprotocol import WSProtocol
     from .lib.message_handler import MessageHandler
     from .lib.routed_factory import RoutedWSFactory
-    from autobahn.twisted.resource import WebSocketResource
+
 
 # Application
 from .log import getLogger
@@ -143,7 +143,7 @@ def bar(message):
         :return:
         """
 
-        def processor(kls, name = None):
+        def processor(kls, name=None):
             kls_name = name if name is not None else kls.__name__.lower()
             if kls_name in self.ws_instances:
                 raise ValueError(
@@ -366,7 +366,7 @@ def add_resource(self, route_str:str, resource, **kwargs):
         :param kwargs:
         :return:
         """
-        return self.router._add_resource(route_str, thing=resource, route_kwargs=kwargs )
+        return self.router.add_resource(route_str, thing=resource, route_kwargs=kwargs )
 
 
     def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> File:
@@ -384,7 +384,7 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Fi
         return self.router.add(route_str)(file_resource)
 
 
-    def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path], recurse = False) -> File:
+    def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path]) -> File:
         """
         TODO - remove calls to `add_staticdir2` and just use `add_staticdir`
         :param route_str:
@@ -447,13 +447,13 @@ def processor(func: ErrorHandler) -> ErrorHandler:
             if error_type in self.error_handlers and write_over is False:
                 old_func = self.error_handlers[error_type]
                 raise ValueError(f"handle_error called twice to handle {error_type} with old {old_func} vs {func}")
-            else:
-                self.error_handlers[error_type] = func
-                return func
+
+            self.error_handlers[error_type] = func
+            return func
 
         return processor
 
-    def add_error_handler(self, handler:T.Callable, error_type: T.Union[HTTPCode, int, Exception, str], override=False):
+    def add_error_handler(self, handler: T.Callable, error_type: T.Union[HTTPCode, int, Exception, str], override=False):
         """
         TODO cull this out or justify it's existence.
 
@@ -563,13 +563,14 @@ def __init__(self,
 
 
     @staticmethod
-    def request_factory_partial(app:'Application', request_kls:StrRequest):
+    def request_factory_partial(app: Application, request_kls: StrRequest):
         """
             A hack to intercept when a new http Request is created deep inside of twisted.web's protocol factory
 
         """
 
         def partial(*args, **kwargs):
+            # pylint: disable=W0212
             request = request_kls(*args, **kwargs)
             request.add_before_render(app._call_before_render)
             request.add_after_render(app._call_after_render)
@@ -642,6 +643,7 @@ def _call_before_render(self, request: StrRequest):
         :param request:
         :return:
         """
+        # pylint: disable=W0703
         for func in self._before_render_handlers:
             try:
                 func(request)
@@ -656,6 +658,7 @@ def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]):
         :param body:
         :return:
         """
+        # pylint: disable=W0703
         for func in self._after_render_handlers:
             try:
                 body = func(request, body)
diff --git a/txweb/lib/__init__.py b/txweb/lib/__init__.py
index 4aca959..bc64449 100644
--- a/txweb/lib/__init__.py
+++ b/txweb/lib/__init__.py
@@ -6,4 +6,4 @@
 from .view_class_assembler import ViewClassResource
 from .view_class_assembler import set_prefilter, set_postfilter
 
-__ALL__ = ["StrRequest", "expose_method", "ViewClassResource", "set_prefilter", "set_postfilter"]
\ No newline at end of file
+__ALL__ = ["StrRequest", "expose_method", "ViewClassResource", "set_prefilter", "set_postfilter"]
diff --git a/txweb/lib/errors/base.py b/txweb/lib/errors/base.py
index 102bd30..91f0c07 100644
--- a/txweb/lib/errors/base.py
+++ b/txweb/lib/errors/base.py
@@ -11,6 +11,7 @@
 
 log = getLogger(__name__)
 
+
 class BaseHandler:
     """
 
@@ -43,4 +44,4 @@ def process(self, request: StrRequest, error: Failure) -> T.Union[None, bool]:
         :param error:
         :return:
         """
-        raise NotImplementedError("Attempting to use Base error handler")
\ No newline at end of file
+        raise NotImplementedError("Attempting to use Base error handler")
diff --git a/txweb/lib/errors/debug.py b/txweb/lib/errors/debug.py
index 5648a38..5b08c06 100644
--- a/txweb/lib/errors/debug.py
+++ b/txweb/lib/errors/debug.py
@@ -70,7 +70,7 @@ def process(self, request: StrRequest, reason: Failure) -> T.Union[None, bool]:
         content = html.ERROR_CONTENT.format(digest=repr(reason.value), error_list=error_list)
         body = html.ERROR_BODY.format(content=content)
 
-        if issubclass(reason.type, HTTPCode):
+        if issubclass(reason.type, http_codes.HTTPCode):
             request.setResponseCode(reason.value.code, reason.value.message)
         else:
             request.setResponseCode(500, "Internal server error")
diff --git a/txweb/lib/errors/default.py b/txweb/lib/errors/default.py
index cb2d5a9..4b47fcc 100644
--- a/txweb/lib/errors/default.py
+++ b/txweb/lib/errors/default.py
@@ -5,9 +5,6 @@
 """
 from __future__ import annotations
 import typing as T
-from pathlib import Path
-import linecache
-from dataclasses import dataclass
 
 from twisted.python.failure import Failure
 from twisted.python.compat import intToBytes
diff --git a/txweb/lib/errors/html.py b/txweb/lib/errors/html.py
index f1f6cef..a6be9bc 100644
--- a/txweb/lib/errors/html.py
+++ b/txweb/lib/errors/html.py
@@ -61,4 +61,4 @@
             
         
     
-"""
\ No newline at end of file
+"""
diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index ec3b06c..d992d44 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -25,9 +25,9 @@ def handle_show(self, request):
 
 from werkzeug.routing import Rule, Submount
 
-from ..resources import ViewFunctionResource, ViewClassResource
-# from txweb.http_codes import Unrenderable
 from txweb.util.basic import get_thing_name
+from ..resources import ViewFunctionResource, ViewClassResource
+
 
 
 EXPOSED_STR = "__exposed__"
@@ -50,13 +50,13 @@ def has_exposed(obj):
     ])
 
 
-def is_exposed(attribute):
-    """
-        Is the provided callable/thing set as exposed?
-    :param attribute:
-    :return:
-    """
-    return has_exposed(attribute, EXPOSED_STR) and is_valid_callable
+# def is_exposed(attribute):
+#     """
+#         Is the provided callable/thing set as exposed?
+#     :param attribute:
+#     :return:
+#     """
+#     return has_exposed(attribute)
 
 
 def is_viewable(attribute):
@@ -71,7 +71,7 @@ def is_viewable(attribute):
                         or inspect.iscoroutine(attribute) \
                         or inspect.iscoroutinefunction(attribute)
 
-    return is_exposed(attribute) and is_valid_callable
+    return has_exposed(attribute) and is_valid_callable
 
 
 def is_renderable(kls):
@@ -137,7 +137,7 @@ def find_member(thing, identifier) -> T.Union[T.Callable, bool]:
     :param identifier:
     :return:
     """
-    for name, member in inspect.getmembers(thing, lambda v: hasattr(v, identifier)):
+    for _, member in inspect.getmembers(thing, lambda v: hasattr(v, identifier)):
         return member
 
     return False
@@ -160,6 +160,7 @@ def view_assembler(prefix, kls, route_args):
     :param route_args:
     :return:
     """
+    # pylint: disable=R0914
     endpoints = {}
 
     rules = []
diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index c915c06..16c0ded 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -22,9 +22,11 @@
 
 from autobahn.twisted.websocket import WebSocketServerProtocol
 
+from txweb.log import getLogger
+
 from .message_handler import MessageHandler
 
-from txweb.log import getLogger
+
 
 
 class WSProtocol(WebSocketServerProtocol):
@@ -40,7 +42,7 @@ class WSProtocol(WebSocketServerProtocol):
     def __init__(self):
         self.pending_responses = {}
 
-        super(WSProtocol, self).__init__()
+        super().__init__()
         # WebSocketServerProtocol.__init__(self, *args, **kwargs)
 
         self.identity = None
@@ -143,6 +145,43 @@ def ask(self, endpoint, **values):
         else:
             raise EnvironmentError("Maximum # of pending asks reached")
 
+    def handleResponse(self, message):
+        """
+            Server side asked client a question, shunt this message through the
+             deferred_asks collection of deferreds to the appropriate/waiting endpoint
+
+        :param message:
+        :return:
+        """
+        caller_id = message.get("caller_id", None)
+
+        if caller_id is not None and caller_id in self.deferred_asks:
+            d = self.deferred_asks[caller_id]  # type: Deferred
+            d.callback(message.get("result"))
+            del self.deferred_asks[caller_id]
+        else:
+            warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
+
+    def handleEndPoint(self, message):
+        endpoint_func = self.factory.get_endpoint(message['endpoint'])
+        self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
+
+        if endpoint_func is None:
+            self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])
+        else:
+            result = endpoint_func(message)
+
+            if result in [NOT_DONE_YET, None]:
+                return
+            elif isinstance(result, Deferred):
+                return
+            elif message.get("type", default=None) == "ask":
+                self.respond(message, result=result)
+            else:
+                warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
+
+
+
     def onMessage(self, payload, is_binary):
         """
             Could be broken apart into smaller pieces perhaps but this handles routing and
@@ -165,39 +204,16 @@ def onMessage(self, payload, is_binary):
             warnings.warn(f"Corrupt/bad payload: {payload}")
         else:
 
-
             message = MessageHandler(raw_message, self)
             result = None
             endpoint_func = None
 
             if message.get("type") == "response":
-                caller_id = message.get("caller_id", None)
-
-                if caller_id is not None and caller_id in self.deferred_asks:
-                    d = self.deferred_asks[caller_id]  # type: Deferred
-                    d.callback(message.get("result"))
-                    del self.deferred_asks[caller_id]
-                else:
-                    warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
-
+                return self.handleResponse(message)
             elif "endpoint" in message:
-
-                endpoint_func = self.factory.get_endpoint(message['endpoint'])
-                self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
-
-                if endpoint_func is None:
-                    self.my_log.error("Bad endpoint {endpoint}", endpoint=message['endpoint'])
-                else:
-                    result = endpoint_func(message)
+                return self.handleEndPoint(message)
             else:
                 self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message)
 
-            if result in [NOT_DONE_YET, None]:
-                return
-            elif isinstance(result, Deferred):
-                return
-            elif message.get("type", default=None) == "ask":
-                self.respond(message, result=result)
-            else:
-                warnings.warn(f"{endpoint_func} returned {result} but I don't know know how to handle it.")
-                pass
+
+
diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index 9862f0d..ac4f051 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -5,7 +5,7 @@
 from collections import OrderedDict
 import typing as T
 import inspect
-import warnings
+# import warnings
 
 from twisted.web.static import File
 
@@ -21,11 +21,11 @@
 from txweb.util.basic import get_thing_name
 from txweb.lib.str_request import StrRequest
 
+from txweb import http_codes as HTTP_Errors
+
 from .view_function import ViewFunctionResource
 from .view_class import ViewClassResource
 # from .directory import Directory
-
-from txweb import http_codes as HTTP_Errors
 from ..lib import view_class_assembler as vca
 from ..log import getLogger
 
@@ -115,7 +115,7 @@ def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[Endp
 
             elif isinstance(original_thing, resource.Resource):
 
-                self._add_resource(route_str, **common_kwargs)
+                self.add_resource(route_str, **common_kwargs)
 
             elif inspect.isclass(original_thing):
                 self._add_class(route_str, **common_kwargs)
@@ -192,9 +192,9 @@ def _add_resource_cls(self, route_str, endpoint=None, thing=None, route_kwargs=N
         route_kwargs = route_kwargs if route_kwargs is not None else {}
         if endpoint not in self._instances:
             self._instances[endpoint] = thing()
-        self._add_resource(route_str, endpoint=endpoint, thing=self._instances[endpoint], route_kwargs=route_kwargs)
+        self.add_resource(route_str, endpoint=endpoint, thing=self._instances[endpoint], route_kwargs=route_kwargs)
 
-    def _add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None):
+    def add_resource(self, route_str, endpoint=None, thing=None, route_kwargs=None):
         """
 
         :param self:
@@ -276,7 +276,7 @@ def _build_map(self, request):
 
         return self._route_map.bind(**map_bind_kwargs)
 
-    def getChildWithDefault(self, path_element: T.Union[bytes, str], request: StrRequest):
+    def getChildWithDefault(self, _, request: StrRequest):
         """
             Routing resource is mostly ignorant of the larger ecosystem so it either
             returns a resource OR it throws up an errors.HTTPCode
diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py
index 01e0aa7..b3054b6 100644
--- a/txweb/resources/view_function.py
+++ b/txweb/resources/view_function.py
@@ -1,3 +1,6 @@
+"""
+    Convert a simple function into a Resource.
+"""
 from __future__ import annotations
 import typing as T
 
diff --git a/txweb/util/templating.py b/txweb/util/templating.py
index 3c4191e..afebe3d 100644
--- a/txweb/util/templating.py
+++ b/txweb/util/templating.py
@@ -7,13 +7,13 @@
 import typing as T
 from pathlib import Path
 try:  # pragma: no cover
-    import jinja2
+    from jinja2 import FileSystemLoader, Environment, BytecodeCache
+    from jinja2.bccache import Bucket
 except ImportError as failed_import:  # pragma: no cover
     raise EnvironmentError("Jinja2 is not install: pip install jinja2 to use the templating utility") from failed_import
 
 
-from jinja2 import FileSystemLoader, Environment, BytecodeCache
-from jinja2.bccache import Bucket
+
 
 # pragma: no cover
 JINJA2_ENV = None  # type: Environment

From 3b5d21d67954e342ad748152306faae9c9c3cf75 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:18:30 -0700
Subject: [PATCH 155/185] Refactor web_views.py to web_site.py for better
 clarity

Note, I did not use site.py because that is a special file in certain cases with the interpreter and could cause unforseen problems.
---
 examples/chat/server.py                  | 2 +-
 examples/forms/server.py                 | 2 +-
 examples/tictactoe/server.py             | 2 +-
 examples/to_replace/web_views/run.py     | 2 +-
 txweb/application.py                     | 2 +-
 txweb/tests/test_static_file_resource.py | 2 +-
 txweb/tests/test_views.py                | 2 +-
 txweb/{web_views.py => web_site.py}      | 2 +-
 8 files changed, 8 insertions(+), 8 deletions(-)
 rename txweb/{web_views.py => web_site.py} (98%)

diff --git a/examples/chat/server.py b/examples/chat/server.py
index 47a94c4..b5f3ee2 100644
--- a/examples/chat/server.py
+++ b/examples/chat/server.py
@@ -4,7 +4,7 @@
 import json
 import typing as T
 
-from txweb.web_views import WebSite, StrRequest
+from txweb.web_site import WebSite, StrRequest
 from txweb import Application
 
 from zope.interface import Interface, Attribute, implementer
diff --git a/examples/forms/server.py b/examples/forms/server.py
index b0887a1..037678d 100644
--- a/examples/forms/server.py
+++ b/examples/forms/server.py
@@ -1,4 +1,4 @@
-from txweb.web_views import WebSite
+from txweb.web_site import WebSite
 from txweb.util.reloader import reloader
 
 from twisted.internet import reactor
diff --git a/examples/tictactoe/server.py b/examples/tictactoe/server.py
index 484dbbf..0d8e471 100644
--- a/examples/tictactoe/server.py
+++ b/examples/tictactoe/server.py
@@ -2,7 +2,7 @@
 import sys
 from pathlib import Path
 
-from txweb.web_views import WebSite
+from txweb.web_site import WebSite
 from txweb.lib.str_request import StrRequest
 
 from twisted.internet import reactor
diff --git a/examples/to_replace/web_views/run.py b/examples/to_replace/web_views/run.py
index 828cacc..eaa31b0 100644
--- a/examples/to_replace/web_views/run.py
+++ b/examples/to_replace/web_views/run.py
@@ -1,4 +1,4 @@
-from txweb.web_views import website
+from txweb.web_site import website
 
 from twisted.application import service, internet as app_internet
 from twisted.web import static, server
diff --git a/txweb/application.py b/txweb/application.py
index 58ec6ac..7ab1fb3 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -44,7 +44,7 @@
 from .resources import RoutingResource
 # from .resources import SimpleFile, Directory
 from .lib import StrRequest, expose_method, set_prefilter, set_postfilter
-from .web_views import WebSite
+from .web_site import WebSite
 from .http_codes import HTTPCode
 from .lib.errors.default import DefaultHandler, BaseHandler
 
diff --git a/txweb/tests/test_static_file_resource.py b/txweb/tests/test_static_file_resource.py
index e2a2029..25e39f0 100644
--- a/txweb/tests/test_static_file_resource.py
+++ b/txweb/tests/test_static_file_resource.py
@@ -1,5 +1,5 @@
 
-from txweb.web_views import WebSite
+from txweb.web_site import WebSite
 
 from twisted.web.server import NOT_DONE_YET
 from twisted.web.static import File
diff --git a/txweb/tests/test_views.py b/txweb/tests/test_views.py
index 051a6e2..c34ef2b 100644
--- a/txweb/tests/test_views.py
+++ b/txweb/tests/test_views.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from txweb import web_views
+from txweb import web_site
 from .helper import ensureBytes, MockRequest
 
 # def getChildForRequest(resource, request):
diff --git a/txweb/web_views.py b/txweb/web_site.py
similarity index 98%
rename from txweb/web_views.py
rename to txweb/web_site.py
index 539fcf0..b7fb1b6 100644
--- a/txweb/web_views.py
+++ b/txweb/web_site.py
@@ -16,7 +16,7 @@
 
 # txweb imports
 import txweb
-from txweb import resources as txw_resources
+# from txweb import resources as txw_resources
 from txweb.lib.str_request import StrRequest
 from txweb.lib import view_class_assembler as vca
 from txweb.resources import RoutingResource

From bfa76f458bd11fa4a183097887c0aa24d8f0ed48 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:18:40 -0700
Subject: [PATCH 156/185] Annotations

---
 txweb/lib/wsprotocol.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 16c0ded..9d7ef69 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -163,6 +163,12 @@ def handleResponse(self, message):
             warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks")
 
     def handleEndPoint(self, message):
+        """
+            Handles incoming ask and tell messages.
+
+        :param message:
+        :return:
+        """
         endpoint_func = self.factory.get_endpoint(message['endpoint'])
         self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint'])
 
@@ -184,7 +190,7 @@ def handleEndPoint(self, message):
 
     def onMessage(self, payload, is_binary):
         """
-            Could be broken apart into smaller pieces perhaps but this handles routing and
+            Entry point for incoming messages from the client
 
         :param payload:
         :param is_binary:

From 6f15f4a1be84808221b42e3b8d3d3eb7d90cc426 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:18:47 -0700
Subject: [PATCH 157/185] typo

---
 lint.bat | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lint.bat b/lint.bat
index 1aae1ac..1046da8 100644
--- a/lint.bat
+++ b/lint.bat
@@ -1 +1 @@
-pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -W0221 -d R0903 -d R0901 -d R0902 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb
+pylint --ignore-patterns=test_* --ignore=helper.py,conftest.py -d W0221 -d R0903 -d R0901 -d R0902 -d R1705 -d C0103 -d C0301 -d C0305 -d C0115 -d W0603 -d C0303 txweb

From 962ae1aaaacfde027b8ede324b7f1bfc4a5ba96a Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:20:09 -0700
Subject: [PATCH 158/185] removed dead code

---
 txweb/web_site.py | 31 +++----------------------------
 1 file changed, 3 insertions(+), 28 deletions(-)

diff --git a/txweb/web_site.py b/txweb/web_site.py
index b7fb1b6..8db36c6 100644
--- a/txweb/web_site.py
+++ b/txweb/web_site.py
@@ -8,7 +8,7 @@
 #stdlib
 import typing as T
 import pathlib
-import copy
+# import copy
 
 # twisted imports
 from twisted.python import failure
@@ -18,9 +18,9 @@
 import txweb
 # from txweb import resources as txw_resources
 from txweb.lib.str_request import StrRequest
-from txweb.lib import view_class_assembler as vca
+# from txweb.lib import view_class_assembler as vca
 from txweb.resources import RoutingResource
-from txweb import http_codes as HTTP_Errors
+# from txweb import http_codes as HTTP_Errors
 from txweb.log import getLogger
 
 
@@ -30,31 +30,6 @@
 ErrorHandler = T.NewType("ErrorHandler", T.Callable[['Website', StrRequest, failure.Failure], None])
 LIBRARY_TEMPLATE_PATH = pathlib.Path(txweb.__file__).parent / "templates"
 
-# class _RoutingSiteConnectors(server.Site):
-#     """
-#         Purpose: provide hooks to the RoutingResource assigned to self.resource
-#     """
-#     resource: RoutingResource
-#
-    # def add(self, route_str: str, **kwargs: T.Optional[T.Dict[str, T.Any]]) -> ResourceView:
-    #     """
-    #         :param route_str: A valid werkzeug routing url
-    #         :param kwargs: optional keyword arguments for werkzeug routing
-    #         :return:
-    #     """
-    #     return self.resource.add(route_str, **kwargs)
-#
-#
-#
-#     def add_resource(self, route_str: str,
-#                      rsrc: resource.Resource,
-#                      **kwargs: T.Dict[str, T.Any]) -> ResourceView:
-#         return self.resource.add(route_str, **kwargs)(rsrc)
-#
-#     def expose(self, route_str, **route_kwargs) -> T.Callable:
-#         return vca.expose(route_str, **route_kwargs)
-
-
 class WebSite(server.Site):
     """
         Public side of the web_views class collection.

From 90accc1a0c05f4cc47895312615503d487fbdc5e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 21:27:20 -0700
Subject: [PATCH 159/185] Final push with pylint.

---
 txweb/lib/wsprotocol.py          |  4 ++--
 txweb/resources/view_class.py    |  1 +
 txweb/resources/view_function.py | 15 +++++++++++++++
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 9d7ef69..0e3495b 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -215,9 +215,9 @@ def onMessage(self, payload, is_binary):
             endpoint_func = None
 
             if message.get("type") == "response":
-                return self.handleResponse(message)
+                self.handleResponse(message)
             elif "endpoint" in message:
-                return self.handleEndPoint(message)
+                self.handleEndPoint(message)
             else:
                 self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message)
 
diff --git a/txweb/resources/view_class.py b/txweb/resources/view_class.py
index b73d3ae..aebb554 100644
--- a/txweb/resources/view_class.py
+++ b/txweb/resources/view_class.py
@@ -32,6 +32,7 @@ class ViewClassResource(resource.Resource):
     def __init__(self, kls_view, instance=None):
         self.kls_view = kls_view
         self.instance = instance
+        super().__init__()
 
     def render(self, request: StrRequest) -> T.Union[bytes, NotDoneYet]:
         """
diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py
index b3054b6..20a6d8a 100644
--- a/txweb/resources/view_function.py
+++ b/txweb/resources/view_function.py
@@ -18,6 +18,9 @@
 
 
 class ViewFunctionResource(resource.Resource):
+    """
+        Given a callable, convert it into a renderable Resource instance
+    """
 
     isLeaf: T.ClassVar[T.Union[bool, int]] = True
 
@@ -31,9 +34,21 @@ def __init__(self, func: T.Callable,
 
     @classmethod
     def Wrap(cls, func):
+        """
+        Utility decorator
+        :param func:
+        :return:
+        """
         return cls(func)
 
     def render(self, request) -> T.Union[int, T.ByteString]:
+        """
+        Called internally by twisted.web this executes the wrapped callable, first calling
+         assigned pre and then post filters.
+
+        :param request:
+        :return:
+        """
 
         request_view_kwargs = getattr(request, "route_args", {})
 

From 05d3e4663b3dfe370e5aa4142ae4134715c0bab3 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Fri, 4 Dec 2020 22:44:53 -0700
Subject: [PATCH 160/185] Move all of the prior examples out of the way so they
 can be replaced.

---
 examples/{ => to_update}/chat/index.html                     | 0
 examples/{ => to_update}/chat/script.js                      | 0
 examples/{ => to_update}/chat/server.py                      | 0
 examples/{ => to_update}/forms/index.html                    | 0
 examples/{ => to_update}/forms/server.py                     | 0
 examples/{ => to_update}/tictactoe/game.py                   | 0
 examples/{ => to_update}/tictactoe/game_session.py           | 0
 examples/{ => to_update}/tictactoe/response_request_types.py | 0
 examples/{ => to_update}/tictactoe/server.py                 | 0
 examples/{ => to_update}/tictactoe/static/index.html         | 0
 examples/{ => to_update}/tictactoe/static/script.js          | 0
 examples/{ => to_update}/tictactoe/test_game.py              | 0
 12 files changed, 0 insertions(+), 0 deletions(-)
 rename examples/{ => to_update}/chat/index.html (100%)
 rename examples/{ => to_update}/chat/script.js (100%)
 rename examples/{ => to_update}/chat/server.py (100%)
 rename examples/{ => to_update}/forms/index.html (100%)
 rename examples/{ => to_update}/forms/server.py (100%)
 rename examples/{ => to_update}/tictactoe/game.py (100%)
 rename examples/{ => to_update}/tictactoe/game_session.py (100%)
 rename examples/{ => to_update}/tictactoe/response_request_types.py (100%)
 rename examples/{ => to_update}/tictactoe/server.py (100%)
 rename examples/{ => to_update}/tictactoe/static/index.html (100%)
 rename examples/{ => to_update}/tictactoe/static/script.js (100%)
 rename examples/{ => to_update}/tictactoe/test_game.py (100%)

diff --git a/examples/chat/index.html b/examples/to_update/chat/index.html
similarity index 100%
rename from examples/chat/index.html
rename to examples/to_update/chat/index.html
diff --git a/examples/chat/script.js b/examples/to_update/chat/script.js
similarity index 100%
rename from examples/chat/script.js
rename to examples/to_update/chat/script.js
diff --git a/examples/chat/server.py b/examples/to_update/chat/server.py
similarity index 100%
rename from examples/chat/server.py
rename to examples/to_update/chat/server.py
diff --git a/examples/forms/index.html b/examples/to_update/forms/index.html
similarity index 100%
rename from examples/forms/index.html
rename to examples/to_update/forms/index.html
diff --git a/examples/forms/server.py b/examples/to_update/forms/server.py
similarity index 100%
rename from examples/forms/server.py
rename to examples/to_update/forms/server.py
diff --git a/examples/tictactoe/game.py b/examples/to_update/tictactoe/game.py
similarity index 100%
rename from examples/tictactoe/game.py
rename to examples/to_update/tictactoe/game.py
diff --git a/examples/tictactoe/game_session.py b/examples/to_update/tictactoe/game_session.py
similarity index 100%
rename from examples/tictactoe/game_session.py
rename to examples/to_update/tictactoe/game_session.py
diff --git a/examples/tictactoe/response_request_types.py b/examples/to_update/tictactoe/response_request_types.py
similarity index 100%
rename from examples/tictactoe/response_request_types.py
rename to examples/to_update/tictactoe/response_request_types.py
diff --git a/examples/tictactoe/server.py b/examples/to_update/tictactoe/server.py
similarity index 100%
rename from examples/tictactoe/server.py
rename to examples/to_update/tictactoe/server.py
diff --git a/examples/tictactoe/static/index.html b/examples/to_update/tictactoe/static/index.html
similarity index 100%
rename from examples/tictactoe/static/index.html
rename to examples/to_update/tictactoe/static/index.html
diff --git a/examples/tictactoe/static/script.js b/examples/to_update/tictactoe/static/script.js
similarity index 100%
rename from examples/tictactoe/static/script.js
rename to examples/to_update/tictactoe/static/script.js
diff --git a/examples/tictactoe/test_game.py b/examples/to_update/tictactoe/test_game.py
similarity index 100%
rename from examples/tictactoe/test_game.py
rename to examples/to_update/tictactoe/test_game.py

From a4e4ad67f9d588da00f1f492f31f7393302fc63e Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 10:51:44 -0700
Subject: [PATCH 161/185] Starting to rename Application to Texas for project
 branding

---
 txweb/__init__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/txweb/__init__.py b/txweb/__init__.py
index 5da66b4..9de4e7c 100644
--- a/txweb/__init__.py
+++ b/txweb/__init__.py
@@ -20,4 +20,5 @@ def bar(request):
 from txweb.application import Application
 
 App = Application
+Texas = Application
 __all__ = ['NOT_DONE_YET', "App", "Application"]

From cee4a634d89df264fb855f06ed96f975133825b4 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 10:52:21 -0700
Subject: [PATCH 162/185] Silence alarm about using eval

---
 txweb/application.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/txweb/application.py b/txweb/application.py
index 7ab1fb3..ea29628 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -176,6 +176,8 @@ def _eval_annotation(statement, func):
         :param func:
         :return:
         """
+        # There is no way around using eval given how I use annotation's for type casting and conversion
+        # pylint: disable=W0123
         return statement if not isinstance(statement, str) else eval(statement, vars(sys.modules[func.__module__]))
 
 

From 1f7a7dbeb755d1cf2d3be43672525771c3554f04 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 10:52:44 -0700
Subject: [PATCH 163/185] Added for use with user application development

---
 txweb/application.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/txweb/application.py b/txweb/application.py
index ea29628..89782ae 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -563,6 +563,17 @@ def __init__(self,
         self._before_render_handlers = []
         self._after_render_handlers = []
 
+        self.__post_init__()
+
+
+    def __post_init__(self) -> None:
+        """
+        Just a stub to make it easier for subclasses that don't want to mess with
+          overloading __init__.
+
+        :return:
+        """
+
 
     @staticmethod
     def request_factory_partial(app: Application, request_kls: StrRequest):

From 8e84c50ee1fa0924287b1ec16ae3dc174781f68c Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:41:50 -0700
Subject: [PATCH 164/185] Change to expose HTTP codes as part of class def

---
 txweb/http_codes.py | 84 +++++++++++++++++++++++++++++++++------------
 1 file changed, 62 insertions(+), 22 deletions(-)

diff --git a/txweb/http_codes.py b/txweb/http_codes.py
index b6c1952..05fdb85 100644
--- a/txweb/http_codes.py
+++ b/txweb/http_codes.py
@@ -7,6 +7,7 @@
 """
 import typing as T
 
+
 class HTTPCode(RuntimeError):
     """
     Arguments:
@@ -22,76 +23,115 @@ def __init__(self, code:int, message:str, exc:T.Optional[Exception]=None):
         self.message = message
         self.exc = exc
 
+
 class HTTP3xx(HTTPCode):
     """
     Arguments:
         redirect: either an absolute or relative URL to tell the client to redirect too
     """
-    def __init__(self, code, redirect, message="3xx Choices"):
+    def __init__(self, code: int, redirect: T.Union[str, bytes], message: str = "3xx Choices"):
         self.redirect = redirect
         super().__init__(code, message=message)
 
+
 class HTTP301(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(301, redirect, "Moved Permanently")
+    CODE = 301
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, "Moved Permanently")
+
 
 class HTTP302(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(302, redirect, "FOUND")
+    CODE = 302
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, "FOUND")
+
 
 class HTTP303(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(303, redirect, message="See Other")
+    CODE = 303
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, message="See Other")
+
 
 class HTTP304(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(304, redirect, "Not Modified")
+    CODE = 304
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, "Not Modified")
+
 
 class HTTP307(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(307, redirect, "Temporary Redirect")
+    CODE = 307
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, "Temporary Redirect")
+
 
 class HTTP308(HTTP3xx):
-    def __init__(self, redirect):
-        super().__init__(308, redirect, "Permanent Redirect")
+    CODE = 308
+
+    def __init__(self, redirect: T.Union[str, bytes]):
+        super().__init__(self.CODE, redirect, "Permanent Redirect")
 
 
 class HTTP4xx(HTTPCode):
     pass
 
+
 class HTTP400(HTTP4xx):
+    CODE = 400
+
     def __init__(self):
-        super().__init__(400, "Bad Request")
+        super().__init__(self.CODE, "Bad Request")
+
 
 class HTTP401(HTTP4xx):
+    CODE = 401
+
     def __init__(self):
-        super().__init__(401, "Unauthorized")
+        super().__init__(self.CODE, "Unauthorized")
+
 
 class HTTP403(HTTP4xx):
+    CODE = 403
+
     def __init__(self):
-        super().__init__(403, "Forbidden")
+        super().__init__(self.CODE, "Forbidden")
+
 
 class HTTP404(HTTP4xx):
+    CODE = 404
+
     def __init__(self, exc=None):
-        super().__init__(404, "Resource not found", exc=exc)
+        super().__init__(self.CODE, "Resource not found", exc=exc)
+
 
 class HTTP410(HTTP4xx):
+    CODE = 410
+
     def __init__(self):
-        super().__init__(410, "Gone")
+        super().__init__(self.CODE, "Gone")
 
 
 class HTTP405(HTTP4xx):
+    CODE = 405
+
     def __init__(self, exc=None):
-        super().__init__(405, "Method not allowed", exc=exc)
+        super().__init__(self.CODE, "Method not allowed", exc=exc)
 
 
 class HTTP5xx(HTTPCode):
     pass
 
+
 class HTTP500(HTTP5xx):
+    CODE = 500
+
     def __init__(self, message="Internal Server Error"):
-        super().__init__(500, message)
+        super().__init__(self.CODE, message)
+
 
 class Unrenderable(HTTP5xx):
-    def __init__(self, message):
-        super().__init__(500, message)
+    pass

From 7e1cd8a51a9ef77a982e6287934779c5f6223edf Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:42:38 -0700
Subject: [PATCH 165/185] whoops, broke the reloader for situations where
 ignore_prefix wasn't provided.

---
 txweb/util/reloader.py | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index dc5b588..c7e00f2 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -100,16 +100,30 @@ def build_list(
     def is_list(obj):
         return obj is not None and isinstance(obj, list)
 
+    def is_prefixed(filepath: pathlib.Path):
+        if not filepath.is_file():
+            return False
+        elif is_list(ignore_prefix):
+            if any([filepath.name.startswith(prefix) for prefix in ignore_prefix]) is True:
+                return True
+        elif ignore_prefix is not None:
+            if filepath.name.startswith(ignore_prefix) is True:
+                return True
+
+        return False
+
+
+
+
     for pathobj in root_dir.iterdir():
         if pathobj.is_dir():
             build_list(pathobj, watch_self=False, ignore_prefix=ignore_prefix)
         elif pathobj.name.endswith(".py") and not (pathobj.name.endswith(".pyc") or pathobj.name.endswith(".pyo")):
             stat = pathobj.stat()
-            if is_list(ignore_prefix) and any([pathobj.name.startswith(prefix) for prefix in ignore_prefix]) is False:
-                WATCH_LIST[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,)
+            if is_prefixed(pathobj):
+                continue
             else:
-                # print("Ignoring", pathobj.name)
-                pass
+                WATCH_LIST[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,)
         else:
             pass
 

From 5729b247dd0eb41849a920bef58ef20aa8280f06 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:47:27 -0700
Subject: [PATCH 166/185] Last push of pylint cleanup

---
 txweb/application.py             |  2 +-
 txweb/lib/errors/debug.py        |  6 +++---
 txweb/lib/str_request.py         | 20 ++++----------------
 txweb/lib/wsprotocol.py          |  2 --
 txweb/resources/routing.py       | 10 ++++++----
 txweb/resources/view_class.py    |  2 +-
 txweb/resources/view_function.py |  1 +
 txweb/util/reloader.py           | 16 +++++++++++++---
 txweb/web_site.py                |  4 ++--
 9 files changed, 31 insertions(+), 32 deletions(-)

diff --git a/txweb/application.py b/txweb/application.py
index 89782ae..0fd178a 100644
--- a/txweb/application.py
+++ b/txweb/application.py
@@ -251,7 +251,7 @@ def argument_decorator(message: MessageHandler):
             kwargs = {}
 
             for name, default in arg_keys.items():
-                kwargs[name] = message.args(name, default=default) #TODO also use annotation for type-casting
+                kwargs[name] = message.args(name, default=default)
                 if name in converter_keys:
                     try:
                         kwargs[name] = converter_keys[name](kwargs[name])
diff --git a/txweb/lib/errors/debug.py b/txweb/lib/errors/debug.py
index 5b08c06..0c1ba65 100644
--- a/txweb/lib/errors/debug.py
+++ b/txweb/lib/errors/debug.py
@@ -1,8 +1,8 @@
 """
-    Base, Default, and generic Debug error handlers for txweb.
-
+    In progress generic Debug error handlers for txweb.
 
 """
+# pylint: disable=E1101
 from __future__ import annotations
 import typing as T
 from pathlib import Path
@@ -10,7 +10,7 @@
 from dataclasses import dataclass
 
 from twisted.python.failure import Failure
-from twisted.python.compat import intToBytes
+# from twisted.python.compat import intToBytes
 
 from txweb.lib.str_request import StrRequest
 from txweb.log import getLogger
diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py
index 6963efd..94a0e8e 100644
--- a/txweb/lib/str_request.py
+++ b/txweb/lib/str_request.py
@@ -20,25 +20,16 @@
 from urllib.parse import parse_qs
 import typing as T
 
-# from twisted.python import reflect
-# noinspection PyProtectedMember
-# from twisted.web.error import UnsupportedMethod
+
 from twisted.web.server import Request, NOT_DONE_YET
 # from twisted.web.server import supportedMethods
 from twisted.web.http import FOUND
 from twisted.web import resource
-# from twisted.web import http
-# noinspection PyProtectedMember
-# from twisted.web.http import _parseHeader
-# noinspection PyProtectedMember
-# from twisted.python.compat import _PY3, _PY37PLUS
-# from twisted.python.compat import nativeString
-# from twisted.python.compat import escape
 from twisted.python.compat import intToBytes
 
 from werkzeug.formparser import FormDataParser
 from werkzeug.datastructures import MultiDict
-from werkzeug import FileStorage
+from werkzeug.datastructures import FileStorage
 
 from ..log import getLogger
 from ..http_codes import HTTP500
@@ -309,11 +300,8 @@ def _processFormData(self, content_type, content_length):
             content_type = content_type.decode("utf-8")  # type: str
 
         if ";" in content_type:
-            """
-                TODO Possible need to replace some of the header processing logic as boundary part of content-type 
-                leaks through.
-                eg "Content-type": "some/mime_type;boundary=----BLAH"
-            """
+            #  TODO Possible need to replace some of the header processing logic as boundary part of content-type
+            #   leaks through.  eg "Content-type": "some/mime_type;boundary=----BLAH"
             content_type, boundary = content_type.split(";", 1)
             if "=" in boundary:
                 _, boundary = boundary.split("=", 1)
diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py
index 0e3495b..976bb6a 100644
--- a/txweb/lib/wsprotocol.py
+++ b/txweb/lib/wsprotocol.py
@@ -211,8 +211,6 @@ def onMessage(self, payload, is_binary):
         else:
 
             message = MessageHandler(raw_message, self)
-            result = None
-            endpoint_func = None
 
             if message.get("type") == "response":
                 self.handleResponse(message)
diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py
index ac4f051..d5f7c73 100644
--- a/txweb/resources/routing.py
+++ b/txweb/resources/routing.py
@@ -96,11 +96,13 @@ def add(self, route_str: str, **kwargs: T.Dict[str, T.Any]):
         :return:
         """
 
-        assert "endpoint" not in kwargs, \
-            "Undefined behavior to use RoutingResource.add('/some/route/', endpoint='something', ...)"
-        assert isinstance(route_str, str) is True, "add must be called with RoutingResource.add('/some/route/', **...)"
+        if "endpoint" in kwargs:
+            raise ValueError("Undefined behavior to use RoutingResource.add('/some/route/', endpoint='something', ...)")
+
+        if isinstance(route_str, str) is False:
+            raise ValueError(f"position 1 argument, route_str must be a str, got {route_str!r} instead")
+
 
-        # todo swap object for
         def processor(original_thing: T.Union[EndpointCallable, object]) -> T.Union[EndpointCallable, object]:
 
             endpoint_name = get_thing_name(original_thing)
diff --git a/txweb/resources/view_class.py b/txweb/resources/view_class.py
index aebb554..5011eee 100644
--- a/txweb/resources/view_class.py
+++ b/txweb/resources/view_class.py
@@ -14,7 +14,7 @@ def render(request):
 from __future__ import annotations
 import typing as T
 from twisted.web import resource
-from twisted.web.server import NOT_DONE_YET
+# from twisted.web.server import NOT_DONE_YET
 
 from txweb.util.basic import sanitize_render_output
 
diff --git a/txweb/resources/view_function.py b/txweb/resources/view_function.py
index 20a6d8a..cf70e46 100644
--- a/txweb/resources/view_function.py
+++ b/txweb/resources/view_function.py
@@ -31,6 +31,7 @@ def __init__(self, func: T.Callable,
         self.func = func
         self.prefilter = prefilter
         self.postfilter = postfilter
+        super().__init__()
 
     @classmethod
     def Wrap(cls, func):
diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py
index c7e00f2..3b224c5 100644
--- a/txweb/util/reloader.py
+++ b/txweb/util/reloader.py
@@ -58,15 +58,21 @@ def main():
 
 
 RUN_RELOADER = True
-SENTINEL_CODE = 7211
+SENTINEL_CODE = 7211  # I wish I remembered why I picked this number.
 SENTINEL_NAME = "RELOADER_ACTIVE"
+#  os._exit is a hard and fast exit from a process, avoiding most cleanup handlers which makes it ideal when used
+#    in a child process.
+#  https://stackoverflow.com/a/9591397/9908
 SENTINEL_OS_EXIT = True
 
+
 """
    "Reason" is here https://code.djangoproject.com/ticket/2330
    TODO - Figure out why threading needs to be imported as this feels like a problem within stdlib.
 """
+
 try:
+    # pylint: disable=W0611
     import threading
 except ImportError:
     pass
@@ -163,9 +169,13 @@ def watch_thread(os_exit: bool = SENTINEL_OS_EXIT, watch_self: bool = False, ign
     :param ignore_prefix: Ignore files starting with this prefix
     :return:
     """
-    exit_func = os._exit if os_exit is True else sys.exit
 
-    build_list(pathlib.Path(os.getcwd()), watch_self=watch_self, ignore_prefix=ignore_prefix)
+    # pylint: disable=W0212
+    exit_func = os._exit if os_exit is True else sys.exit
+    base_path = pathlib.Path(os.getcwd())
+    print(f"RELOADER: Base is {base_path}")
+    build_list(base_path, watch_self=watch_self, ignore_prefix=ignore_prefix)
+    print(f"RELOADER: Watching {len(WATCH_LIST)} files for changes")
 
     while True:
         if file_changed():
diff --git a/txweb/web_site.py b/txweb/web_site.py
index 8db36c6..0288a6e 100644
--- a/txweb/web_site.py
+++ b/txweb/web_site.py
@@ -15,7 +15,7 @@
 from twisted.web import server
 
 # txweb imports
-import txweb
+# import txweb
 # from txweb import resources as txw_resources
 from txweb.lib.str_request import StrRequest
 # from txweb.lib import view_class_assembler as vca
@@ -28,7 +28,7 @@
 
 ResourceView = T.Type["_ResourceThing"]
 ErrorHandler = T.NewType("ErrorHandler", T.Callable[['Website', StrRequest, failure.Failure], None])
-LIBRARY_TEMPLATE_PATH = pathlib.Path(txweb.__file__).parent / "templates"
+# LIBRARY_TEMPLATE_PATH = pathlib.Path(txweb.__file__).parent / "templates"
 
 class WebSite(server.Site):
     """

From 6da3dd0455a897e6d1ce05c9e3f5130edf27c85c Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:48:43 -0700
Subject: [PATCH 167/185] Added utility property to make accessing the
 connection unique id easier/quicker

---
 txweb/lib/message_handler.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index f1f973e..909e08a 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -62,6 +62,15 @@ def get(self, key, default=None, type=None):
 
         return value
 
+    @property
+    def identity(self):
+        """
+            Utility to make it quicker to access the connection's unique identifier
+        :return:
+        """
+        return self.connection.identity
+
+    # pylint: disable=redefined-builtin
     def args(self, key, default=None, type=None):
         """
             A more explicit/direct getter that looks for an `args` dictionary in the client message and

From 2e70db086900a000e5961515359ac6c3f1b375f7 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:48:56 -0700
Subject: [PATCH 168/185] More pylint

---
 txweb/lib/message_handler.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py
index 909e08a..2f43780 100644
--- a/txweb/lib/message_handler.py
+++ b/txweb/lib/message_handler.py
@@ -13,6 +13,8 @@
 
 
 if T.TYPE_CHECKING or False:  # pragma: no cover
+    # pylint: disable=cyclic-import
+    # used for type hinting with PyCharm but this would break the app if it was ever imported.
     # recursion import
     from .wsprotocol import WSProtocol
 
@@ -47,6 +49,7 @@ def items(self):  # pragma: no cover
     def values(self):  # pragma: no cover
         return self.raw_message.values()
 
+    # pylint: disable=redefined-builtin
     def get(self, key, default=None, type=None):
 
         try:

From 80ad9285851cb104940cc54c63feca8bb1742b7d Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:49:33 -0700
Subject: [PATCH 169/185] speed up change, I don't need to grab the pre/post
 filters repeatedly, just once

---
 txweb/lib/view_class_assembler.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index d992d44..4b174bc 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -178,13 +178,14 @@ def view_assembler(prefix, kls, route_args):
             and hasattr(getattr(instance, name), EXPOSED_STR)
         }
 
+        prefilter = find_member(instance, PREFILTER_ID)
+        postfilter = find_member(instance, POSTFILTER_ID)
+
         for name, bound_method in attributes.items():
 
             sub_rule = getattr(bound_method, EXPOSED_RULE)
             bound_endpoint = get_thing_name(bound_method)
             rule = Rule(sub_rule.route, **sub_rule.route_kwargs, endpoint=bound_endpoint)
-            prefilter = find_member(instance, PREFILTER_ID)
-            postfilter = find_member(instance, POSTFILTER_ID)
 
             endpoints[bound_endpoint] = ViewFunctionResource(bound_method, prefilter=prefilter, postfilter=postfilter)
             rules.append(rule)

From baa83a72ace10cd2c75100c3484a329abc360697 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 11:49:42 -0700
Subject: [PATCH 170/185] type hint

---
 txweb/lib/view_class_assembler.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py
index 4b174bc..cc4b6bc 100644
--- a/txweb/lib/view_class_assembler.py
+++ b/txweb/lib/view_class_assembler.py
@@ -183,7 +183,7 @@ def view_assembler(prefix, kls, route_args):
 
         for name, bound_method in attributes.items():
 
-            sub_rule = getattr(bound_method, EXPOSED_RULE)
+            sub_rule = getattr(bound_method, EXPOSED_RULE)  # type: ExposeSubRule
             bound_endpoint = get_thing_name(bound_method)
             rule = Rule(sub_rule.route, **sub_rule.route_kwargs, endpoint=bound_endpoint)
 

From 9295d7b54737a26fde0f06a4ec59501b037acca4 Mon Sep 17 00:00:00 2001
From: devdave 
Date: Sat, 5 Dec 2020 12:30:59 -0700
Subject: [PATCH 171/185] cleaner chat service example

---
 examples/chat/chat.js   | 50 +++++++++++++++++++++++
 examples/chat/home.html | 44 ++++++++++++++++++++
 examples/chat/server.py | 89 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 183 insertions(+)
 create mode 100644 examples/chat/chat.js
 create mode 100644 examples/chat/home.html
 create mode 100644 examples/chat/server.py

diff --git a/examples/chat/chat.js b/examples/chat/chat.js
new file mode 100644
index 0000000..226df97
--- /dev/null
+++ b/examples/chat/chat.js
@@ -0,0 +1,50 @@
+export class ChatClient {
+    constructor(socket, chat_window, input, send_btn) {
+        this.socket = socket;
+        this.chat_window = document.getElementById(chat_window);
+        this.input = document.getElementById(input);
+        this.send = document.getElementById(send_btn);
+        this.is_registered = false;
+
+        this.send.addEventListener("click", this.on_chat.bind(this));
+        this.socket.register("client.hear", this.provide_hear.bind(this));
+
+
+    }
+
+    async register() {
+        if (this.is_registered == false) {
+            const username = window.prompt("What is your username?", "Anonymous")
+            const result = await this.socket.ask("chat.register", {username});
+            this.is_registered = result
+        }
+    }
+
+    async on_chat(evt) {
+        const text = this.input.value;
+        if(await this.socket.ask("chat.speak", {text}) == true) {
+            this.input.value = "";
+        }
+        else {
+            this.render_output("Client", "Failed to connect with server");
+        }
+    }
+
+    async provide_hear({who, what}) {
+        console.log("Got a message from server", who, what);
+        this.render_output(who, what);
+    }
+
+    render_output(who, what) {
+        const chat_line = `${who}${what}`;
+        const div_line = document.createElement("div")
+        div_line.classList.add("text-msg");
+        div_line.innerHTML = chat_line;
+
+        this.chat_window.appendChild(div_line);
+    }
+
+
+
+
+}
\ No newline at end of file
diff --git a/examples/chat/home.html b/examples/chat/home.html
new file mode 100644
index 0000000..789ed9a
--- /dev/null
+++ b/examples/chat/home.html
@@ -0,0 +1,44 @@
+
+
+
+    
+    Chat client
+    
+
+
+    
+
+
+ +
+ +
+ + \ No newline at end of file diff --git a/examples/chat/server.py b/examples/chat/server.py new file mode 100644 index 0000000..7860df5 --- /dev/null +++ b/examples/chat/server.py @@ -0,0 +1,89 @@ +from pathlib import Path +from dataclasses import dataclass +import sys + +from twisted.internet import reactor +from twisted.python.log import startLogging + +from txweb import Texas +from txweb.lib.message_handler import MessageHandler +from txweb.lib.wsprotocol import WSProtocol +from txweb.util.reloader import reloader + +HERE = Path(__file__).parent + + +app = Texas(__name__) + + +# Serve home.html from http://{HOST}:{PORT}/ +app.add_file("/", HERE / "home.html") +app.add_file("/chat.js", HERE / "chat.js") + +@dataclass(frozen=True) +class User: + name: str + identity: str + emitter: WSProtocol + + +@app.ws_class +class Chat: + + def __init__(self, application: Texas): + self.app = application + self.users = {} + + def on_user_leave(self, identity): + if identity in self.users: + user = self.users[identity] + del self.users[identity] + + self.emit("SERVER", f"{user.name} has disconnected") + + def emit(self, who, what): + print(f"Emitting to {len(self.users)} users") + for identity, user in self.users.items(): + user: User # No magic here, just a typehint + user.emitter.tell("client.hear", who=who, what=what) + + + @app.ws_expose(assign_args=True) + def register(self, message:MessageHandler, username:str = None): + """ + + :param message: + :param username: + :return: + """ + print(f"New registration: {username} @ {message.identity}") + + self.users[message.identity] = User(username, message.identity, message.connection) + message.connection.on_disconnect.addCallback(self.on_user_leave) + self.emit("SERVER", f"{username} has joined.") + + return True + + @app.ws_expose(assign_args=True) + def speak(self, message: MessageHandler, text:str = None): + user = self.users[message.connection.identity] + print(f"New chat: {user.name}@{message.identity} says {text!r}") + self.emit(user.name, text) + + + +def main(): + HOST = "127.0.0.1" + PORT = 7070 + + # provide a websocket connection at ws://{HOST}:{POST}/ws for the client side + app.enable_websockets(f"ws://{HOST}:{PORT}", "/ws") + app.ws_sharelib("/lib") + app.listenTCP(PORT, HOST) + print(f"Listening on http://{HOST}:{PORT}/") + reactor.run() + + +if __name__ == "__main__": + startLogging(sys.stdout) + reloader(main) \ No newline at end of file From 37d475d6125f248c0ff59ab97a70c04591b13291 Mon Sep 17 00:00:00 2001 From: devdave Date: Sat, 5 Dec 2020 12:38:06 -0700 Subject: [PATCH 172/185] Much simpler chat server example --- examples/chat/chat.js | 2 +- examples/chat/home.html | 8 ++++++++ examples/chat/server.py | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/chat/chat.js b/examples/chat/chat.js index 226df97..9d0a477 100644 --- a/examples/chat/chat.js +++ b/examples/chat/chat.js @@ -36,7 +36,7 @@ export class ChatClient { } render_output(who, what) { - const chat_line = `${who}${what}`; + const chat_line = `${who}${what}`; const div_line = document.createElement("div") div_line.classList.add("text-msg"); div_line.innerHTML = chat_line; diff --git a/examples/chat/home.html b/examples/chat/home.html index 789ed9a..a8a5a48 100644 --- a/examples/chat/home.html +++ b/examples/chat/home.html @@ -22,6 +22,14 @@ #outgoing { min-width: 60vw; } + + .text-msg .who { + margin-right: 10px; + } + + .text-msg .who:after { + content: ">"; + } diff --git a/examples/chat/server.py b/examples/chat/server.py index 7860df5..c9b1443 100644 --- a/examples/chat/server.py +++ b/examples/chat/server.py @@ -35,8 +35,10 @@ def __init__(self, application: Texas): self.users = {} def on_user_leave(self, identity): + if identity in self.users: user = self.users[identity] + print(f"{user}@{identity} has disconnected") del self.users[identity] self.emit("SERVER", f"{user.name} has disconnected") From e20a26e03b53f76959cb30902ee0a216d149ff21 Mon Sep 17 00:00:00 2001 From: devdave Date: Sat, 5 Dec 2020 20:28:52 -0700 Subject: [PATCH 173/185] Updated/created new examples --- examples/chat/server.py | 21 +++++++- examples/forms/main.html | 89 ++++++++++++++++++++++++++++++++++ examples/forms/server.py | 100 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 examples/forms/main.html create mode 100644 examples/forms/server.py diff --git a/examples/chat/server.py b/examples/chat/server.py index c9b1443..be47b15 100644 --- a/examples/chat/server.py +++ b/examples/chat/server.py @@ -1,6 +1,7 @@ from pathlib import Path from dataclasses import dataclass import sys +import typing as T from twisted.internet import reactor from twisted.python.log import startLogging @@ -18,10 +19,16 @@ # Serve home.html from http://{HOST}:{PORT}/ app.add_file("/", HERE / "home.html") +# Serve chat.js from http://{HOST}:{PORT}/chat.js app.add_file("/chat.js", HERE / "chat.js") + @dataclass(frozen=True) class User: + """ + House keeping aid, keep track of user names to their connection ID's as well as + hold a reference to their connection. + """ name: str identity: str emitter: WSProtocol @@ -29,12 +36,21 @@ class User: @app.ws_class class Chat: + """ + A working example of a very simple chat client. + + This is fragile code as it doesn't make any effort to catch potential exceptions/errors + and instead just manages registering new users and then forwarding their messages to all + other registered users + """ + + users: T.Dict[str, User] def __init__(self, application: Texas): self.app = application self.users = {} - def on_user_leave(self, identity): + def on_user_leave(self, identity: str): if identity in self.users: user = self.users[identity] @@ -49,7 +65,7 @@ def emit(self, who, what): user: User # No magic here, just a typehint user.emitter.tell("client.hear", who=who, what=what) - + # creates an endpoint `chat.register` @app.ws_expose(assign_args=True) def register(self, message:MessageHandler, username:str = None): """ @@ -66,6 +82,7 @@ def register(self, message:MessageHandler, username:str = None): return True + # endpoint chat.speak @app.ws_expose(assign_args=True) def speak(self, message: MessageHandler, text:str = None): user = self.users[message.connection.identity] diff --git a/examples/forms/main.html b/examples/forms/main.html new file mode 100644 index 0000000..18f6167 --- /dev/null +++ b/examples/forms/main.html @@ -0,0 +1,89 @@ + + + + + Texas forms + + + +
+ A simple form +
+ +
+

Radio

+ + + +
+
+ Checkboxs + + + + + +
+ + +
+
+ +
+ A POST form +
+ +
+

Radio

+ + + +
+
+ Checkboxs + + + + + +
+ + +
+
+ +
+ A file upload form +
+ +
+ + +
+ + +
+
+ +

Form result

+ + + + + \ No newline at end of file diff --git a/examples/forms/server.py b/examples/forms/server.py new file mode 100644 index 0000000..72a5c52 --- /dev/null +++ b/examples/forms/server.py @@ -0,0 +1,100 @@ +from pathlib import Path +import sys + +from twisted.python.log import startLogging +from twisted.internet import reactor + +from txweb import Texas +from txweb.lib.str_request import StrRequest +from txweb.util.reloader import reloader + +HERE = Path(__file__).parent +PORT = 7070 +HOST = "127.0.0.1" + +app = Texas(__name__) + + + +app.add_file("/", HERE / "main.html") + + +@app.route("/simple", methods=["GET"]) +def simple_form(request: StrRequest): + form_fields = list(request.args.items()) + + # Note that for scenarios where a form has multiple inputs with the same name. + check_box = request.args.getlist("check_field") + + return f""" + URL: {request.uri}
+ Arguments:
+ {repr(form_fields)}
+ Check field:
+ {repr(check_box)}
+ """ + +@app.route("/simple_posted_form", methods=["post"]) +def simple_posted(request: StrRequest): + """ + Not the difference of using texas.form versus texas.args + + The reason is that form's that use GET, append their values to the submitting + URL like /my_resource?text_field=blah&check_field=1&check_field=2 + + so .args is for URL arguments. + + while POST'd forms send the form field in the response body. + + :param request: + :return: + """ + form_fields = list(request.form.items()) + + # Note that for scenarios where a form has multiple inputs with the same name. + check_box = request.form.getlist("check_field") + + return f""" + Arguments: + {repr(form_fields)} + Check field: + {repr(check_box)} + """ + +@app.route("/file_upload", methods=["POST"]) +def file_upload(request: StrRequest): + + uploaded_file = request.files.get("upload") + + file_name = uploaded_file.filename + field_name = uploaded_file.name + try: + file = uploaded_file.stream.read().decode("utf-8") + except UnicodeDecodeError: + file = "Unable to decode file to UTF-8" + + file_type = uploaded_file.content_type + + return f""" + Field Name: {field_name}
+ Source file name: {file_name}
+ File type: {file_type}
+
+ Uploaded file:
+
{file}
+ """ + + + +def main(): + + startLogging(sys.stdout) + app.listenTCP(PORT, HOST) + + print(f"Hosting on http://{HOST}:{PORT}/") + + reactor.run() + + +if __name__ == "__main__": + reloader(main) \ No newline at end of file From ba51370840b0569eb337884cc3b435d6e55a026b Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 8 Dec 2020 17:22:02 -0700 Subject: [PATCH 174/185] Update txWeb.iml --- .idea/txWeb.iml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.idea/txWeb.iml b/.idea/txWeb.iml index fc288ed..3f0ad35 100644 --- a/.idea/txWeb.iml +++ b/.idea/txWeb.iml @@ -12,7 +12,17 @@ + + + + + + \ No newline at end of file From 9c1624b713ce74c2eae5be9787633a946d27dd74 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 8 Dec 2020 17:22:14 -0700 Subject: [PATCH 175/185] Update README.md --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7239966..575f484 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,8 @@ Super beta Major issues ============ -Error handling needs to be drastically improved/refactored +Error handling still needs to be improved/refactored -TODO - support for SCRIPT_NAME so the app could theoretically run in a sub directory of a -another application. Purpose & History ====== @@ -53,8 +51,9 @@ def handle_form(request): app.listenTCP(8080) reactor.run() - - - + ``` +Also, please refer to the example's directory (but not to_replace or to_update sub-dirs) for slightly +more filled out examples of use. + From d29b974169ddea457386723e9050227a1967a51f Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 8 Dec 2020 17:23:00 -0700 Subject: [PATCH 176/185] mass doc block reformat to numpy style --- source/api/txweb.lib.errors.rst | 20 ++- source/api/txweb.lib.rst | 16 +-- source/api/txweb.resources.rst | 24 ---- source/api/txweb.rst | 6 +- source/conf.py | 3 + txweb/__init__.py | 2 +- txweb/application.py | 209 +++++++++++++++++++---------- txweb/http_codes.py | 2 +- txweb/lib/errors/__init__.py | 2 +- txweb/lib/errors/base.py | 27 +++- txweb/lib/errors/default.py | 11 +- txweb/lib/message_handler.py | 118 +++++++++++----- txweb/lib/routed_factory.py | 18 ++- txweb/lib/str_request.py | 141 +++++++++++++------ txweb/lib/view_class_assembler.py | 109 +++++++++++---- txweb/lib/wsprotocol.py | 140 +++++++++++++------ txweb/resources/routing.py | 22 ++- txweb/tests/test_messagehandler.py | 4 +- txweb/util/reloader.py | 32 +++-- txweb/util/templating.py | 3 +- txweb/web_site.py | 2 +- 21 files changed, 628 insertions(+), 283 deletions(-) diff --git a/source/api/txweb.lib.errors.rst b/source/api/txweb.lib.errors.rst index e572cf4..44e39b3 100644 --- a/source/api/txweb.lib.errors.rst +++ b/source/api/txweb.lib.errors.rst @@ -4,10 +4,26 @@ txweb.lib.errors package Submodules ---------- -txweb.lib.errors.handler module +txweb.lib.errors.base module +---------------------------- + +.. automodule:: txweb.lib.errors.base + :members: + :undoc-members: + :show-inheritance: + +txweb.lib.errors.debug module +----------------------------- + +.. automodule:: txweb.lib.errors.debug + :members: + :undoc-members: + :show-inheritance: + +txweb.lib.errors.default module ------------------------------- -.. automodule:: txweb.lib.errors.handler +.. automodule:: txweb.lib.errors.default :members: :undoc-members: :show-inheritance: diff --git a/source/api/txweb.lib.rst b/source/api/txweb.lib.rst index eb069b5..9849a2b 100644 --- a/source/api/txweb.lib.rst +++ b/source/api/txweb.lib.rst @@ -12,14 +12,6 @@ Subpackages Submodules ---------- -txweb.lib.at\_wsprotocol module -------------------------------- - -.. automodule:: txweb.lib.at_wsprotocol - :members: - :undoc-members: - :show-inheritance: - txweb.lib.message\_handler module --------------------------------- @@ -52,6 +44,14 @@ txweb.lib.view\_class\_assembler module :undoc-members: :show-inheritance: +txweb.lib.wsprotocol module +--------------------------- + +.. automodule:: txweb.lib.wsprotocol + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/source/api/txweb.resources.rst b/source/api/txweb.resources.rst index ab5944b..fb34fe1 100644 --- a/source/api/txweb.resources.rst +++ b/source/api/txweb.resources.rst @@ -4,22 +4,6 @@ txweb.resources package Submodules ---------- -txweb.resources.directory module --------------------------------- - -.. automodule:: txweb.resources.directory - :members: - :undoc-members: - :show-inheritance: - -txweb.resources.error module ----------------------------- - -.. automodule:: txweb.resources.error - :members: - :undoc-members: - :show-inheritance: - txweb.resources.routing module ------------------------------ @@ -28,14 +12,6 @@ txweb.resources.routing module :undoc-members: :show-inheritance: -txweb.resources.simple\_file module ------------------------------------ - -.. automodule:: txweb.resources.simple_file - :members: - :undoc-members: - :show-inheritance: - txweb.resources.view\_class module ---------------------------------- diff --git a/source/api/txweb.rst b/source/api/txweb.rst index a253619..8302f16 100644 --- a/source/api/txweb.rst +++ b/source/api/txweb.rst @@ -38,10 +38,10 @@ txweb.log module :undoc-members: :show-inheritance: -txweb.web\_views module ------------------------ +txweb.web\_site module +---------------------- -.. automodule:: txweb.web_views +.. automodule:: txweb.web_site :members: :undoc-members: :show-inheritance: diff --git a/source/conf.py b/source/conf.py index ba47010..87b70ab 100644 --- a/source/conf.py +++ b/source/conf.py @@ -40,6 +40,9 @@ "sphinx_rtd_theme" ] +# Autodoc options +autodoc_member_order = 'bysource' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/txweb/__init__.py b/txweb/__init__.py index 9de4e7c..50239b6 100644 --- a/txweb/__init__.py +++ b/txweb/__init__.py @@ -21,4 +21,4 @@ def bar(request): App = Application Texas = Application -__all__ = ['NOT_DONE_YET', "App", "Application"] +__all__ = ['NOT_DONE_YET', "App", "Application", "Texas"] diff --git a/txweb/application.py b/txweb/application.py index 0fd178a..00a49f5 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -8,7 +8,6 @@ 3. Websocket support 4. General utilities and resources ( website, router, reactor, etc) `Application` - """ from __future__ import annotations @@ -69,19 +68,36 @@ class ApplicationWebsocketMixin: WS_EXPOSED_FUNC = "WS_EXPOSED_FUNC" def __init__(self, *args, **kwargs): + """ + + Parameters + ---------- + args + kwargs + """ super().__init__(*args, **kwargs) self.ws_endpoints = {} self.ws_factory = None self.ws_resource = None self.ws_instances = {} - def enable_websockets(self, url, route): + def enable_websockets(self, url: str, route: str): """ - Setup websocket support for txweb + Enables a websocket, undefined behavior to have multiple sockets, resource be added to + the URL router. - :param url: - :param route: - :return: + The first argument should be something like `ws://host:port/` while the second argument just a URL + + Parameters + ---------- + url : str + ws://host:port/ + route : str + The URL endpoint to connect the http side of the server to websocket - /ws + + Returns + ------- + None """ if AUTOBAHN_MISSING is True: raise EnvironmentError("Unable to provide websocket support without autobahn installed/present") @@ -90,19 +106,28 @@ def enable_websockets(self, url, route): self.ws_resource = WebSocketResource(self.ws_factory) self.add_resource(route, self.ws_resource) - def ws_add(self, name, assign_args=False) -> T.Callable[[WSEndpoint], WSEndpoint]: + def ws_add(self, name: str, assign_args: bool = False) -> T.Callable[[WSEndpoint], WSEndpoint]: """ Add a new endpoint for use with the connected websocket. - Example usage - ``` - @ws_add + Example + ------- + + @ws_add("some.endpoint") def my_websocket_endpoint(message): ... - ``` - :param name: - :param assign_args: - :return: + + Parameters + ---------- + name : str + The name the function or method should be accessed via ResilientSocket + + assign_args : bool + + + Returns + ------- + callable: A decorator which adds the added function to the ws_endpoints map """ def processor(func: WSEndpoint) -> WSEndpoint: @@ -118,9 +143,15 @@ def processor(func: WSEndpoint) -> WSEndpoint: # noinspection SpellCheckingInspection def ws_sharelib(self, route_str="/lib"): """ - Adds utility libraries to the provided route str for use by client facing html javascript. - :param route_str: - :return: + Exposes Texas web's built in javascript libraries under the `route_str` argument + + Parameters + ---------- + route_str + + Returns + ------- + """ self.add_staticdir2(route_str, WS_STATIC_LIB) @@ -129,25 +160,34 @@ def ws_class(self, kls=None, name: str = None): Add an entire class of @ws_expose'd class methods as endpoints for the websocket. Example - ``` + ------- @app.ws_class class Foo: @app.expose def bar(message): + # accessible at the endpoint `foo.bar` ... - ``` - That will create a "foo.bar" endpoint - :param kls: - :param name: Override the default behavior of using the class.__name__ property for the base endpoint. - :return: + Parameters + ---------- + kls: object + The class to be registered as a endpoint + name: str + Optional override so in the case of the example above, providing `name=Bob` would create `Bob.bar` endpoint + instead. + + Returns + ------- + Either a processor decorator or the original class + """ def processor(kls, name=None): kls_name = name if name is not None else kls.__name__.lower() if kls_name in self.ws_instances: raise ValueError( - f"Websocket ws_class: a class with {kls_name} is already registered! Use ws_class(name=NewName) to override.") + f"Websocket ws_class: a class with {kls_name}" + f" is already registered! Use ws_class(name=NewName) to override.") self.ws_instances[kls_name] = kls(self) @@ -161,32 +201,36 @@ def processor(kls, name=None): return kls - if kls is None: return functools.partial(processor, name=name) else: return processor(kls, name) - @staticmethod def _eval_annotation(statement, func): """ - TODO - Figure out if there is a way to the type of an annotation without eval - :param statement: - :param func: - :return: + TODO - Figure out if there is a way to get the type of an annotation without eval + + Parameters + ---------- + A string to be evaluated into a callable or type object. + + Returns + ------- + type or callable + """ # There is no way around using eval given how I use annotation's for type casting and conversion # pylint: disable=W0123 return statement if not isinstance(statement, str) else eval(statement, vars(sys.modules[func.__module__])) - @classmethod def websocket_class_arguments_decorator(cls, func): """ Internal method not intended for users :param func: :return: + """ params = inspect.signature(func).parameters arg_keys = {} @@ -204,7 +248,6 @@ def websocket_class_arguments_decorator(cls, func): if "message" not in params: raise TypeError("ws_expose convention expects (self, message, **kwargs)") - @functools.wraps(func) def method_argument_decorator(parent, message): kwargs = {} @@ -230,12 +273,14 @@ def websocket_function_arguments_decorator(cls, func): Internal method not intended for users :param func: :return: + """ params = inspect.signature(func).parameters arg_keys = {} - converter_keys= {} + converter_keys = {} for name, param in params.items(): # type: inspect.Parameter + name: str if param.default is not inspect.Parameter.empty: if param.name in ["message"]: raise TypeError( @@ -245,7 +290,6 @@ def websocket_function_arguments_decorator(cls, func): if param.annotation is not inspect.Parameter.empty: converter_keys[name] = cls._eval_annotation(param.annotation, func) - @functools.wraps(func) def argument_decorator(message: MessageHandler): kwargs = {} @@ -267,13 +311,14 @@ def argument_decorator(message: MessageHandler): return argument_decorator - def ws_expose(self, func: callable = None, assign_args=False): """ See ws_class for use + :param func: :param assign_args: :return: + """ if func is None and assign_args is True: @@ -293,38 +338,43 @@ class ApplicationRoutingHelperMixin: Provides a wrapping interface around :ref: `RoutingResource` """ - router:RoutingResource + router: RoutingResource def add(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: """ :param route_str: A valid URI (starts with a forward slash and no spaces) :param kwargs: + :return: + """ return self.router.add(route_str, **kwargs) # mimic flask's API route = add - def add_class(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: + def add_class(self, route_str: str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: """ + Expose a class and all @app.expose'd method/functions as URL endpoints. - example usage - ``` - @app.add_class("/my_foo") - class Foo: - @app.expose("/bar") - def some_endpoint(self, request): - ... + example + ------- + ``` + @app.add_class("/my_foo") + class Foo: + @app.expose("/bar") + def some_endpoint(self, request): + ... ``` - would connect an instance of Foo and it's method `some_endpoint` to the url `/my_foo/bar` + + Parameters + ---------- + route_str : str + The base URI for all exposed methods of the class. - :param route_str: - :param kwargs: - :return: """ return self.router.add(route_str, **kwargs) @@ -336,6 +386,7 @@ def expose(route_str, **kwargs): :param route_str: :param kwargs: :return: + """ return expose_method(route_str, **kwargs) @@ -344,8 +395,12 @@ def expose(route_str, **kwargs): def set_view_prefilter(func): """ Experimental, sets a view class method to be called before any `expose`'d method. - :param func: - :return: + + Parameters + ---------- + func: callable + A valid callable to be marked as a prefilter. + """ return set_prefilter(func) @@ -353,8 +408,12 @@ def set_view_prefilter(func): def set_view_postfilter(func): """ Experimental, sets a view class method to be called after any `expose`'d method. - :param func: - :return: + + Parameters + ---------- + func: callable + A valid callable to be marked as a psot filter on a class. + """ return set_postfilter(func) @@ -367,6 +426,7 @@ def add_resource(self, route_str:str, resource, **kwargs): :param resource: :param kwargs: :return: + """ return self.router.add_resource(route_str, thing=resource, route_kwargs=kwargs ) @@ -379,6 +439,7 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Fi :param filepath: An absolute or relative path to a file to be served over HTTP :param default_type: What content type should a file be served as :return: twisted.web.static.File + """ assert Path(filePath).exists() @@ -393,6 +454,7 @@ def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path]) -> File: :param dirPath: :param recurse: :return: + """ if route_str.endswith("/") is False: @@ -425,6 +487,7 @@ class ApplicationErrorHandlingMixin: def __init__(self, enable_debug: bool =False): """ :param enable_debug: Flag to decide if to use debugging tools + """ self.enable_debug = enable_debug @@ -438,11 +501,12 @@ def handle_error(self, error_type: T.Union[HTTPCode, int, Exception, str], write Acceptable types currently is a subclass of Exception (eg error.HTTP404) or a numeric HTTP error code (eg 404,500, etc). - :param error_type: The error to catch, either the thrown exception or a numeric HTTP CODE :param write_over: Used to over ride or replace a currently set error handler. Currently the only error - handler set is the catch all 'default" handler which I don't recommend replacing. - :return: + handler set is the catch all 'default" handler which I don't recommend replacing. + + :return: T.Callable + """ def processor(func: ErrorHandler) -> ErrorHandler: @@ -464,6 +528,7 @@ def add_error_handler(self, handler: T.Callable, error_type: T.Union[HTTPCode, i :param error_type: :param override: :return: + """ if error_type in self.error_handlers and override is False: @@ -483,6 +548,7 @@ def processingFailed(self, request:StrRequest, reason: failure.Failure): :param request: :param reason: :return: + """ default_handler = self.error_handlers['default'] @@ -513,7 +579,8 @@ class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin, """ Grand unified god module of txweb. - TODO rename to TXWeb versus application to avoid confusion. + TODO rename to TXWeb or Texas versus application to avoid confusion with `txweb` on pypi + """ NOT_DONE_YET = NOT_DONE_YET @@ -525,17 +592,16 @@ def __init__(self, enable_debug:bool=False ): """ - Similar to Klein and its influence Flask, the goal is to consolidate + Similar to Klein and its influence Flask, the goal is to consolidate technical debt into one God module antipattern class. Purposes: Provides a public API to Site, HTTPErrors, RoutingResource, and additional helpers. - Arguments: - namespace: the base module/package name of the application, currently intended to assist logging and debugging - twisted_reactor: currently unused - request_factory: currently unused - enable_debug: Not implemented yet, switches on extra debugging tools + :param namespace: + :param twisted_reactor: + :param request_factory: + :param enable_debug: """ @@ -572,6 +638,7 @@ def __post_init__(self) -> None: overloading __init__. :return: + """ @@ -584,6 +651,7 @@ def request_factory_partial(app: Application, request_kls: StrRequest): def partial(*args, **kwargs): # pylint: disable=W0212 + # noinspection PyCallingNonCallable request = request_kls(*args, **kwargs) request.add_before_render(app._call_before_render) request.add_after_render(app._call_after_render) @@ -597,7 +665,8 @@ def partial(*args, **kwargs): def router(self) -> RoutingResource: """ Provide access to the routing resource object. - :return: + + """ return self._router @@ -605,7 +674,8 @@ def router(self) -> RoutingResource: def site(self) -> WebSite: """ Provides access to the server.Site instance - :return: + + """ return self._site @@ -613,7 +683,8 @@ def site(self) -> WebSite: def reactor(self) -> PosixReactorBase: """ Provides access to the currently used reactor, used specifically to make testing easier. - :return: + + """ return self._reactor @@ -625,6 +696,7 @@ def listenTCP(self, port:int, interface:str= "127.0.0.1") -> Port: """ Convenience helper which adds the HTTP protocol factory to the reactor and set it to listen to the provided interface and port + """ self._listening_port = self.reactor.listenTCP(port, self._site, interface=interface) return self._listening_port @@ -634,6 +706,7 @@ def before_render(self, func: T.Callable[[StrRequest], None]): Intended as a convenience decorator to set a global before render handler Arguments: func: a callable that expects to receive the current Request + """ self._before_render_handlers.append(func) return func @@ -643,6 +716,7 @@ def after_render(self, func: T.Callable[[StrRequest], None]): Intended as a convenience decorator to set a global after render handler Arguments: func: a callable that expects to receive the current Request + """ self._after_render_handlers.append(func) return func @@ -655,6 +729,7 @@ def _call_before_render(self, request: StrRequest): :param request: :return: + """ # pylint: disable=W0703 for func in self._before_render_handlers: @@ -670,6 +745,7 @@ def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]): :param request: :param body: :return: + """ # pylint: disable=W0703 for func in self._after_render_handlers: @@ -679,6 +755,3 @@ def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]): log.error(f"After render failed {func}") return body - - - diff --git a/txweb/http_codes.py b/txweb/http_codes.py index 05fdb85..27ec31b 100644 --- a/txweb/http_codes.py +++ b/txweb/http_codes.py @@ -133,5 +133,5 @@ def __init__(self, message="Internal Server Error"): super().__init__(self.CODE, message) -class Unrenderable(HTTP5xx): +class Unrenderable(EnvironmentError): pass diff --git a/txweb/lib/errors/__init__.py b/txweb/lib/errors/__init__.py index 3be09d4..a75f5fa 100644 --- a/txweb/lib/errors/__init__.py +++ b/txweb/lib/errors/__init__.py @@ -1,5 +1,5 @@ """ - Error handlers for the txweb + Error handlers for txweb """ from twisted.python.failure import Failure from .default import DefaultHandler diff --git a/txweb/lib/errors/base.py b/txweb/lib/errors/base.py index 91f0c07..bdc8a2f 100644 --- a/txweb/lib/errors/base.py +++ b/txweb/lib/errors/base.py @@ -24,9 +24,19 @@ def __init__(self, application): def __call__(self, request: StrRequest, error: Failure) -> T.Union[None, bool]: """ - :param request: Provided to allow managing the connection (writing, http code, etc). - :param reason: - :return: Return False if the handler was unable to handle the error + Parameters + ---------- + request: StrRequest + The request and current response to a failed transaction. + + error: Failure + + + Returns + ------- + False signals the errorhandler failed to properly handle the error. + None or True signals the errorhandler was successfull + """ # noinspection PyBroadException try: @@ -40,8 +50,13 @@ def process(self, request: StrRequest, error: Failure) -> T.Union[None, bool]: """ Just a stub - :param request: - :param error: - :return: + Parameters + ---------- + request + error + + Returns + ------- + """ raise NotImplementedError("Attempting to use Base error handler") diff --git a/txweb/lib/errors/default.py b/txweb/lib/errors/default.py index 4b47fcc..d10acc8 100644 --- a/txweb/lib/errors/default.py +++ b/txweb/lib/errors/default.py @@ -43,9 +43,14 @@ def process(self, request: StrRequest, reason: Failure) -> T.Union[None, bool]: else it sends a 500 HTTP response and then raises the exception back into the user application. - :param request: - :param reason: - :return: + Parameters + ---------- + request: StrRequest + reason: Failure + + Returns + ------- + False on failure to handle error """ if request.startedWriting not in [0, False]: diff --git a/txweb/lib/message_handler.py b/txweb/lib/message_handler.py index 2f43780..263048e 100644 --- a/txweb/lib/message_handler.py +++ b/txweb/lib/message_handler.py @@ -9,7 +9,7 @@ import typing as T from collections.abc import Mapping -# from twisted.web.server import NOT_DONE_YET +from twisted.internet.defer import Deferred if T.TYPE_CHECKING or False: # pragma: no cover @@ -50,38 +50,67 @@ def values(self): # pragma: no cover return self.raw_message.values() # pylint: disable=redefined-builtin - def get(self, key, default=None, type=None): + def get(self, key: str, default=None, vtype: type=None): + """ + + Parameters + ---------- + key + default + vtype + + Returns + ------- + + """ + + try: value = self[key] except KeyError: return default - if type: + if vtype: try: - value = type(value) + value = vtype(value) except ValueError: return default return value @property - def identity(self): + def identity(self) -> str: """ Utility to make it quicker to access the connection's unique identifier - :return: + + Returns + ------- + A unique identifier string + """ return self.connection.identity # pylint: disable=redefined-builtin - def args(self, key, default=None, type=None): + def args(self, key: str, default=None, vtype=None): """ A more explicit/direct getter that looks for an `args` dictionary in the client message and if it exists, returns the requested key. - :param key: - :param default: - :param type: What type to cast the arg value as. (eg type=int would cast a str to int if possible) - :return: + + Parameters + ---------- + key: str + A dict key for the message's `args` subdictionary of arguments + + default: str + TODO implement a not empty default sentinel. + + vtype: type + Used to cast the requested key's value into. (eg vtype=int would cast key=foo to int or try to atleast) + + Returns + ------- + T.Any """ try: @@ -96,44 +125,73 @@ def args(self, key, default=None, type=None): except ValueError: return default - if type: + if vtype: try: - value = type(value) + value = vtype(value) except ValueError: return default return value - def respond(self, result): + def respond(self, result) -> None: """ - If the message was an request/ask for response, send back a result. - :param result: - :return: + For client messages that are `ask` type, send back a response to their request. + + Parameters + ---------- + result: T.Any + Anything that can be serialized by json.dumps is a valid variable type. + + Returns + ------- + None + """ return self.connection.respond(self.raw_message, result=result) - def tell(self, endpoint, **kwargs): + def tell(self, endpoint: str, **kwargs) -> None: """ - Tell the client to do something if it provides the requested end point. - :param endpoint: - :param kwargs: - :return: + Tell the client to do something at the specified `endpoint` + + Parameters + ---------- + endpoint: str + Ideally a valid client side endpoint + kwargs: dict + json serializable friendly data + + Returns + ------- + None """ return self.connection.tell(endpoint, **kwargs) - def ask(self, endpoint, **kwargs): + def ask(self, endpoint: str, **kwargs: T.Dict[str, T.Any]) -> Deferred: """ Ask the client for information or acknowledgement of success/failure for an action. - :param endpoint: - :param kwargs: - :return: + + Parameters + ---------- + endpoint + kwargs + + Returns + ------- + A Deferred object the server side code can add a callback to. """ return self.connection.ask(endpoint, type="ask", args=kwargs) - def get_session(self, get_key=None): + def get_session(self, get_key: str=None) -> T.Union[T.Dict, T.Any]: """ - see WSProtocol's get_session - :param get_key: - :return: + See WSProtocol's get_session + + Parameters + ---------- + get_key: str + A shortcut to fetch a specific key of the session dictionary versus grabbing the whole dictionary. + + Returns + ------- + """ return self.connection.application.get_session(self.connection, get_key=get_key) diff --git a/txweb/lib/routed_factory.py b/txweb/lib/routed_factory.py index b61236b..fd6fec7 100644 --- a/txweb/lib/routed_factory.py +++ b/txweb/lib/routed_factory.py @@ -3,12 +3,19 @@ for server side websocket endpoints. """ +import typing as T + from autobahn.twisted.websocket import WebSocketServerFactory +from .message_handler import MessageHandler from .wsprotocol import WSProtocol +EndpointFunc = T.Callable[[MessageHandler], None] + class RoutedWSFactory(WebSocketServerFactory): # pragma: no cover + routes: T.Dict[str, T.Callable] + def __init__(self, url, routes, protocol_cls=WSProtocol, application=None): WebSocketServerFactory.__init__(self, url) self.protocol = protocol_cls @@ -16,17 +23,20 @@ def __init__(self, url, routes, protocol_cls=WSProtocol, application=None): self.routes = routes self._application = application - def get_endpoint(self, name): + def get_endpoint(self, name: str) -> EndpointFunc: """ + Just a simple getter to return a given endpoint. + + Returns + ------- + Returns None if the endpoint isn't in the routes dictionary. - :param name: - :return: """ return self.routes.get(name, None) def get_application(self): """ + Possible bad code smell but used primarily for debugging. - :return: """ return self._application diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index 94a0e8e..7cdb09b 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -37,6 +37,16 @@ log = getLogger(__name__) class StrRequest(Request): + """ + Request is actually a merger of three different topics. + + 1. StrRequest contains all of the request data: headers & request body. + 2. StrRequest holds the connection API. + 3. StrRequest holds the response headers, http code, and response body until finalization. + + + + """ NOT_DONE_YET: T.Union[int, bool] = NOT_DONE_YET @@ -51,13 +61,18 @@ def __init__(self, *args, **kwargs): self._call_before_render = None self._call_after_render = None - def getCookie(self, cookie_name: T.Union[str, bytes]): + def getCookie(self, cookie_name: T.Union[str, bytes]) -> T.Union[str, bytes]: """ Wrapper around Request's getCookie to convert to and from byte strings to unicode/str's - :param cookie_name: - :return: + Parameters + ---------- + cookie_name: str + + Returns + ------- + If cookie_name argument is bytes, returns a byte string else returns str/unicode string """ expect_bytes = isinstance(cookie_name, bytes) @@ -74,8 +89,7 @@ def getCookie(self, cookie_name: T.Union[str, bytes]): def add_before_render(self, func): """ Utility intended solely to make testing easier - :param func: - :return: + """ self._call_before_render = func return func @@ -83,8 +97,7 @@ def add_before_render(self, func): def add_after_render(self, func): """ Utility intended solely to make testing easier - :param func: - :return: + """ self._call_after_render = func return func @@ -107,11 +120,15 @@ def writeTotal(self, response_body: T.Union[bytes, str], code: T.Union[int, str, Utility to write and then close the connection in one go. Especially useful for error handling events. + Parameters + ---------- + response_body: + Content intended to be sent to the client browser + code: + Optional HTTP Code to use + message: + Optional HTTP response message to use - :param response_body: Content intended for after headers - :param code: Optional HTTP Code to use - :param message: Optional HTTP response message to use - :return: """ content_length = intToBytes(len(response_body)) @@ -136,10 +153,14 @@ def writeJSON(self, data: T.Dict): def setHeader(self, name: T.Union[str, bytes], value: T.Union[str, bytes]): """ A quick wrapper to convert unicode inputs to utf-8 bytes + Set's a header for the RESPONSE - Arguments: - name: A valid HTTP header - value: Syntactically correct value for the header name + Parameters + ---------- + name: + A valid HTTP header + value + Syntactically correct value for the provided header name """ if isinstance(name, str): name = name.encode("utf-8") @@ -151,22 +172,28 @@ def setHeader(self, name: T.Union[str, bytes], value: T.Union[str, bytes]): def setResponseCode(self, code: int = 500, - message: T.Optional[T.Union[str, bytes]] = b"Failure processing request"): + message: T.Optional[T.Union[str, bytes]] = b"Failure processing request") -> T.NoReturn: """ - Str wrapper - :param code: - :param message: - :return: + Str to unicode wrapper around twisted.web's Request class. + + Parameters + ---------- + code + message + + Returns + ------- + """ if message and not isinstance(message, bytes): message = message.encode("utf-8") - return Request.setResponseCode(self, code, message) + Request.setResponseCode(self, code, message) - def ensureFinished(self): + def ensureFinished(self) -> None: """ Ensure's the connection has been flushed and closed without throwing an error. - :return: + """ if self.finished not in [1, True]: self.finish() @@ -176,7 +203,6 @@ def requestReceived(self, command, path, version): Looks for POST'd arguments in form format (eg multipart). Allows for file uploads and adds them to .args - TODO add a files attribute to StrRequest? """ self.content.seek(0, 0) @@ -224,8 +250,11 @@ def query_iter(arguments): @property def methodIsPost(self) -> bool: """ - Utility method - :return: + Utility method + + Returns + ------- + bool - Is the current request a POST request """ return self.method == b"POST" @@ -233,26 +262,34 @@ def methodIsPost(self) -> bool: def methodIsGet(self) -> bool: """ Utility method - :return: + + Returns + ------- + True if the current request is a HTTP GET request. """ return self.method == b"GET" def render(self, resrc: resource.Resource) -> None: """ - Ask a resource to render itself. - - If the resource does not support the requested method, - generate a C{NOT IMPLEMENTED} or C{NOT ALLOWED} response. + Ask a resource to render itself unless a prefilter returns a string/bytes + body which will be rendered instead. - @param resrc: The resource to render. - @type resrc: L{twisted.web.resource.IResource} + Parameters + ---------- + resrc: Resource + The resource to be rendered. - @see: L{IResource.render()} + Returns + ------- + None, output is written directly to the underlying HTTP channel. """ + body = None if self._call_before_render is not None: body = self._call_before_render(self) - # TODO halt rendering resource if call before render provides a response - body = resrc.render(self) + + if body is None: + body = resrc.render(self) + if self._call_after_render is not None: self._call_after_render(self, body) @@ -291,7 +328,7 @@ def _processFormData(self, content_type, content_length): Thank you Cristina - http://www.cristinagreen.com/uploading-files-using-twisted-web.html - TODO this can be problematic if a binary file is being uploaded + TODO this can be problematic if a large binary file is being uploaded TODO verify Twisted HTTP channel/transport blows up if file upload size is "too big" """ options = {} @@ -324,17 +361,32 @@ def processingFailed(self, reason): self.site.processingFailed(self, reason) @property - def json(self) -> bool: + def json(self) -> T.Any: """ Is this a JSON posted request? - :return: + + Returns + ------- + Ideally returns a dict object as I cannot think of what else a sane client would send in JSON format. + """ if self.getHeader("Content-Type") in ["application/json", "text/json"]: return json.loads(self.content.read()) else: return None - def redirect(self, url: T.Union[str, bytes], code=FOUND): + def get_json(self) -> T.Any: + """ + Intended to mimic Flask api + + Returns + ------- + dict - a json decoded object + """ + return self.json + + + def redirect(self, url: T.Union[str, bytes], code=FOUND) -> T.NoReturn: """ Utility function that does a redirect. @@ -343,9 +395,14 @@ def redirect(self, url: T.Union[str, bytes], code=FOUND): The request should have C{finish()} called after this. - @param url: I{Location} header value. - @param code: {int} http response code to use - @type url: L{bytes} or L{str} + Parameters + ---------- + url: bytes + What to set the LOCATION http response header to + code: int + What to set the HTTP response code to (eg 3xx) + """ self.setResponseCode(code) self.setHeader(b"location", url) + self.ensureFinished() diff --git a/txweb/lib/view_class_assembler.py b/txweb/lib/view_class_assembler.py index cc4b6bc..f49d3f0 100644 --- a/txweb/lib/view_class_assembler.py +++ b/txweb/lib/view_class_assembler.py @@ -37,11 +37,17 @@ def handle_show(self, request): POSTFILTER_ID = "__POSTFILTER_ID__" -def has_exposed(obj): +def has_exposed(obj) -> bool: """ Does the provided object have an exposed endpoint? - :param obj: - :return: + + Parameters + ---------- + obj: An instance of a View class with exposed members OR a render method + + Returns + ------- + True if any method/function has been decorated with @texas.expose """ return any([ True @@ -59,11 +65,18 @@ def has_exposed(obj): # return has_exposed(attribute) -def is_viewable(attribute): +def is_viewable(attribute) -> bool: """ - Check if whatever this is, it can behave as a web endpoint when called. - :param attribute: - :return: + Check if whatever this is, it is callable and has been marked as expose'd + + Parameters + ---------- + A callable that has been expose'd + + Returns + ------- + True if conditions have been met. + """ is_valid_callable = inspect.ismethod(attribute) \ or inspect.isfunction(attribute) \ @@ -78,8 +91,14 @@ def is_renderable(kls): """ Does a class definition have a valid render method/function Generally checked if it has no exposed methods. - :param kls: - :return: + + Parameters + ---------- + A potential view class + + Returns + ------- + True if it has a renderable method (render or render_{HTTP REQUEST METHOD[POST, GET, HEAD, PUT, etc]) """ return \ any([ @@ -95,9 +114,15 @@ def is_renderable(kls): def expose(route, **route_kwargs): """ Decorator to set the exposed method's routing url and tag it with the exposed sentinel attribute - :param route: - :param route_kwargs: - :return: + + Parameters + ---------- + route: str + route_kwargs: arguments intended to be passed on to werkzeug routing logic + + Returns + ------- + T.Callable[[func], func] - a decorating function to set the exposed attribute and append routing arguments """ def processor(func): setattr(func, EXPOSED_STR, True) @@ -110,8 +135,14 @@ def processor(func): def set_prefilter(func): """ decorator used to mark a class method as a prefilter for the class. - :param func: - :return: + + Parameters + ---------- + func: a valid prefilter callable + + Returns + ------- + The same callable as was passed in as an argument """ setattr(func, PREFILTER_ID, True) return func @@ -120,8 +151,15 @@ def set_prefilter(func): def set_postfilter(func): """ decorator used to mark a class method as a post filter for a class. - :param func: - :return: + + Parameters + ---------- + func - a valid postfilter callable + + Returns + ------- + callable - the same function that was passed in as an argument + """ setattr(func, POSTFILTER_ID, True) return func @@ -130,12 +168,17 @@ def set_postfilter(func): ViewAssemblerResult = namedtuple("ViewAssemblerResult", "instance,rule,endpoints") -def find_member(thing, identifier) -> T.Union[T.Callable, bool]: +def find_member(thing, identifier: str) -> T.Union[T.Callable, bool]: """ Utility to search every member of an object for the provided `identifier` attribute - :param thing: - :param identifier: - :return: + + Parameters + ---------- + thing: an instance of a view class + + Returns + ------- + The first matching instance method or attribute to have the `identifier` attribute """ for _, member in inspect.getmembers(thing, lambda v: hasattr(v, identifier)): return member @@ -143,7 +186,9 @@ def find_member(thing, identifier) -> T.Union[T.Callable, bool]: return False -def view_assembler(prefix, kls, route_args): +def view_assembler(prefix: str, + kls, + route_args: T.Dict[str, T.Union[str, T.List[str]]]) -> T.Union[ViewAssemblerResult, None]: """ Given a class definition, this instantiates the class, searches it for exposed methods and pre/post filters @@ -154,11 +199,23 @@ def view_assembler(prefix, kls, route_args): else it checks if the class def has a render/render_METHOD method. else it throws UnrenderableException - - :param prefix: - :param kls: - :param route_args: - :return: + Parameters + ---------- + prefix: str + The view classes' URL prefix + kls: claasdef + A view class definition + route_args: dict + a dictionary of arguments intended for the werkzeug URL routing library + + Raises + ------ + EnvironmentError + Throws this error if the class defintion is not a valid View class + + Returns + ------- + A ViewAssemblerResult if `kls` is a view class or has a valid render method function. """ # pylint: disable=R0914 endpoints = {} diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py index 976bb6a..1dc3b81 100644 --- a/txweb/lib/wsprotocol.py +++ b/txweb/lib/wsprotocol.py @@ -30,12 +30,34 @@ class WSProtocol(WebSocketServerProtocol): - - my_log = getLogger() """ - To prevent an OOM/out of memory event, limit the number of - pending asks to 100 + Handles new websocket connections by routing them to the requested endpoint. + + All incoming requests from the client match one of two JSON formats + + 1. The message has an `endpoint` key and `type` set to either "ask" or "tell" + If type is set to ask, a caller_id key MUST be present so the response can be properly routed back. + + 2. The message has a `type` key set to "response" + + + Attributes + ---------- + my_log: Logger + MAX_ASKS: int + The maximum number of pending asks to wait for from the client + factory: + Reference to the factory which spawned this protocol/connection + identity: str + A unique identifier for this connection which MUST be different than what is used for cookies + on_disconnect: Deferred + A deferred to allow for parts of the server application to be notified when the connection closes + deferred_asks: T.Dict[str, Deferred] + All pending deferred asks to the client. + + """ + my_log = getLogger() MAX_ASKS = 100 # Need to make this tunable factory: RoutedFactory @@ -43,7 +65,6 @@ def __init__(self): self.pending_responses = {} super().__init__() - # WebSocketServerProtocol.__init__(self, *args, **kwargs) self.identity = None self.on_disconnect = Deferred() @@ -54,17 +75,26 @@ def __init__(self): def application(self): """ Utility intended mostly for unit-testing - :return: + + Returns + ------- + The current instance of Texas + """ return self.factory.get_application() - def getCookie(self, cookie_name, default=None): + def getCookie(self, cookie_name: str, default=None) -> str: """ Mirror's the behavior of StrRequest.getCookie - :param cookie_name: - :param default: - :return: + Parameters + ---------- + cookie_name: str + default: str + + Returns + ------- + If the cookie exists, it returnx the cookie value ELSE returns `default` which is set to None by default """ raw_cookies = self.http_headers.get('cookie', "") for params in raw_cookies.split(";"): @@ -74,67 +104,92 @@ def getCookie(self, cookie_name, default=None): return default - def onConnect(self, request): + def onConnect(self, request) -> T.NoReturn: """ - Set's a unique identifier for the connection. + Hook to the underlying websocket protocol so a unique identifier for the connection can be set. + Called externally by the protocol factory class. - :param request: - :return: """ self.identity = uuid4().hex self.my_log.debug("Client connecting: {request.peer}", request=request) - def onClose(self, was_clean, code, reason): + def onClose(self, was_clean: bool, code: int, reason) -> T.NoReturn: """ Connection was lost, currently I don't care why but I likely should. - :param was_clean: - :param code: - :param reason: - :return: + + Parameters + ---------- + + was_clean: bool + code: int + reason: unknown + """ self.on_disconnect.addErrback(self.my_log.error) self.on_disconnect.callback(self.identity) + # Delete the Deferred to force it to be garbage collected and emit any unhandled errors as soon as possible del self.on_disconnect self.my_log.debug("WebSocket connection closed: {reason!r}", reason=reason) - def sendDict(self, **values): + def sendDict(self, **values) -> T.NoReturn: """ Utility used by every other method that follows - :param values: - :return: + + Raises + ------ + TypeError + Throws this if a key/value in values cannot be encoded to JSON (eg. Enum) + + Parameters: + ----------- + values: dict + A valid dictionary view of arguments that can be serialized by JSON + """ response = json.dumps(values) # Always send synchronously for now self.sendMessage(response.encode("utf-8"), isBinary=False, sync=True) - def respond(self, original_message, result): + def respond(self, original_message, result) -> T.NoReturn: """ The client asked for a result/response. - :param original_message: - :param result: - :return: + Parameters + ---------- + original_message: MessageHandler or dict + The original request message so caller_id can be retrieved + + + """ self.sendDict(caller_id=original_message['caller_id'], type="response", result=result) - def tell(self, endpoint, **values): + def tell(self, endpoint: str, **values) -> T.NoReturn: """ Tell the client to do something and don't expect a response - :param endpoint: - :param values: - :return: + Parameters + ---------- + endpoint: str + The remote client side endpoint to be told to do something. + """ self.sendDict(endpoint=endpoint, type="tell", args=values) - def ask(self, endpoint, **values): + def ask(self, endpoint, **values) -> Deferred: """ Ask the client to do something and I should get a response back. - :param endpoint: - :param values: - :return: + Creates a Deferred object so the server side application can be notified of a response + + Parameters + ---------- + endpoint: str + values: dict + dictionary of keyname arguments to be serialized and sent to the client. + + """ if len(self.deferred_asks) < self.MAX_ASKS: request_token = uuid4().hex @@ -145,13 +200,16 @@ def ask(self, endpoint, **values): else: raise EnvironmentError("Maximum # of pending asks reached") - def handleResponse(self, message): + def handleResponse(self, message: MessageHandler) -> T.NoReturn: """ Server side asked client a question, shunt this message through the deferred_asks collection of deferreds to the appropriate/waiting endpoint - :param message: - :return: + Parameters + ---------- + message: MessageHandler + The client request with the response to a server ask + """ caller_id = message.get("caller_id", None) @@ -162,12 +220,12 @@ def handleResponse(self, message): else: warnings.warn(f"Response to ask {caller_id} arrived but was not found in deferred_asks") - def handleEndPoint(self, message): + def handleEndPoint(self, message: MessageHandler) -> T.NoReturn: """ Handles incoming ask and tell messages. :param message: - :return: + """ endpoint_func = self.factory.get_endpoint(message['endpoint']) self.my_log.debug("Processing {endpoint!r}", endpoint=message['endpoint']) @@ -188,13 +246,13 @@ def handleEndPoint(self, message): - def onMessage(self, payload, is_binary): + def onMessage(self, payload, is_binary) -> T.NoReturn: """ Entry point for incoming messages from the client :param payload: :param is_binary: - :return: + """ if is_binary: # pragma: no cover diff --git a/txweb/resources/routing.py b/txweb/resources/routing.py index d5f7c73..147052c 100644 --- a/txweb/resources/routing.py +++ b/txweb/resources/routing.py @@ -49,9 +49,14 @@ class RoutingResource(resource.Resource): def __init__(self, script_name: bytes = None): """ + The bridge between twisted's object graph routing system and werkzeug's url pattern + recognition routing system. - :param script_name: Optional ability to add a prefix to all routed URL's in case this application - is nested inside another web app. See CGI's SCRIPT_NAME variable. + Parameters + ---------- + script_name: bytes + Optional ability to add a prefix to all routed URL's in case this application + is nested inside another web app. See CGI's SCRIPT_NAME variable. """ resource.Resource.__init__(self) @@ -67,7 +72,11 @@ def __init__(self, script_name: bytes = None): def site(self): # pragma: no cover """ Return a reference to the parent site of this resource - :return: + + Returns + ------- + web_site instance + """ return self._site @@ -83,7 +92,7 @@ def site(self, site): # pragma: no cover def iter_rules(self) -> T.Generator: """ Debug method for iterating over all of the currently set URL Rule's for the werkzeug routing map. - :return: + """ return self._route_map.iter_rules() @@ -91,9 +100,8 @@ def add(self, route_str: str, **kwargs: T.Dict[str, T.Any]): """ Possibly super overloaded - :param route_str: - :param kwargs: - :return: + + """ if "endpoint" in kwargs: diff --git a/txweb/tests/test_messagehandler.py b/txweb/tests/test_messagehandler.py index 2d9bf04..3b89a3a 100644 --- a/txweb/tests/test_messagehandler.py +++ b/txweb/tests/test_messagehandler.py @@ -4,11 +4,11 @@ def test_type_casting(): message = MessageHandler({"foo":"1"}, None) - actual = message.get("foo", type=int) + actual = message.get("foo", vtype=int) assert actual == 1 def test_type_casting_args(): message = MessageHandler({"args": {"foo":"1"}}, None) - actual = message.args("foo", type=int) + actual = message.args("foo", vtype=int) assert actual == 1 \ No newline at end of file diff --git a/txweb/util/reloader.py b/txweb/util/reloader.py index 3b224c5..fac4c9e 100644 --- a/txweb/util/reloader.py +++ b/txweb/util/reloader.py @@ -4,14 +4,16 @@ In user script it must follow the pattern - def main(): - my main func that starts twisted + ```python + def main(): + # my main func that starts twisted - if __name__ == "__main__": - from txweb.sugar.reloader import reloader - reloader(main) - else: - TAC logic goes here but isn't necessary for short term dev work + if __name__ == "__main__": + from txweb.sugar.reloader import reloader + reloader(main) + else: + TAC logic goes here but isn't necessary for short term dev work + ``` Originally found via https://blog.elsdoerfer.name/2010/03/09/twisted-twistd-autoreload/ @@ -52,7 +54,7 @@ def main(): import dummy_thread as thread except ImportError as missing_import: print("Alright... so I tried importing thread, that failed, so I tried _thread, that failed too") - print("..so then I tried dummy_thread, then _dummy_thread. All failed") + print("..so then I tried dummy_thread. All failed") print(", at this point I am out of ideas here") raise RuntimeError("Failed to import threading library") from missing_import @@ -69,6 +71,7 @@ def main(): """ "Reason" is here https://code.djangoproject.com/ticket/2330 TODO - Figure out why threading needs to be imported as this feels like a problem within stdlib. + """ try: @@ -95,6 +98,7 @@ def build_list( :param watch_self: bool Watch all of txweb for changes :param ignore_prefix: simple check that if provided compares file names to the prefix and skips if they match :return: None + """ global WATCH_LIST @@ -128,8 +132,8 @@ def is_prefixed(filepath: pathlib.Path): stat = pathobj.stat() if is_prefixed(pathobj): continue - else: - WATCH_LIST[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,) + + WATCH_LIST[pathobj] = (stat.st_size, stat.st_ctime, stat.st_mtime,) else: pass @@ -137,7 +141,9 @@ def is_prefixed(filepath: pathlib.Path): def file_changed() -> bool: """ Scans the watched list of files for change in size, created & modified timestamps - :return: + + :return: Returns True if a change has been detected else False + """ global WATCH_LIST change_detected = False @@ -168,6 +174,7 @@ def watch_thread(os_exit: bool = SENTINEL_OS_EXIT, watch_self: bool = False, ign :param watch_self: Should reloader watch its own source code :param ignore_prefix: Ignore files starting with this prefix :return: + """ # pylint: disable=W0212 @@ -252,9 +259,10 @@ def reloader(main_func, *args, watch_self=False, ignore_prefix=None, **kwargs): :param main_func: The function to run in the main/primary thread :param args: list of arguments :param watch_self: Should reloader also watch it's own src code for changes? - :param ignore_prefix: Files to ignore based on their prefix (eg test_ files) + :param ignore_prefix: Files to ignore based on their prefix (eg test files) :param kwargs: dictionary of arguments :return: None + """ if args is None: args = () diff --git a/txweb/util/templating.py b/txweb/util/templating.py index afebe3d..ea28a43 100644 --- a/txweb/util/templating.py +++ b/txweb/util/templating.py @@ -46,13 +46,14 @@ def initialize_jinja2( template_dir: T.Union[Path, str, T.List[T.Union[Path, str]]], - cache_dir: T.Optional[str, Path] = None): # pragma: no cover + cache_dir: T.Optional[T.Union[str, Path]] = None): # pragma: no cover """ :param template_dir: Can be either a string, Path object, or a list of string/path objects pointing to template directories :param cache_dir: optional absolute path to a cache dir intended for storing compiled templates :return: + """ global JINJA2_ENV diff --git a/txweb/web_site.py b/txweb/web_site.py index 0288a6e..7cee6cf 100644 --- a/txweb/web_site.py +++ b/txweb/web_site.py @@ -7,7 +7,7 @@ #stdlib import typing as T -import pathlib +# import pathlib # import copy # twisted imports From 37bebccc3e397a892a6bdb0a4aae54c994cfa29a Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:00:54 -0700 Subject: [PATCH 177/185] Additional tutorials added --- .gitignore | 2 +- source/index.rst | 7 ++-- source/manual/http_view_classes.md | 58 ++++++++++++++++++++++++++++++ source/manual/static_routing.md | 44 +++++++++++++++++++++++ 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 source/manual/http_view_classes.md create mode 100644 source/manual/static_routing.md diff --git a/.gitignore b/.gitignore index 1c95fcc..7c097ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ *.pyc .komodotools *.komodoproject -build/ +docs/ dist/ MANIFEST htmlcov diff --git a/source/index.rst b/source/index.rst index 639fc14..1c0deed 100644 --- a/source/index.rst +++ b/source/index.rst @@ -7,18 +7,17 @@ Welcome to Texas web's documentation! ===================================== .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: manual/quick_start manual/error_handling + manual/http_view_classes + manual/static_routing api/txweb - - - Indices and tables ================== diff --git a/source/manual/http_view_classes.md b/source/manual/http_view_classes.md new file mode 100644 index 0000000..cc577dd --- /dev/null +++ b/source/manual/http_view_classes.md @@ -0,0 +1,58 @@ +HTTP View classes +============ + +View classes are a code style for organizing common http endpoints into one class. Some caveats apply. + + + +```python +import my_app as app +from txweb.lib.str_request import StrRequest + +@app.add_class("/widget") +class Widget: + + def __init__(self): + self.counter = 0 + + def add_widget(self): + self.counter += 1 + + def delete_widget(self): + self.counter = max(0, self.counter - 1) + + @app.expose("/add") + def add(self, request: StrRequest): + self.add_widget() + return f"There are {self.counter} widgets." + + @app.expose("/delete") + def delete(self, request: StrRequest): + self.delete_widget() + return f"One widget was deleted, there are now {self.counter}" + +``` + +URLS added to the routing map +----------------------------- + +Above would add two new URLS to the map. + +1. `/widget/add` +2. `/widget/delete` + +Both endpoints default to only accept GET and HEAD requests. + +Warning +------- + +Though the above example uses `self.counter` as a class persistent variable, care should be taken +on a couple counts: + +1. class variables are not persistent between restarts/reloads of the application. + +2. You can create memory leaks if you are not careful to manage variables. + +3. For more complex applications where a request yields to the reactor while waiting on an + expensive/blocking resource, the view class variable can be changed by another request that runs + while the first request is yielded. \ No newline at end of file diff --git a/source/manual/static_routing.md b/source/manual/static_routing.md new file mode 100644 index 0000000..176105b --- /dev/null +++ b/source/manual/static_routing.md @@ -0,0 +1,44 @@ +Routing static assets +===================== + +Texas can expose both individual static files and entire directory assets. + + +Static files and directories +---------------------------- + +## Exposing static files + +```python +from pathlib import Path +from txweb import Texas + +app = Texas(__name__) + +app.route_file("/my_file", Path(__file__).parent / "path" / "too" / "my_file.txt") + +``` + +The snippet above would make `./path/too/my_file.txt` accessible to a client browser at +the URI "/my_file" via GET. Internally this uses `twisted.web.static.File` which does its +best to determine the correct MIME type of the file being served, BUT it defaults to `text/html` +if it cannot. + +In that scenario you can set a default content type via + +`app.add_file("/my_file", "foo/bar.txt", defaultType="application/jsonp")` + +## Exposing static directories + +```python +from pathlib import Path +from txweb import Texas + +app = Texas(__name__) + +app.route_directory("/my_file", Path(__file__).parent / "path" / "too" / "my_static_dir") + +``` + +Similar to `app.add_file` this uses `twisted.web.static.File` but if provided a directory path +it will recursively expose all subdirectories as well which should be noted. From 84cc39e3994845703709cbcf0612bfcca81f5887 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:01:13 -0700 Subject: [PATCH 178/185] Create .pylintrc --- .pylintrc | 592 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 592 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..9cde995 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,592 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,helper.py,conftest.py,html.py + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=test_* + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + line-too-long, + C0103, + too-many-instance-attributes, + too-many-ancestors + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception From 4f9cce7155683d41e51032c32592212e13149e00 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:01:29 -0700 Subject: [PATCH 179/185] Change documentation path --- make.bat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make.bat b/make.bat index 4bde88c..956d52f 100644 --- a/make.bat +++ b/make.bat @@ -8,7 +8,7 @@ if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=source -set BUILDDIR=build +set BUILDDIR=docs if "%1" == "" goto help From 61779e3c3dd66018574a4912756bd70220fb64b3 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:02:39 -0700 Subject: [PATCH 180/185] pep8, documentation, and verb changes (route_ vs add_) --- txweb/application.py | 78 +++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/txweb/application.py b/txweb/application.py index 00a49f5..7160f04 100644 --- a/txweb/application.py +++ b/txweb/application.py @@ -37,7 +37,6 @@ from .lib.message_handler import MessageHandler from .lib.routed_factory import RoutedWSFactory - # Application from .log import getLogger from .resources import RoutingResource @@ -47,13 +46,11 @@ from .http_codes import HTTPCode from .lib.errors.default import DefaultHandler, BaseHandler - HERE = Path(__file__).parent WS_STATIC_LIB = HERE / "websocket_static_libraries" log = getLogger(__name__) - ArbitraryListArg = T.NewType("ArbitraryListArg", T.List[T.Any]) ArbitraryKWArguments = T.NewType("ArbitraryKWArguments", T.Optional[T.Dict[str, T.Any]]) @@ -64,6 +61,10 @@ class ApplicationWebsocketMixin: + """ + Collection of functions and utilities specific to websocket support with Texas applications. + + """ WS_EXPOSED_FUNC = "WS_EXPOSED_FUNC" @@ -131,7 +132,6 @@ def my_websocket_endpoint(message): """ def processor(func: WSEndpoint) -> WSEndpoint: - if assign_args is True: func = self.websocket_function_arguments_decorator(func) @@ -153,7 +153,7 @@ def ws_sharelib(self, route_str="/lib"): ------- """ - self.add_staticdir2(route_str, WS_STATIC_LIB) + self.route_directory(route_str, WS_STATIC_LIB) def ws_class(self, kls=None, name: str = None): """ @@ -203,8 +203,8 @@ def processor(kls, name=None): if kls is None: return functools.partial(processor, name=name) - else: - return processor(kls, name) + + return processor(kls, name) @staticmethod def _eval_annotation(statement, func): @@ -326,11 +326,11 @@ def processor(real_func): setattr(real_func, self.WS_EXPOSED_FUNC, True) magic_func = self.websocket_class_arguments_decorator(real_func) return magic_func + return processor - else: - setattr(func, self.WS_EXPOSED_FUNC, True) - return func + setattr(func, self.WS_EXPOSED_FUNC, True) + return func class ApplicationRoutingHelperMixin: @@ -340,7 +340,7 @@ class ApplicationRoutingHelperMixin: """ router: RoutingResource - def add(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: + def route(self, route_str: str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: """ :param route_str: A valid URI (starts with a forward slash and no spaces) @@ -351,10 +351,9 @@ def add(self, route_str:str, **kwargs: ArbitraryKWArguments) -> CallableToResour """ return self.router.add(route_str, **kwargs) - # mimic flask's API - route = add + add = route - def add_class(self, route_str: str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: + def route_class(self, route_str: str, **kwargs: ArbitraryKWArguments) -> CallableToResourceDecorator: """ Expose a class and all @app.expose'd method/functions as URL endpoints. @@ -378,7 +377,7 @@ def some_endpoint(self, request): """ return self.router.add(route_str, **kwargs) - + add_class = route_class @staticmethod def expose(route_str, **kwargs): """ @@ -417,8 +416,7 @@ def set_view_postfilter(func): """ return set_postfilter(func) - - def add_resource(self, route_str:str, resource, **kwargs): + def route_resource(self, route_str: str, resource, **kwargs): """ Add's a native/vanilla twisted.web.Resource object to the provided route_str @@ -428,10 +426,11 @@ def add_resource(self, route_str:str, resource, **kwargs): :return: """ - return self.router.add_resource(route_str, thing=resource, route_kwargs=kwargs ) + return self.router.add_resource(route_str, thing=resource, route_kwargs=kwargs) + add_resource = route_resource - def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> File: + def route_file(self, route_str: str, filePath: str, defaultType="text/html") -> File: """ Just a simple helper for a common task of serving individual files @@ -443,11 +442,13 @@ def add_file(self, route_str: str, filePath: str, defaultType="text/html") -> Fi """ assert Path(filePath).exists() + assert Path(filePath).is_file() file_resource = File(filePath, defaultType=defaultType) return self.router.add(route_str)(file_resource) + add_file = route_file - def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path]) -> File: + def route_directory(self, route_str: str, dirPath: T.Union[str, Path]) -> File: """ TODO - remove calls to `add_staticdir2` and just use `add_staticdir` :param route_str: @@ -466,10 +467,8 @@ def add_staticdir2(self, route_str: str, dirPath: T.Union[str, Path]) -> File: return directory_resource - add_staticdir = add_staticdir2 - - - + add_staticdir = route_directory + add_staticdir2 = route_directory class ApplicationErrorHandlingMixin: @@ -478,13 +477,12 @@ class ApplicationErrorHandlingMixin: """ - error_handlers: T.Dict[str, ErrorHandler] - default_handler_cls:BaseHandler = DefaultHandler + default_handler_cls: BaseHandler = DefaultHandler enable_debug: bool site: WebSite - def __init__(self, enable_debug: bool =False): + def __init__(self, enable_debug: bool = False): """ :param enable_debug: Flag to decide if to use debugging tools @@ -494,7 +492,6 @@ def __init__(self, enable_debug: bool =False): self.error_handlers = dict(default=self.default_handler_cls(self)) self.site.setErrorHandler(self.processingFailed) - def handle_error(self, error_type: T.Union[HTTPCode, int, Exception, str], write_over=False) -> T.Callable: """ Decorator utility to add a new :param error_type: specific handler. @@ -519,7 +516,8 @@ def processor(func: ErrorHandler) -> ErrorHandler: return processor - def add_error_handler(self, handler: T.Callable, error_type: T.Union[HTTPCode, int, Exception, str], override=False): + def add_error_handler(self, handler: T.Callable, error_type: T.Union[HTTPCode, int, Exception, str], + override=False): """ TODO cull this out or justify it's existence. @@ -537,7 +535,7 @@ def add_error_handler(self, handler: T.Callable, error_type: T.Union[HTTPCode, i self.error_handlers[error_type] = handler - def processingFailed(self, request:StrRequest, reason: failure.Failure): + def processingFailed(self, request: StrRequest, reason: failure.Failure): """ Internal method @@ -571,10 +569,6 @@ def processingFailed(self, request:StrRequest, reason: failure.Failure): return True - - - - class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin, ApplicationWebsocketMixin): """ Grand unified god module of txweb. @@ -588,8 +582,8 @@ class Application(ApplicationRoutingHelperMixin, ApplicationErrorHandlingMixin, def __init__(self, namespace: str = None, twisted_reactor: T.Optional[PosixReactorBase] = None, - request_factory:StrRequest=StrRequest, - enable_debug:bool=False + request_factory: StrRequest = StrRequest, + enable_debug: bool = False ): """ Similar to Klein and its influence Flask, the goal is to consolidate @@ -604,7 +598,6 @@ def __init__(self, :param enable_debug: """ - self._router = RoutingResource() self._site = WebSite(self._router, request_factory=Application.request_factory_partial(self, request_factory)) self._router.site = self._site @@ -619,19 +612,16 @@ def __init__(self, else: self.__owner_module = None - ApplicationWebsocketMixin.__init__(self) ApplicationRoutingHelperMixin.__init__(self) ApplicationErrorHandlingMixin.__init__(self, enable_debug=enable_debug) - - #Hooks + # Hooks self._before_render_handlers = [] self._after_render_handlers = [] self.__post_init__() - def __post_init__(self) -> None: """ Just a stub to make it easier for subclasses that don't want to mess with @@ -641,7 +631,6 @@ def __post_init__(self) -> None: """ - @staticmethod def request_factory_partial(app: Application, request_kls: StrRequest): """ @@ -660,7 +649,6 @@ def partial(*args, **kwargs): return partial - @property def router(self) -> RoutingResource: """ @@ -692,7 +680,7 @@ def reactor(self) -> PosixReactorBase: def reactor(self, active_reactor: PosixReactorBase): self._reactor = active_reactor - def listenTCP(self, port:int, interface:str= "127.0.0.1") -> Port: + def listenTCP(self, port: int, interface: str = "127.0.0.1") -> Port: """ Convenience helper which adds the HTTP protocol factory to the reactor and set it to listen to the provided interface and port @@ -738,7 +726,7 @@ def _call_before_render(self, request: StrRequest): except Exception: log.error(f"Before render failed {func}") - def _call_after_render(self, request: StrRequest, body:T.Union[bytes,str,int]): + def _call_after_render(self, request: StrRequest, body: T.Union[bytes, str, int]): """ Internal method that iterates over all post filters. From baa8fd2180149944b0b4836418b587d88fa3da43 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:02:48 -0700 Subject: [PATCH 181/185] Make pylint happy --- txweb/http_codes.py | 92 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/txweb/http_codes.py b/txweb/http_codes.py index 27ec31b..aae9f36 100644 --- a/txweb/http_codes.py +++ b/txweb/http_codes.py @@ -26,6 +26,9 @@ def __init__(self, code:int, message:str, exc:T.Optional[Exception]=None): class HTTP3xx(HTTPCode): """ + This class of status code indicates the client must take additional action to complete the request. + Many of these status codes are used in URL redirection. + Arguments: redirect: either an absolute or relative URL to tell the client to redirect too """ @@ -35,6 +38,10 @@ def __init__(self, code: int, redirect: T.Union[str, bytes], message: str = "3xx class HTTP301(HTTP3xx): + """ + 301 Moved Permanently + This and all future requests should be directed to the given URI + """ CODE = 301 def __init__(self, redirect: T.Union[str, bytes]): @@ -42,6 +49,15 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP302(HTTP3xx): + """ + 302 Found (Previously "Moved temporarily") + Tells the client to look at (browse to) another URL. 302 has been superseded by 303 and 307. + This is an example of industry practice contradicting the standard. The HTTP/1.0 specification + (RFC 1945) required the client to perform a temporary redirect (the original describing phrase + was "Moved Temporarily"), but popular browsers implemented 302 with the functionality of a + 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours. + However, some Web applications and frameworks use the 302 status code as if it were the 303. + """ CODE = 302 def __init__(self, redirect: T.Union[str, bytes]): @@ -49,6 +65,11 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP303(HTTP3xx): + """ + The response to the request can be found under another URI using the GET method. + When received in response to a POST (or PUT/DELETE), the client should presume that + the server has received the data and should issue a new GET request to the given URI. + """ CODE = 303 def __init__(self, redirect: T.Union[str, bytes]): @@ -56,6 +77,12 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP304(HTTP3xx): + """ + 304 Not Modified (RFC 7232) + Indicates that the resource has not been modified since the version specified by the request headers + If-Modified-Since or If-None-Match. In such case, there is no need to retransmit the resource since the client + still has a previously-downloaded copy. + """ CODE = 304 def __init__(self, redirect: T.Union[str, bytes]): @@ -63,6 +90,12 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP307(HTTP3xx): + """ + 307 Temporary Redirect (since HTTP/1.1) + In this case, the request should be repeated with another URI; however, future requests should still use the + original URI. In contrast to how 302 was historically implemented, the request method is not allowed to be changed + when reissuing the original request. For example, a POST request should be repeated using another POST request. + """ CODE = 307 def __init__(self, redirect: T.Union[str, bytes]): @@ -70,6 +103,12 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP308(HTTP3xx): + """ + 308 Permanent Redirect (RFC 7538) + The request and all future requests should be repeated using another URI. 307 and 308 parallel the behaviors of + 302 and 301, but do not allow the HTTP method to change. So, for example, submitting a form to a permanently + redirected resource may continue smoothly.[ + """ CODE = 308 def __init__(self, redirect: T.Union[str, bytes]): @@ -77,10 +116,20 @@ def __init__(self, redirect: T.Union[str, bytes]): class HTTP4xx(HTTPCode): + """ + Base Exception for 4xx HTTP codes + + This class of status code is intended for situations in which the error seems to have been caused by the client. + """ pass class HTTP400(HTTP4xx): + """ + 400 Bad Request + The server cannot or will not process the request due to an apparent client error + (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing). + """ CODE = 400 def __init__(self): @@ -88,6 +137,10 @@ def __init__(self): class HTTP401(HTTP4xx): + """ + Similar to 403 Forbidden, but specifically for use when authentication is required and has failed + or has not yet been provided. + """ CODE = 401 def __init__(self): @@ -95,6 +148,10 @@ def __init__(self): class HTTP403(HTTP4xx): + """ + The request contained valid data and was understood by the server, but the server is refusing action. + + """ CODE = 403 def __init__(self): @@ -102,6 +159,12 @@ def __init__(self): class HTTP404(HTTP4xx): + """ + 404 Not Found + + The requested resource could not be found but may be available in the future. + Subsequent requests by the client are permissible. + """ CODE = 404 def __init__(self, exc=None): @@ -109,6 +172,11 @@ def __init__(self, exc=None): class HTTP410(HTTP4xx): + """ + 410 Gone + Indicates that the resource requested is no longer available and will not be available again. + + """ CODE = 410 def __init__(self): @@ -116,6 +184,11 @@ def __init__(self): class HTTP405(HTTP4xx): + """ + A request method is not supported for the requested resource; + for example, a GET request on a form that requires data to be + presented via POST, or a PUT request on a read-only resource. + """ CODE = 405 def __init__(self, exc=None): @@ -123,10 +196,25 @@ def __init__(self, exc=None): class HTTP5xx(HTTPCode): + """ + The server failed to fulfill a request.[61] + + Response status codes beginning with the digit "5" indicate cases in which the server is aware that it has + encountered an error or is otherwise incapable of performing the request. + Except when responding to a HEAD request, the server should include an entity containing an explanation of the + error situation, and indicate whether it is a temporary or permanent condition. + Likewise, user agents should display any included entity to the user. + These response codes are applicable to any request method. + """ pass class HTTP500(HTTP5xx): + """ + 500 Internal Server Error + A generic error message, given when an unexpected condition was encountered and no more specific message + is suitable. + """ CODE = 500 def __init__(self, message="Internal Server Error"): @@ -134,4 +222,8 @@ def __init__(self, message="Internal Server Error"): class Unrenderable(EnvironmentError): + """ + An class definition or view function's result cannot be rendered to the client. + + """ pass From 1e34febc0181733febdedac6f8788d9207be1ca3 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:03:19 -0700 Subject: [PATCH 182/185] Don't finish on redirect helper, let the error handler or application decide when the session is finished --- txweb/lib/str_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/txweb/lib/str_request.py b/txweb/lib/str_request.py index 7cdb09b..0f90d81 100644 --- a/txweb/lib/str_request.py +++ b/txweb/lib/str_request.py @@ -405,4 +405,4 @@ def redirect(self, url: T.Union[str, bytes], code=FOUND) -> T.NoReturn: """ self.setResponseCode(code) self.setHeader(b"location", url) - self.ensureFinished() + #self.ensureFinished() From b4e4a69660691b3929bdcc8e3457e3b0c66fa899 Mon Sep 17 00:00:00 2001 From: devdave Date: Mon, 21 Dec 2020 08:03:58 -0700 Subject: [PATCH 183/185] make pylint happy and code cleaner --- txweb/lib/wsprotocol.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/txweb/lib/wsprotocol.py b/txweb/lib/wsprotocol.py index 1dc3b81..b4d51f2 100644 --- a/txweb/lib/wsprotocol.py +++ b/txweb/lib/wsprotocol.py @@ -191,15 +191,16 @@ def ask(self, endpoint, **values) -> Deferred: """ - if len(self.deferred_asks) < self.MAX_ASKS: - request_token = uuid4().hex - d = Deferred() - self.deferred_asks[request_token] = d - self.sendDict(endpoint=endpoint, type="ask", caller_id=request_token, args=values) - return d - else: + if len(self.deferred_asks) > self.MAX_ASKS: raise EnvironmentError("Maximum # of pending asks reached") + request_token = uuid4().hex + d = Deferred() + self.deferred_asks[request_token] = d + self.sendDict(endpoint=endpoint, type="ask", caller_id=request_token, args=values) + return d + + def handleResponse(self, message: MessageHandler) -> T.NoReturn: """ Server side asked client a question, shunt this message through the @@ -224,7 +225,7 @@ def handleEndPoint(self, message: MessageHandler) -> T.NoReturn: """ Handles incoming ask and tell messages. - :param message: + """ endpoint_func = self.factory.get_endpoint(message['endpoint']) @@ -276,6 +277,3 @@ def onMessage(self, payload, is_binary) -> T.NoReturn: self.handleEndPoint(message) else: self.my_log.error("Got message without an endpoint or caller_id: {raw!r}", raw=message.raw_message) - - - From c4496f2f2d8abbcb2643fa95f7cf84e57da773f8 Mon Sep 17 00:00:00 2001 From: devdave Date: Thu, 14 Jan 2021 12:00:11 -0700 Subject: [PATCH 184/185] Add in some minor documentation --- source/manual/http_view_classes.md | 18 +++++++++++ source/manual/short_history.md | 51 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 source/manual/short_history.md diff --git a/source/manual/http_view_classes.md b/source/manual/http_view_classes.md index cc577dd..79a73d3 100644 --- a/source/manual/http_view_classes.md +++ b/source/manual/http_view_classes.md @@ -43,6 +43,24 @@ Above would add two new URLS to the map. Both endpoints default to only accept GET and HEAD requests. +Motivation/purpose for design +----------------------------- + +Can be used for rapidly building out an HTTP based API where +the `add_class` is provided a noun and all of the `exposed` methods are verbs against +that noun (eg CRUD ). Additionally all of the API endpoints are consolidated into +one place instead of spread out across multiple module level functions. + + +Mechanism +--------- + +As demonstrated with the `Widget` example class, the class is instantiated internally +by Texas and a hard reference is stored internally to ensure the objects persistence for +the life of the process. + + + Warning ------- diff --git a/source/manual/short_history.md b/source/manual/short_history.md new file mode 100644 index 0000000..727d723 --- /dev/null +++ b/source/manual/short_history.md @@ -0,0 +1,51 @@ + +Short history +------------- + +I started on TxWeb/Texas all the way back in early 2011 and have iterated and done major +refactors of this project since then. + +Generation one of TxWeb mimicked the original CherryPy in that a website was an object graph + +```python + class Widget(): + + def index(self, request): + return "I am the list of widgets" + + def create(self, request): + pass + + def replace(self, request): + pass + + def update(self, request): + pass + + def delete(self, request): + pass + + class Root(): + + def index(self, request): + return "Hello world" + + widgets = Widget() +``` + +That created a URL heirachy like + +```python + / + /widgets/ + /widgets/create/ + /widgets/replace/ + /widgets/update/ + /widgets/delete/ +``` + +I switched that out for my own URL parser which kind of worked but for anyone familiar +with werkzeug, is a massive project to itself. + +In 2020, I gutted out my own URL parser in favor of using werkzeug (similar if not exactly like +Klein). From e447fbefd16134cb2f83323c04c20c41638d7da3 Mon Sep 17 00:00:00 2001 From: devdave Date: Wed, 24 Mar 2021 12:48:16 -0600 Subject: [PATCH 185/185] fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 575f484..73b951f 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ from txweb import Application app = Application(__name__) -@app,route("/hello") +@app.route("/hello") def provide_hello(request): return "Hello World"