diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 455c241..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: ci -on: - push: - branches: - - master -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: 3.x - - run: pip install mkdocs-material - - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index fa58d62..d3d6425 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ *.egg-info/ *.eggs/ *.log +*.map +*.sqlite3 dist/ __pycache__/ -reactor/static/reactor.js* +.pytest_cache/ +reactor/static/reactor.min.* node_modules/* -*.sqlite3 .coverage site/ +.vscode/ diff --git a/Makefile b/Makefile index 9dc2504..408d5ee 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,19 @@ -all: build-deps build - -build-deps: - npm install coffeescript uglify-es - -build: - ./node_modules/.bin/coffee -c reactor/static/reactor/reactor.coffee - ./node_modules/.bin/uglifyjs reactor/static/reactor/reactor.js >reactor/static/reactor/reactor.min.js - mv reactor/static/reactor/reactor.min.js reactor/static/reactor/reactor.js - poetry build +all: install build install: + yarn install pip install --upgrade pip - pip install poetry - poetry install + pip install -e .[dev] + python setup.py develop + +watch-js: + node esbuild.conf.js -w + +build: + node esbuild.conf.js + python setup.py sdist test: flake8 --max-line-length=80 reactor diff --git a/esbuild.conf.js b/esbuild.conf.js new file mode 100644 index 0000000..aa40d51 --- /dev/null +++ b/esbuild.conf.js @@ -0,0 +1,21 @@ +const esbuild = require('esbuild'); + +const isWatch = process.argv.includes('-w'); +const isBuild = !isWatch; +const mode = isBuild ? 'production' : 'development'; + + +const buildOptions = { + entryPoints: ['reactor/static/reactor/reactor.js'], + define: { + 'process.env.NODE_ENV': JSON.stringify(mode), + }, + bundle: true, + sourcemap: true, + minify: isBuild, + incremental: isWatch, + outfile: 'reactor/static/reactor/reactor.min.js', + watch: isWatch, +}; + +esbuild.build(buildOptions); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fcba7db..0000000 --- a/package-lock.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "coffeescript": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.5.1.tgz", - "integrity": "sha512-J2jRPX0eeFh5VKyVnoLrfVFgLZtnnmp96WQSLAS8OrLm2wtQLcnikYKe1gViJKDH7vucjuhHvBKKBP3rKcD1tQ==" - }, - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "requires": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - } - } - } -} diff --git a/package.json b/package.json new file mode 100644 index 0000000..712b80a --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "reactor", + "version": "1.0.0", + "description": "Brings LiveView from Phoenix framework into Django", + "main": "index.js", + "repository": "git@github.com:edelvalle/reactor.git", + "author": "Eddy Ernesto del Valle Pino ", + "license": "MIT", + "dependencies": { + "morphdom": "^2.6.1", + "reconnecting-websocket": "^4.4.0" + }, + "devDependencies": { + "esbuild": "^0.13.9" + } +} diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index a593d98..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1261 +0,0 @@ -[[package]] -name = "asgiref" -version = "3.3.1" -description = "ASGI specs, helper code, and adapters" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.extras] -tests = ["pytest", "pytest-asyncio"] - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "20.3.0" -description = "Classes Without Boilerplate" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] - -[[package]] -name = "autobahn" -version = "21.1.1" -description = "WebSocket client & server library, WAMP real-time framework" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cryptography = ">=2.9.2" -hyperlink = ">=20.0.1" -txaio = ">=20.4.1" - -[package.extras] -accelerate = ["wsaccel (>=0.6.2)"] -all = ["zope.interface (>=3.6.0)", "twisted (>=20.3.0)", "attrs (>=19.2.0)", "wsaccel (>=0.6.2)", "python-snappy (>=0.5)", "msgpack (>=0.6.1)", "ujson (>=1.35)", "cbor2 (>=5.0.1)", "cbor (>=1.0.0)", "py-ubjson (>=0.8.4)", "flatbuffers (>=1.10)", "pyopenssl (>=16.2.0)", "service-identity (>=18.1.0)", "pynacl (>=1.0.1)", "pytrie (>=0.2)", "pyqrcode (>=1.1)", "cffi (>=1.11.5)", "argon2-cffi (>=18.1.0)", "passlib (>=1.7.1)", "cbor2 (>=5.1.0)", "zlmdb (>=20.4.1)", "autobahn (>=18.11.2)", "web3 (>=4.8.1)", "jinja2 (>=2.11.2)", "rlp (>=2.0.1)", "py-eth-sig-utils (>=0.4.0)", "py-ecc (>=1.7.1)", "eth-abi (>=1.3.0)", "mnemonic (>=0.13)", "base58 (>=1.0.2,<2.0)", "ecdsa (>=0.13)", "py-multihash (>=0.2.3)"] -compress = ["python-snappy (>=0.5)"] -dev = ["pep8-naming (>=0.3.3)", "flake8 (>=2.5.1)", "pyflakes (>=1.0.0)", "pytest (>=2.8.6,<3.3.0)", "twine (>=1.6.5)", "sphinx (>=1.2.3)", "sphinxcontrib-images (>=0.9.2)", "pyenchant (>=1.6.6)", "sphinxcontrib-spelling (>=2.1.2)", "sphinx-rtd-theme (>=0.1.9)", "awscli", "qualname", "passlib", "wheel", "pytest-asyncio (<0.6)", "pytest-aiohttp"] -encryption = ["pyopenssl (>=16.2.0)", "service-identity (>=18.1.0)", "pynacl (>=1.0.1)", "pytrie (>=0.2)", "pyqrcode (>=1.1)"] -nvx = ["cffi (>=1.11.5)"] -scram = ["cffi (>=1.11.5)", "argon2-cffi (>=18.1.0)", "passlib (>=1.7.1)"] -serialization = ["msgpack (>=0.6.1)", "ujson (>=1.35)", "cbor2 (>=5.0.1)", "cbor (>=1.0.0)", "py-ubjson (>=0.8.4)", "flatbuffers (>=1.10)"] -twisted = ["zope.interface (>=3.6.0)", "twisted (>=20.3.0)", "attrs (>=19.2.0)"] -xbr = ["cbor2 (>=5.1.0)", "zlmdb (>=20.4.1)", "twisted (>=20.3.0)", "autobahn (>=18.11.2)", "web3 (>=4.8.1)", "jinja2 (>=2.11.2)", "rlp (>=2.0.1)", "py-eth-sig-utils (>=0.4.0)", "py-ecc (>=1.7.1)", "eth-abi (>=1.3.0)", "mnemonic (>=0.13)", "base58 (>=1.0.2,<2.0)", "ecdsa (>=0.13)", "py-multihash (>=0.2.3)"] - -[[package]] -name = "automat" -version = "20.2.0" -description = "Self-service finite-state machines for the programmer on the go." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -attrs = ">=19.2.0" -six = "*" - -[package.extras] -visualize = ["graphviz (>0.5.1)", "Twisted (>=16.1.1)"] - -[[package]] -name = "cffi" -version = "1.14.4" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "channels" -version = "3.0.3" -description = "Brings async, event-driven capabilities to Django. Django 2.2 and up only." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -asgiref = ">=3.2.10,<4" -daphne = ">=3.0,<4" -Django = ">=2.2" - -[package.extras] -tests = ["pytest", "pytest-django", "pytest-asyncio", "async-generator", "async-timeout", "coverage (>=4.5,<5.0)"] - -[[package]] -name = "click" -version = "7.1.2" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "colorama" -version = "0.4.4" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "constantly" -version = "15.1.0" -description = "Symbolic constants in Python" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "cryptography" -version = "3.4.1" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.12" -setuptools-rust = ">=0.11.4" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] - -[[package]] -name = "cssselect" -version = "1.1.0" -description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "daphne" -version = "3.0.1" -description = "Django ASGI (HTTP/WebSocket) server" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -asgiref = ">=3.2.10,<4" -autobahn = ">=0.18" -twisted = {version = ">=18.7", extras = ["tls"]} - -[package.extras] -tests = ["hypothesis (==4.23)", "pytest (>=3.10,<4.0)", "pytest-asyncio (>=0.8,<1.0)"] - -[[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "main" -optional = false -python-versions = ">=3.6, <3.7" - -[[package]] -name = "django" -version = "3.1.6" -description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -asgiref = ">=3.2.10,<4" -pytz = "*" -sqlparse = ">=0.2.2" - -[package.extras] -argon2 = ["argon2-cffi (>=16.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "flake8" -version = "3.8.4" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" - -[[package]] -name = "h11" -version = "0.12.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "httptools" -version = "0.1.1" -description = "A collection of framework independent HTTP protocol utils." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -test = ["Cython (==0.29.14)"] - -[[package]] -name = "hyperlink" -version = "21.0.0" -description = "A featureful, immutable, and correct URL for Python." -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -idna = ">=2.5" - -[[package]] -name = "idna" -version = "3.1" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" -optional = false -python-versions = ">=3.4" - -[[package]] -name = "importlib-metadata" -version = "3.4.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] - -[[package]] -name = "incremental" -version = "17.5.0" -description = "" -category = "main" -optional = false -python-versions = "*" - -[package.extras] -scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "lxml" -version = "4.6.2" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] -source = ["Cython (>=0.29.7)"] - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "orjson" -version = "3.4.8" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "packaging" -version = "20.9" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = ">=2.0.2" - -[[package]] -name = "pluggy" -version = "0.13.1" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "py" -version = "1.10.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyasn1" -version = "0.4.8" -description = "ASN.1 types and codecs" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyasn1-modules" -version = "0.2.8" -description = "A collection of ASN.1-based protocols modules." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.5.0" - -[[package]] -name = "pycodestyle" -version = "2.6.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pycparser" -version = "2.20" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pydantic" -version = "1.7.3" -description = "Data validation and settings management using python 3.6 type hinting" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] -typing_extensions = ["typing-extensions (>=3.7.2)"] - -[[package]] -name = "pyflakes" -version = "2.2.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyhamcrest" -version = "2.0.2" -description = "Hamcrest framework for matcher objects" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "pyopenssl" -version = "20.0.1" -description = "Python wrapper module around the OpenSSL library" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" - -[package.dependencies] -cryptography = ">=3.2" -six = ">=1.5.2" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -test = ["flaky", "pretend", "pytest (>=3.0.1)"] - -[[package]] -name = "pyparsing" -version = "2.4.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "pytest" -version = "6.2.2" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<1.0.0a1" -py = ">=1.8.2" -toml = "*" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.14.0" -description = "Pytest support for asyncio." -category = "dev" -optional = false -python-versions = ">= 3.5" - -[package.dependencies] -pytest = ">=5.4.0" - -[package.extras] -testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-django" -version = "4.1.0" -description = "A Django plugin for pytest." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -pytest = ">=5.4.0" - -[package.extras] -docs = ["sphinx", "sphinx-rtd-theme"] -testing = ["django", "django-configurations (>=2.0)"] - -[[package]] -name = "python-dotenv" -version = "0.15.0" -description = "Add .env support to your django/flask apps in development and deployments" -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytz" -version = "2021.1" -description = "World timezone definitions, modern and historical" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pyyaml" -version = "5.4.1" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[[package]] -name = "selenium" -version = "3.141.0" -description = "Python bindings for Selenium" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -urllib3 = "*" - -[[package]] -name = "semantic-version" -version = "2.8.5" -description = "A library implementing the 'SemVer' scheme." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "service-identity" -version = "18.1.0" -description = "Service identity verification for pyOpenSSL & cryptography." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -attrs = ">=16.0.0" -cryptography = "*" -pyasn1 = "*" -pyasn1-modules = "*" - -[package.extras] -dev = ["coverage (>=4.2.0)", "pytest", "sphinx", "idna", "pyopenssl"] -docs = ["sphinx"] -idna = ["idna"] -tests = ["coverage (>=4.2.0)", "pytest"] - -[[package]] -name = "setuptools-rust" -version = "0.11.6" -description = "Setuptools rust extension plugin" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -semantic-version = ">=2.6.0" -toml = ">=0.9.0" - -[[package]] -name = "six" -version = "1.15.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "splinter" -version = "0.14.0" -description = "browser abstraction for web acceptance testing" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -cssselect = {version = "*", optional = true, markers = "extra == \"django\""} -Django = {version = ">=2.0.6", optional = true, markers = "python_version > \"3.3\" and extra == \"django\""} -lxml = {version = ">=2.3.6", optional = true, markers = "extra == \"django\""} -selenium = ">=3.141.0" -six = "*" - -[package.extras] -django = ["lxml (>=2.3.6)", "cssselect", "six", "Django (>=1.7.11)", "Django (>=2.0.6)"] -flask = ["Flask (>=1.0.2)", "lxml (>=2.3.6)", "cssselect"] -"zope.testbrowser" = ["zope.testbrowser (>=5.2.4)", "lxml (>=4.2.4)", "cssselect"] - -[[package]] -name = "sqlparse" -version = "0.4.1" -description = "A non-validating SQL parser." -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "twisted" -version = "20.3.0" -description = "An asynchronous networking framework written in Python" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" - -[package.dependencies] -attrs = ">=19.2.0" -Automat = ">=0.3.0" -constantly = ">=15.1" -hyperlink = ">=17.1.1" -idna = {version = ">=0.6,<2.3 || >2.3", optional = true, markers = "extra == \"tls\""} -incremental = ">=16.10.1" -PyHamcrest = ">=1.9.0,<1.10.0 || >1.10.0" -pyopenssl = {version = ">=16.0.0", optional = true, markers = "extra == \"tls\""} -service_identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} -"zope.interface" = ">=4.4.2" - -[package.extras] -all_non_platform = ["pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)", "idna (>=0.6,!=2.3)", "pyasn1", "cryptography (>=2.5)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "soappy", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)"] -conch = ["pyasn1", "cryptography (>=2.5)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)"] -dev = ["pyflakes (>=1.0.0)", "twisted-dev-tools (>=0.0.2)", "python-subunit", "sphinx (>=1.3.1)", "towncrier (>=17.4.0)"] -http2 = ["h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)"] -macos_platform = ["pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)", "idna (>=0.6,!=2.3)", "pyasn1", "cryptography (>=2.5)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "soappy", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)"] -osx_platform = ["pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)", "idna (>=0.6,!=2.3)", "pyasn1", "cryptography (>=2.5)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "soappy", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)"] -serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] -soap = ["soappy"] -tls = ["pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)", "idna (>=0.6,!=2.3)"] -windows_platform = ["pywin32 (!=226)", "pyopenssl (>=16.0.0)", "service_identity (>=18.1.0)", "idna (>=0.6,!=2.3)", "pyasn1", "cryptography (>=2.5)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "soappy", "pyserial (>=3.0)", "h2 (>=3.0,<4.0)", "priority (>=1.1.0,<2.0)", "pywin32 (!=226)"] - -[[package]] -name = "txaio" -version = "20.12.1" -description = "Compatibility API between asyncio/Twisted/Trollius" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -all = ["zope.interface (>=3.6)", "twisted (>=20.3.0)"] -dev = ["wheel", "pytest (>=2.6.4)", "pytest-cov (>=1.8.1)", "pep8 (>=1.6.2)", "sphinx (>=1.2.3)", "pyenchant (>=1.6.6)", "sphinxcontrib-spelling (>=2.1.2)", "sphinx-rtd-theme (>=0.1.9)", "tox (>=2.1.1)", "mock (==1.3.0)", "twine (>=1.6.5)", "tox-gh-actions (>=2.2.0)"] -twisted = ["zope.interface (>=3.6)", "twisted (>=20.3.0)"] - -[[package]] -name = "typing-extensions" -version = "3.7.4.3" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "urllib3" -version = "1.26.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "uvicorn" -version = "0.12.3" -description = "The lightning-fast ASGI server." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -click = ">=7.0.0,<8.0.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.1.0,<0.2.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} -uvloop = {version = ">=0.14.0", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchgod = {version = ">=0.6,<0.7", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=8.0.0,<9.0.0", optional = true, markers = "extra == \"standard\""} - -[package.extras] -standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6,<0.7)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0)", "colorama (>=0.4)"] - -[[package]] -name = "uvloop" -version = "0.14.0" -description = "Fast implementation of asyncio event loop on top of libuv" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "watchgod" -version = "0.6" -description = "Simple, modern file watching and code reload in python." -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "websockets" -version = "8.0.2" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "whitenoise" -version = "5.2.0" -description = "Radically simplified static file serving for WSGI applications" -category = "dev" -optional = false -python-versions = ">=3.5, <4" - -[package.extras] -brotli = ["brotli"] - -[[package]] -name = "zipp" -version = "3.4.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] - -[[package]] -name = "zope.interface" -version = "5.2.0" -description = "Interfaces for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -docs = ["sphinx", "repoze.sphinx.autointerface"] -test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] -testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] - -[metadata] -lock-version = "1.1" -python-versions = ">=3.6,<4" -content-hash = "10f10296f05edd0779fe140d473fbdd479e35665df744ad85653bd5fb61afffd" - -[metadata.files] -asgiref = [ - {file = "asgiref-3.3.1-py3-none-any.whl", hash = "sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17"}, - {file = "asgiref-3.3.1.tar.gz", hash = "sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, -] -autobahn = [ - {file = "autobahn-21.1.1-py2.py3-none-any.whl", hash = "sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"}, - {file = "autobahn-21.1.1.tar.gz", hash = "sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e"}, -] -automat = [ - {file = "Automat-20.2.0-py2.py3-none-any.whl", hash = "sha256:b6feb6455337df834f6c9962d6ccf771515b7d939bca142b29c20c2376bc6111"}, - {file = "Automat-20.2.0.tar.gz", hash = "sha256:7979803c74610e11ef0c0d68a2942b152df52da55336e0c9d58daf1831cbdf33"}, -] -cffi = [ - {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, - {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, - {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, - {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, - {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, - {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, - {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, - {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, - {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, - {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, - {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, - {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, - {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, - {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, - {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, - {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, - {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, - {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, - {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, - {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, - {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, - {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, - {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, - {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, - {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, - {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, -] -channels = [ - {file = "channels-3.0.3-py3-none-any.whl", hash = "sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317"}, - {file = "channels-3.0.3.tar.gz", hash = "sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -constantly = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, -] -cryptography = [ - {file = "cryptography-3.4.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:e63b8da1d77ff60a73d72db68cb72e8ffbe9f7319e5ffa23f6bfe2757d6871e3"}, - {file = "cryptography-3.4.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:4d8df1f5b6b172fe53a465d8fc32134a07ccd4dc677f19af85562bcbc7e97504"}, - {file = "cryptography-3.4.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cd1d14b6c52d9372a7f4682576fa7d9c9d256afa6cc50828b337dbb8a9596066"}, - {file = "cryptography-3.4.1-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:85c7d84beacf32bf629767f06c78af0350eb869e2830e4ebe05b239c695eca38"}, - {file = "cryptography-3.4.1-cp36-abi3-win32.whl", hash = "sha256:007edf78a0b96513b94c1c7cc515286aec72f787bdcd11892edcdec9e27e936d"}, - {file = "cryptography-3.4.1-cp36-abi3-win_amd64.whl", hash = "sha256:423c12d04df7ed3323e74745cba91056a411bd8f57609a6a64562845ccc5541a"}, - {file = "cryptography-3.4.1.tar.gz", hash = "sha256:be70bdaa29bcacf70896dae3a6f3eef91daf51bfba8a49dbfb9c23bb2cc914ba"}, -] -cssselect = [ - {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, - {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, -] -daphne = [ - {file = "daphne-3.0.1-py3-none-any.whl", hash = "sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a"}, - {file = "daphne-3.0.1.tar.gz", hash = "sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"}, -] -dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, -] -django = [ - {file = "Django-3.1.6-py3-none-any.whl", hash = "sha256:169e2e7b4839a7910b393eec127fd7cbae62e80fa55f89c6510426abf673fe5f"}, - {file = "Django-3.1.6.tar.gz", hash = "sha256:c6c0462b8b361f8691171af1fb87eceb4442da28477e12200c40420176206ba7"}, -] -flake8 = [ - {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, - {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, -] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -httptools = [ - {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, - {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, - {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, - {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, - {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, - {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, - {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, - {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, - {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, - {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, - {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, - {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, -] -hyperlink = [ - {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"}, - {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"}, -] -idna = [ - {file = "idna-3.1-py3-none-any.whl", hash = "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16"}, - {file = "idna-3.1.tar.gz", hash = "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"}, -] -importlib-metadata = [ - {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, - {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, -] -incremental = [ - {file = "incremental-17.5.0-py2.py3-none-any.whl", hash = "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f"}, - {file = "incremental-17.5.0.tar.gz", hash = "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -lxml = [ - {file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"}, - {file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"}, - {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"}, - {file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"}, - {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"}, - {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"}, - {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"}, - {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"}, - {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"}, - {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"}, - {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"}, - {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"}, - {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"}, - {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"}, - {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"}, - {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"}, - {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"}, - {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"}, - {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"}, - {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"}, - {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"}, - {file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"}, - {file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"}, - {file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"}, - {file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -orjson = [ - {file = "orjson-3.4.8-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:3bf9cd593f48329d8356192b453c20850ecb135a92c70df42ccd652e0496c206"}, - {file = "orjson-3.4.8-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:a2c6b0436f89a8393add5c8ea493176f4ff671257720e221eb52c6c51973c07b"}, - {file = "orjson-3.4.8-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:b7907822cc6cc4bfc3fe6dc8ed2ea98b4b36714812a9ac329b7dd740a7076e02"}, - {file = "orjson-3.4.8-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:567c380acca015cdaf520d39853fea43e23c75c9ad7c49890464d2d509cf1025"}, - {file = "orjson-3.4.8-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ba9c05874d5eab35e5fe6e47cd4b9a1cf89eb9400efb11783f864e04747f298c"}, - {file = "orjson-3.4.8-cp36-none-win_amd64.whl", hash = "sha256:bdbf4ec86a6a8a907a085933ecc4dd15177f1dab20063590bb6f7f0517c391eb"}, - {file = "orjson-3.4.8-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:e4c0ba0b532ef82b992813b01ef896b8ebc3ed8a07f7001f37374184ff98e552"}, - {file = "orjson-3.4.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:63cbf9602d79e55aafdb28afd6d5456a503f6ced99daf03d80411f7885970bd1"}, - {file = "orjson-3.4.8-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:dd5c96427fea3a2ebbcf035494f6b291ec6eb9c29be75493cbbae5e7282fbbb4"}, - {file = "orjson-3.4.8-cp37-none-win_amd64.whl", hash = "sha256:5a742382013466d79a2b0c81413fdd308059d094f432c0797ce721e5e549708d"}, - {file = "orjson-3.4.8-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:d7069adfc5ddd1b264c06e86cea742445c5c2a9acaa2f72add98b3b5b2b6d1c9"}, - {file = "orjson-3.4.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e2d5cc1186e5bc9910ad96d8f241105a998d6e02d1374a3cbe9997838f30385a"}, - {file = "orjson-3.4.8-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:bbe405d84c4ab14dafdb9fd08aa11b172803089094f9f8f2e9552a617d5bdcd2"}, - {file = "orjson-3.4.8-cp38-none-win_amd64.whl", hash = "sha256:6d7c3edace4ac7314d3b98cee30191af38f1581fcad7b2c5be139b8bd3c45da8"}, - {file = "orjson-3.4.8-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:0e4c4b7151b88f6d7deda26ce04880fbdf15e31b0e1af226e25134b10d1ba0b3"}, - {file = "orjson-3.4.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bb3bd703069127b899090c0ca24bc8ce5e5137f17e7d1a8d2080e0e3361c4ee3"}, - {file = "orjson-3.4.8-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:2599ac12c5992dfb44870e71bd96ce6b0df7f7248a9548664810e889685b967b"}, - {file = "orjson-3.4.8-cp39-none-win_amd64.whl", hash = "sha256:c7ddf86586810cffa37b24150f6f29c193d80322ad1d807be791cb2a1b8954e8"}, - {file = "orjson-3.4.8.tar.gz", hash = "sha256:08ac106a4e67c7dd3010a948d336294a7549c62677bee9752011347c7688af37"}, -] -packaging = [ - {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, - {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, -] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] -pyasn1-modules = [ - {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"}, - {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"}, - {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"}, - {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"}, - {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"}, - {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"}, - {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"}, - {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"}, - {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"}, - {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"}, - {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"}, - {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"}, - {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"}, -] -pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, -] -pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, -] -pydantic = [ - {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, - {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, - {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"}, - {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"}, - {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"}, - {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"}, - {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"}, - {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"}, - {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"}, - {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"}, - {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"}, - {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"}, - {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"}, - {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"}, - {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"}, - {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"}, - {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"}, - {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"}, - {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"}, - {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"}, - {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"}, - {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, -] -pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, -] -pyhamcrest = [ - {file = "PyHamcrest-2.0.2-py3-none-any.whl", hash = "sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"}, - {file = "PyHamcrest-2.0.2.tar.gz", hash = "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316"}, -] -pyopenssl = [ - {file = "pyOpenSSL-20.0.1-py2.py3-none-any.whl", hash = "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"}, - {file = "pyOpenSSL-20.0.1.tar.gz", hash = "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pytest = [ - {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, - {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.14.0.tar.gz", hash = "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700"}, - {file = "pytest_asyncio-0.14.0-py3-none-any.whl", hash = "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d"}, -] -pytest-django = [ - {file = "pytest-django-4.1.0.tar.gz", hash = "sha256:26f02c16d36fd4c8672390deebe3413678d89f30720c16efb8b2a6bf63b9041f"}, - {file = "pytest_django-4.1.0-py3-none-any.whl", hash = "sha256:10e384e6b8912ded92db64c58be8139d9ae23fb8361e5fc139d8e4f8fc601bc2"}, -] -python-dotenv = [ - {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, - {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, -] -pytz = [ - {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, - {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, -] -pyyaml = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, -] -selenium = [ - {file = "selenium-3.141.0-py2.py3-none-any.whl", hash = "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c"}, - {file = "selenium-3.141.0.tar.gz", hash = "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d"}, -] -semantic-version = [ - {file = "semantic_version-2.8.5-py2.py3-none-any.whl", hash = "sha256:45e4b32ee9d6d70ba5f440ec8cc5221074c7f4b0e8918bdab748cc37912440a9"}, - {file = "semantic_version-2.8.5.tar.gz", hash = "sha256:d2cb2de0558762934679b9a104e82eca7af448c9f4974d1f3eeccff651df8a54"}, -] -service-identity = [ - {file = "service_identity-18.1.0-py2.py3-none-any.whl", hash = "sha256:001c0707759cb3de7e49c078a7c0c9cd12594161d3bf06b9c254fdcb1a60dc36"}, - {file = "service_identity-18.1.0.tar.gz", hash = "sha256:0858a54aabc5b459d1aafa8a518ed2081a285087f349fe3e55197989232e2e2d"}, -] -setuptools-rust = [ - {file = "setuptools-rust-0.11.6.tar.gz", hash = "sha256:a5b5954909cbc5d66b914ee6763f81fa2610916041c7266105a469f504a7c4ca"}, - {file = "setuptools_rust-0.11.6-py3-none-any.whl", hash = "sha256:5acf8cd8e89d57f0cd3cc942f60fa2ccfdede4c7a0b0d4b28eb7ab756df30347"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -splinter = [ - {file = "splinter-0.14.0-py3-none-any.whl", hash = "sha256:7e5e69c5b76ada909283465cdc3636e2632f7e557932ce96ab9c0432b0b32f7f"}, - {file = "splinter-0.14.0.tar.gz", hash = "sha256:459e39e7a9f7572db6f1cdb5fdc5ccfc6404f021dccb969ee6287be2386a40db"}, -] -sqlparse = [ - {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, - {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -twisted = [ - {file = "Twisted-20.3.0-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:cdbc4c7f0cd7a2218b575844e970f05a1be1861c607b0e048c9bceca0c4d42f7"}, - {file = "Twisted-20.3.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d267125cc0f1e8a0eed6319ba4ac7477da9b78a535601c49ecd20c875576433a"}, - {file = "Twisted-20.3.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:356e8d8dd3590e790e3dba4db139eb8a17aca64b46629c622e1b1597a4a92478"}, - {file = "Twisted-20.3.0-cp27-cp27m-win32.whl", hash = "sha256:ca3a0b8c9110800e576d89b5337373e52018b41069bc879f12fa42b7eb2d0274"}, - {file = "Twisted-20.3.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cd1dc5c85b58494138a3917752b54bb1daa0045d234b7c132c37a61d5483ebad"}, - {file = "Twisted-20.3.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:94ac3d55a58c90e2075c5fe1853f2aa3892b73e3bf56395f743aefde8605eeaa"}, - {file = "Twisted-20.3.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7408c6635ee1b96587289283ebe90ee15dbf9614b05857b446055116bc822d29"}, - {file = "Twisted-20.3.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c09c47ff9750a8e3aa60ad169c4b95006d455a29b80ad0901f031a103b2991cd"}, - {file = "Twisted-20.3.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:158ddb80719a4813d292293ac44ba41d8b56555ed009d90994a278237ee63d2c"}, - {file = "Twisted-20.3.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:040eb6641125d2a9a09cf198ec7b83dd8858c6f51f6770325ed9959c00f5098f"}, - {file = "Twisted-20.3.0-cp35-cp35m-win32.whl", hash = "sha256:147780b8caf21ba2aef3688628eaf13d7e7fe02a86747cd54bfaf2140538f042"}, - {file = "Twisted-20.3.0-cp35-cp35m-win_amd64.whl", hash = "sha256:25ffcf37944bdad4a99981bc74006d735a678d2b5c193781254fbbb6d69e3b22"}, - {file = "Twisted-20.3.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:a58e61a2a01e5bcbe3b575c0099a2bcb8d70a75b1a087338e0c48dd6e01a5f15"}, - {file = "Twisted-20.3.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:7c547fd0215db9da8a1bc23182b309e84a232364cc26d829e9ee196ce840b114"}, - {file = "Twisted-20.3.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2182000d6ffc05d269e6c03bfcec8b57e20259ca1086180edaedec3f1e689292"}, - {file = "Twisted-20.3.0-cp36-cp36m-win32.whl", hash = "sha256:70952c56e4965b9f53b180daecf20a9595cf22b8d0935cd3bd664c90273c3ab2"}, - {file = "Twisted-20.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3281d9ce889f7b21bdb73658e887141aa45a102baf3b2320eafcfba954fcefec"}, - {file = "Twisted-20.3.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:e92703bed0cc21d6cb5c61d66922b3b1564015ca8a51325bd164a5e33798d504"}, - {file = "Twisted-20.3.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467"}, - {file = "Twisted-20.3.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:894f6f3cfa57a15ea0d0714e4283913a5f2511dbd18653dd148eba53b3919797"}, - {file = "Twisted-20.3.0-cp37-cp37m-win32.whl", hash = "sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"}, - {file = "Twisted-20.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d95803193561a243cb0401b0567c6b7987d3f2a67046770e1dccd1c9e49a9780"}, - {file = "Twisted-20.3.0.tar.bz2", hash = "sha256:d72c55b5d56e176563b91d11952d13b01af8725c623e498db5507b6614fc1e10"}, -] -txaio = [ - {file = "txaio-20.12.1-py2.py3-none-any.whl", hash = "sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"}, - {file = "txaio-20.12.1.tar.gz", hash = "sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, -] -urllib3 = [ - {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, - {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, -] -uvicorn = [ - {file = "uvicorn-0.12.3-py3-none-any.whl", hash = "sha256:562ef6aaa8fa723ab6b82cf9e67a774088179d0ec57cb17e447b15d58b603bcf"}, - {file = "uvicorn-0.12.3.tar.gz", hash = "sha256:5836edaf4d278fe67ba0298c0537bdb6398cf359eb644f79e6500ca1aad232b3"}, -] -uvloop = [ - {file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"}, - {file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"}, - {file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"}, - {file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"}, - {file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"}, - {file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"}, - {file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"}, - {file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"}, - {file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"}, -] -watchgod = [ - {file = "watchgod-0.6-py35.py36.py37-none-any.whl", hash = "sha256:59700dab7445aa8e6067a5b94f37bae90fc367554549b1ed2e9d0f4f38a90d2a"}, - {file = "watchgod-0.6.tar.gz", hash = "sha256:e9cca0ab9c63f17fc85df9fd8bd18156ff00aff04ebe5976cee473f4968c6858"}, -] -websockets = [ - {file = "websockets-8.0.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:e906128532a14b9d264a43eb48f9b3080d53a9bda819ab45bf56b8039dc606ac"}, - {file = "websockets-8.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:83e63aa73331b9ca21af61df8f115fb5fbcba3f281bee650a4ad16a40cd1ef15"}, - {file = "websockets-8.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e9102043a81cdc8b7c8032ff4bce39f6229e4ac39cb2010946c912eeb84e2cb6"}, - {file = "websockets-8.0.2-cp36-cp36m-win32.whl", hash = "sha256:8d7a20a2f97f1e98c765651d9fb9437201a9ccc2c70e94b0270f1c5ef29667a3"}, - {file = "websockets-8.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c82e286555f839846ef4f0fdd6910769a577952e1e26aa8ee7a6f45f040e3c2b"}, - {file = "websockets-8.0.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:73ce69217e4655783ec72ce11c151053fcbd5b837cc39de7999e19605182e28a"}, - {file = "websockets-8.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8c77f7d182a6ea2a9d09c2612059f3ad859a90243e899617137ee3f6b7f2b584"}, - {file = "websockets-8.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a7affaeffbc5d55681934c16bb6b8fc82bb75b175e7fd4dcca798c938bde8dda"}, - {file = "websockets-8.0.2-cp37-cp37m-win32.whl", hash = "sha256:f5cb2683367e32da6a256b60929a3af9c29c212b5091cf5bace9358d03011bf5"}, - {file = "websockets-8.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:049e694abe33f8a1d99969fee7bfc0ae6761f7fd5f297c58ea933b27dd6805f2"}, - {file = "websockets-8.0.2.tar.gz", hash = "sha256:882a7266fa867a2ebb2c0baaa0f9159cabf131cf18c1b4270d79ad42f9208dc5"}, -] -whitenoise = [ - {file = "whitenoise-5.2.0-py2.py3-none-any.whl", hash = "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"}, - {file = "whitenoise-5.2.0.tar.gz", hash = "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7"}, -] -zipp = [ - {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, - {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, -] -"zope.interface" = [ - {file = "zope.interface-5.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b"}, - {file = "zope.interface-5.2.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386"}, - {file = "zope.interface-5.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec"}, - {file = "zope.interface-5.2.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a"}, - {file = "zope.interface-5.2.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2"}, - {file = "zope.interface-5.2.0-cp27-cp27m-win32.whl", hash = "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523"}, - {file = "zope.interface-5.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b"}, - {file = "zope.interface-5.2.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e"}, - {file = "zope.interface-5.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d"}, - {file = "zope.interface-5.2.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96"}, - {file = "zope.interface-5.2.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc"}, - {file = "zope.interface-5.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3"}, - {file = "zope.interface-5.2.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7"}, - {file = "zope.interface-5.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332"}, - {file = "zope.interface-5.2.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5"}, - {file = "zope.interface-5.2.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c"}, - {file = "zope.interface-5.2.0-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86"}, - {file = "zope.interface-5.2.0-cp35-cp35m-win32.whl", hash = "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d"}, - {file = "zope.interface-5.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b"}, - {file = "zope.interface-5.2.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549"}, - {file = "zope.interface-5.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11"}, - {file = "zope.interface-5.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546"}, - {file = "zope.interface-5.2.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c"}, - {file = "zope.interface-5.2.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50"}, - {file = "zope.interface-5.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20"}, - {file = "zope.interface-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e"}, - {file = "zope.interface-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4"}, - {file = "zope.interface-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7"}, - {file = "zope.interface-5.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102"}, - {file = "zope.interface-5.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7"}, - {file = "zope.interface-5.2.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a"}, - {file = "zope.interface-5.2.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae"}, - {file = "zope.interface-5.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3"}, - {file = "zope.interface-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520"}, - {file = "zope.interface-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45"}, - {file = "zope.interface-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3"}, - {file = "zope.interface-5.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034"}, - {file = "zope.interface-5.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232"}, - {file = "zope.interface-5.2.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a"}, - {file = "zope.interface-5.2.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104"}, - {file = "zope.interface-5.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd"}, - {file = "zope.interface-5.2.0-cp38-cp38-win32.whl", hash = "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a"}, - {file = "zope.interface-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123"}, - {file = "zope.interface-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb"}, - {file = "zope.interface-5.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d"}, - {file = "zope.interface-5.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65"}, - {file = "zope.interface-5.2.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"}, - {file = "zope.interface-5.2.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc"}, - {file = "zope.interface-5.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1"}, - {file = "zope.interface-5.2.0-cp39-cp39-win32.whl", hash = "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00"}, - {file = "zope.interface-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095"}, - {file = "zope.interface-5.2.0.tar.gz", hash = "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24"}, -] diff --git a/pyproject.toml b/pyproject.toml index df60c81..e06dacb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,7 @@ -[tool.poetry] -name = "django-reactor" -version = "2.2.1b" -description = "Brings LiveView from Phoenix framework into Django" -authors = ["Eddy Ernesto del Valle Pino "] -license = "MIT" -readme = "README.md" -repository = "https://github.com/edelvalle/reactor" -keywords = ["django", "python", "javascript", "fullstack", "liveview"] -packages = [{include = "reactor"}] -include = ["reactor/static/reactor/reactor.js"] +[tool.isort] +profile = "black" +line_length = 80 -[tool.poetry.dependencies] -python = ">=3.6,<4" -channels = ">=3.0.0,<3.1" -orjson = ">=3,<4" -pydantic = ">=1.5" - -[tool.poetry.dev-dependencies] -flake8 = "^3.8.4" -pytest-django = "^4.0.0" -pytest-asyncio = "^0.14.0" -whitenoise = "^5.2.0" -uvicorn = {extras = ["standard"], version = "^0.12.2"} -splinter = {extras = ["django"], version = "^0.14.0"} - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.black] +line-length = 80 +target-version = ['py36'] diff --git a/reactor/channels.py b/reactor/channels.py deleted file mode 100644 index 1ce391a..0000000 --- a/reactor/channels.py +++ /dev/null @@ -1,125 +0,0 @@ -import logging - -from asgiref.sync import async_to_sync -from channels.generic.websocket import JsonWebsocketConsumer -from django.core.signing import Signer - -from . import json -from .component import RootComponent, Component -from .utils import extract_data - - -log = logging.getLogger('reactor') - - -class ReactorConsumer(JsonWebsocketConsumer): - channel_name = '' - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.signer = Signer() - self.subscriptions = set() - - # Group operations - - def subscribe(self, event): - room_name = event['room_name'] - if room_name not in self.subscriptions: - log.debug(f':: SUBSCRIBE {self.channel_name} {room_name}') - async_to_sync(self.channel_layer.group_add)( - room_name, - self.channel_name - ) - self.subscriptions.add(room_name) - - def unsubscribe(self, event): - room_name = event['room_name'] - if room_name in self.subscriptions: - log.debug(f':: UNSUBSCRIBE {self.channel_name} {room_name}') - async_to_sync(self.channel_layer.group_discard)( - room_name, - self.channel_name - ) - self.subscriptions.discard(room_name) - - # Channel events - - def connect(self): - super().connect() - self.scope['channel_name'] = self.channel_name - self.root_component = RootComponent(request=self.scope) - self.send_json({ - 'type': 'components', - 'component_types': { - name: c.extends for name, c in Component._all.items() - } - }) - log.debug(f':: CONNECT {self.channel_name}') - - def disconnect(self, close_code): - for room in list(self.subscriptions): - self.unsubscribe({'room_name': room}) - log.debug(f':: DISCONNECT {self.channel_name}') - - # Dispatching - - def receive_json(self, request): - name = request['command'] - payload = request['payload'] - getattr(self, f'receive_{name}')(**payload) - - def receive_join(self, tag_name, id, state): - state = json.loads(self.signer.unsign(state)) - log.debug(f'>>> JOIN {tag_name} {state}') - component = self.root_component.get_or_create(tag_name, id=id, **state) - html_diff = component._render_diff() - self.render({'id': component.id, 'html_diff': html_diff}) - - def receive_user_event(self, id, name, implicit_args, explicit_args): - explicit_args = explicit_args or {} - kwargs = dict(extract_data(implicit_args), **explicit_args) - log.debug(f'>>> USER_EVENT {name} {kwargs}') - html_diff = self.root_component.dispatch_user_event(id, name, kwargs) - self.render({'id': id, 'html_diff': html_diff}) - - def receive_leave(self, id): - self.root_component.pop(id) - - # Internal event - - def update(self, event): - log.debug(f'>>> UPDATE {event}') - for render_event in self.root_component.propagate_update(event): - self.render(render_event) - - def send_component(self, event): - log.debug(f'>>> DISPATCH {event}') - self.receive_user_event( - event['state']['id'], - event['name'], - event['state'] - ) - - # Broadcasters - - def render(self, event): - if event['html_diff']: - log.debug(f"<<< RENDER {event['id']}") - self.send_json(dict(event, type='render')) - - def remove(self, event): - log.debug(f"<<< REMOVE {event['id']}") - self.receive_leave(event['id']) - self.send_json(dict(event, type='remove')) - - def visit(self, event): - log.debug(f"<<< VISIT {event['action']} {event['url']}") - self.send_json(event) - - @classmethod - def decode_json(cls, text_data): - return json.loads(text_data) - - @classmethod - def encode_json(cls, content): - return json.dumps(content) diff --git a/reactor/component.py b/reactor/component.py index 7e8fd4a..1c0f818 100644 --- a/reactor/component.py +++ b/reactor/component.py @@ -1,239 +1,91 @@ import difflib -from uuid import uuid4 from functools import reduce +from uuid import uuid4 -from django.http import HttpRequest from django.contrib.auth.models import AnonymousUser +from django.db import models from django.shortcuts import resolve_url -from django.template.loader import get_template, select_template +from django.template import loader +from django.utils.html import format_html from django.utils.safestring import mark_safe -from django.utils.functional import cached_property +from pydantic import BaseModel, validate_arguments +from pydantic.fields import Field try: from hmin.base import html_minify except ImportError: + def html_minify(html): return html -from . import settings -from . import json -from .utils import send_to_channel, get_model - - -class RootComponent(dict): - - def __init__(self, request): - super().__init__() - self._request = request - - @cached_property - def _channel_name(self): - return self._request.get('channel_name') - - def get_or_create(self, _name, _parent_id=None, id=None, **state): - id = str(id or '') - kwargs = dict( - request=self._request, - id=id, - _root_component=self, - _parent_id=_parent_id, - **state - ) - component: Component = self.get(id) - if component: - component.__init__(**kwargs) - else: - component = Component._build(_name, **kwargs) - self[component.id] = component - return component - - def pop(self, id, default=None): - return super().pop(id, default) - def dispatch_user_event(self, id, name, args): - component: Component = self.get(id) - if component: - return component._dispatch(name, args) - else: - send_to_channel(self._channel_name, 'remove', id=id) - - def propagate_update(self, event): - origin = event['origin'] - for component in list(self.values()): - if origin in component._subscriptions: - component = component._clone() - self[component.id] = component - html_diff = component._render_diff() - yield {'id': component.id, 'html_diff': html_diff} - - -class Component: - template_name = '' - template = None - extends = 'div' - _all = {} - - def __init_subclass__(cls, name=None, public=True): - if public: - name = name or cls.__name__ - name = ''.join([('-' + c if c.isupper() else c) for c in name]) - name = name.strip('-').lower() - cls._all[name] = cls - cls._tag_name = name - - cls._models = {} - for attr_name in dir(cls): - attr = getattr(cls, attr_name) - if (not attr_name.startswith('_') - and attr_name.islower() - and callable(attr) - and not getattr(attr, 'private', False)): - cls._models[attr_name] = get_model(attr, ignore=['self']) - - cls._constructor_model = get_model(cls) - cls._constructor_params = set( - cls._constructor_model.schema()['properties'] - ) - return super().__init_subclass__() +from . import settings - @classmethod - def _build( - cls, - _tag_name, - request, - id: str = None, - _parent_id=None, - _root_component=None, - **kwargs - ): - klass = cls._all[_tag_name] - if not _parent_id: - kwargs = dict(klass._constructor_model.parse_obj(kwargs)) - return klass( - request=request, - _parent_id=_parent_id, - _root_component=_root_component, - id=id, - **kwargs - ) - def __init__( - self, - request, - id: str = None, - _parent_id: str = None, - _root_component: RootComponent = None, - _last_sent_html: list = None, - **kwargs - ): - self.request = request - self.id = id or uuid4().hex - self._subscriptions = set() - self._parent_id = _parent_id - self._destroy_sent = False +class ReactorMeta: + def __init__(self, user=None, channel_name=None, parent_id=None): + self.user = user or AnonymousUser() + self.channel_name = channel_name + self.parent_id = parent_id + self._destroyed = False self._is_frozen = False self._redirected_to = None - self._last_sent_html = _last_sent_html or [] - if _root_component is None: - _root_component = RootComponent(request) - self._root_component = _root_component - - @cached_property - def user(self): - return ( - getattr(self.request, 'user', None) or - self.request.get('user') or - AnonymousUser() - ) + self._last_sent_html = [] + self._template = None + self._subscriptions = set() + self._messages_to_send: list[tuple(str, str, dict)] = [] - def _clone(self): - return type(self)( - request=self.request, - _parent_id=self._parent_id, - _root_component=self._root_component, - _last_sent_html=self._last_sent_html, - **self._state - ) + def destroy(self, component_id: str): + if not self._destroyed: + self._destroyed = True + self.send("remove", id=component_id) - @property - def _state_json(self): - if self._parent_id: - return json.dumps({'id': self.id}) - else: - return self._constructor_model(**self._state).json() + def freeze(self): + self._is_frozen = True - @property - def _state(self): - state = { - name: value - for name, value in vars(self).items() - if name in self._constructor_params - } - return state | {'id': self.id} - - def _subscribe(self, *room_names): - for room_name in room_names: - if room_name not in self._subscriptions: - self._subscriptions.add(room_name) - send_to_channel( - self._channel_name, - 'subscribe', - room_name=room_name - ) + def redirect_to(self, to, **kwargs): + self._redirect(to, kwargs) - def _unsubscribe(self, room_name): - self._subscriptions.discard(room_name) + def replace_to(self, to, **kwargs): + self._redirect(to, kwargs, replace=True) - @cached_property - def _channel_name(self): - if isinstance(self.request, dict): - return self.request.get('channel_name') + def push_to(self, to, **kwargs): + self._push(to, kwargs) - def _dispatch(self, name, args=None): - model = self._models[name] - getattr(self, name)(**dict(model.parse_obj(args or {}))) - return self._render_diff() + def _redirect(self, to, kwargs, replace: bool = False): + url = resolve_url(to, **kwargs) + self._redirected_to = url + if self.channel_name: + self.freeze() + self.send("redirect_to", url=url, replace=replace) - # State persistence & front-end communication + def _push(self, to, kwargs): + url = resolve_url(to, **kwargs) + self._redirected_to = url + if self.channel_name: + self.freeze() + self.send("push_page", url=url) - def freeze(self): - self._is_frozen = True + def subscribe(self, *channels): + self._subscriptions.update(channels) + for channel in channels: + self.send("subscribe", channel=channel) - def destroy(self): - self._destroy_sent = True - send_to_channel(self._channel_name, 'remove', id=self.id) - - def visit(self, url: str, action: str = 'advance', **kwargs): - url = resolve_url(url, **kwargs) - if self._channel_name: - send_to_channel(self._channel_name, 'visit', url=url, action=action) - elif action == 'advance': - self._redirected_to = url - - def _send_parent(self, _name, **kwargs): - if self._parent_id: - self._send(_name, id=self._parent_id, **kwargs) - - def _send(self, _name, id=None, **kwargs): - send_to_channel( - self._channel_name, - 'send_component', - name=_name, - state=dict(kwargs, id=id or self.id), - ) + def unsubscribe(self, *channels): + self._subscriptions = self._subscriptions - set(channels) - def _render_diff(self): - html = self._render().split() - if html and self._last_sent_html != html: + def render_diff(self, component, repository): + html = self.render(component, repository) + if html and self._last_sent_html != (html := html.split()): if settings.USE_HTML_DIFF: diff = [] for x in difflib.ndiff(self._last_sent_html, html): indicator = x[0] - if indicator == ' ': + if indicator == " ": diff.append(1) - elif indicator == '+': + elif indicator == "+": diff.append(x[2:]) - elif indicator == '-': + elif indicator == "-": diff.append(-1) if diff: @@ -243,66 +95,51 @@ def _render_diff(self): self._last_sent_html = html return diff - def _render(self): - if self._is_frozen: - html = self._last_sent_html - elif self._destroy_sent: - html = '' - elif self._redirected_to: - html = ( - f'' + def render(self, component, repo): + html = None + if not self.channel_name and self._redirected_to: + html = format_html( + '', + url=self._redirected_to, ) - else: - if isinstance(self.request, HttpRequest): - request = self.request - else: - request = None - template = self._get_template() - html = template.render(self._get_context(), request=request).strip() + elif not (self._is_frozen or self._redirected_to): + template = self._get_template(component._template_name) + context = self._get_context(component, repo) + html = template.render(context).strip() - if settings.USE_HMIN: + if html and settings.USE_HMIN: html = html_minify(html) - return mark_safe(html) + if html: + return mark_safe(html) - def _get_template(self): - if not self.template: - if isinstance(self.template_name, (list, tuple)): - self.template = select_template(self.template_name) + def _get_template(self, template_name): + if not self._template: + if isinstance(template_name, (list, tuple)): + self._template = loader.select_template(template_name) else: - self.template = get_template(self.template_name) - return self.template + self._template = loader.get_template(template_name) + return self._template + + def send(self, _topic, **kwargs): + if self.channel_name: + self.send_to(self.channel_name, _topic, **kwargs) - def _get_context(self): + def send_to(self, _channel, _topic, **kwargs): + self._messages_to_send.append((_channel, _topic, kwargs)) + + def _get_context(self, component, repo): return dict( { - attr: getattr(self, attr) - for attr in dir(self) - if not attr.startswith('_') + attr: getattr(component, attr) + for attr in dir(component) + if not attr.startswith("_") }, - this=self, + this=component, + reactor_repository=repo, ) -class AuthComponent(Component, public=False): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.user.is_authenticated: - self.destroy() - - -class StaffComponent(AuthComponent, public=False): - def mount(self, *args, **kwargs): - if super().mount() and self.user.is_staff: - return True - else: - self.destroy() - - def compress_diff(diff, diff_item): if isinstance(diff_item, str) or isinstance(diff[-1], str): diff.append(diff_item) @@ -313,3 +150,102 @@ def compress_diff(diff, diff_item): else: diff.append(diff_item) return diff + + +class Component(BaseModel): + _all = {} + _urls = {} + _name = ... + _extends = "div" + + class Config: + arbitrary_types_allowed = True + json_encoders = { + models.Model: lambda x: x.pk, + models.QuerySet: lambda qs: list(qs.values_list("pk", flat=True)), + } + + def __init_subclass__(cls, name=None, public=True, template_name=None): + if public: + name = name or cls.__name__ + cls._all[name] = cls + cls._name = name + + name = "".join([("-" + c if c.isupper() else c) for c in name]) + name = name.strip("-").lower() + cls._tag_name = "x-" + name + + if template_name is not None: + cls._template_name = template_name + + for attr_name in vars(cls): + attr = getattr(cls, attr_name) + if ( + not attr_name.startswith("_") + and attr_name.islower() + and callable(attr) + ): + setattr( + cls, + attr_name, + validate_arguments( + config={"arbitrary_types_allowed": True} + )(attr), + ) + + return super().__init_subclass__() + + @classmethod + def _build( + cls, + _component_name, + state, + user=None, + channel_name=None, + parent_id=None, + ): + if _component_name not in cls._all: + raise ComponentNotFound( + f"Could not find requested component '{_component_name}'. " + f"Did you load the component?" + ) + instance = cls._all[_component_name]( + reactor=ReactorMeta( + user=user, + channel_name=channel_name, + parent_id=parent_id, + ), + **state, + ) + instance.mounted() + return instance + + # instance + id: str = Field(default_factory=lambda: f"rx-{uuid4()}") + reactor: ReactorMeta + + def mounted(self): + ... + + def mutation(self, channel): + ... + + @property + def user(self): + return self.reactor.user + + def destroy(self): + self.reactor.destroy(self.id) + + def render(self, repo): + return self.reactor.render(self, repo) + + def render_diff(self, repo): + return self.reactor.render_diff(self, repo) + + def focus_on(self, selector): + return self.reactor.send("focus_on", selector=selector) + + +class ComponentNotFound(LookupError): + pass diff --git a/reactor/consumer.py b/reactor/consumer.py new file mode 100644 index 0000000..bcca6a9 --- /dev/null +++ b/reactor/consumer.py @@ -0,0 +1,156 @@ +import json +import logging +from re import T + +import channels +from channels.db import database_sync_to_async as db +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django.contrib.auth.models import AnonymousUser +from django.core.signing import Signer +from django.http.response import HttpResponse +from django.test import Client +from django.utils.datastructures import MultiValueDict +from django.utils.http import url_has_allowed_host_and_scheme + +from .repository import ComponentRepository +from .utils import parse_request_data + +log = logging.getLogger("reactor") + + +class ReactorConsumer(AsyncJsonWebsocketConsumer): + @property + def user(self): + return self.scope.get("user") or AnonymousUser() + + async def connect(self): + await super().connect() + self.subscriptions = set() + self.repo = ComponentRepository( + user=self.user, + channel_name=self.channel_name, + ) + + # Fronted commands + + async def receive_json(self, content): + await getattr(self, f'command_{content["command"]}')( + **content["payload"] + ) + + async def command_join(self, name, state): + state = json.loads(Signer().unsign(state)) + log.debug(f"<<< JOIN {name} {state}") + component = await db(self.repo.build)(name, state) + await self.send_render(component) + await self.send_pending_messages() + + async def command_leave(self, id): + log.debug(f"<<< LEAVE {id}") + self.repo.remove(id) + + async def command_user_event( + self, id, command, implicit_args, explicit_args + ): + kwargs = dict( + parse_request_data(MultiValueDict(implicit_args)), **explicit_args + ) + log.debug(f"<<< USER-EVENT {id} {command} {kwargs}") + component = await db(self.repo.dispatch_event)(id, command, kwargs) + await self.send_render(component) + await self.send_pending_messages() + + # Componet commands + + async def message_from_component(self, data): + await getattr(self, f"component_{data['command']}")(**data["kwargs"]) + + async def component_subscribe(self, channel): + if channel not in self.subscriptions: + log.debug(f"::: SUBSCRIBE {self.channel_name} to {channel}") + self.subscriptions.add(channel) + await self.channel_layer.group_add(channel, self.channel_name) + + async def component_unsubscribe(self, channel): + if channels in self.subscriptions: + log.debug(f"::: UNSUBSCRIBE {self.channel_name} to {channel}") + self.subscriptions.discard(channel) + await self.channel_layer.group_discard(channel, self.channel_name) + + async def component_remove(self, id): + log.debug(f">>> REMOVE {id}") + await self.send_command("remove", {"id": id}) + + async def component_focus_on(self, selector): + log.debug(f'>>> FOCUS ON "{selector}"') + await self.send_command("focus_on", {"selector": selector}) + + async def component_redirect_to(self, url: str, replace: bool = False): + action = "REPLACE WITH" if replace else "REDIRECT TO" + log.debug(f'>>> {action} "{url}"') + await self.send_command("visit", {"url": url, "replace": replace}) + + async def component_push_page(self, url: str): + try: + log.debug(f'>>> PUSH PAGE "{url}"') + client = Client() + if not isinstance(self.user, AnonymousUser): + await db(client.force_login)(self.user) + response: HttpResponse = await db(client.get)( + url, + follow=True, + ) + if response.redirect_chain: + page_url, _status = response.redirect_chain[-1] + else: + page_url = url + await self.send_command( + "page", + { + "url": page_url, + "content": response.content.decode().strip(), + }, + ) + except Exception as e: + log.debug(f'>>> PUSH FAILED "{e}"') + await self.component_redirect_to(url) + + # Model mutation + + async def model_mutation(self, data): + channel = data["origin"] + no_interest_on_this_channel = True + for component in self.repo.components_subscribed_to(channel): + no_interest_on_this_channel = False + await db(component.mutation)(channel) + await self.send_render(component) + + if no_interest_on_this_channel: + await self.component_unsubscribe(channel) + else: + await self.send_pending_messages() + + # Reply to frontned + + async def send_render(self, component): + diff = await db(component.render_diff)(self.repo) + if diff is not None: + log.debug(f">>> RENDER {component._name} {component.id}") + await self.send_command( + "render", + {"id": component.id, "diff": diff}, + ) + + async def send_command(self, command, payload): + await self.send_json({"command": command, "payload": payload}) + + async def send_pending_messages(self): + for channel, command, kwargs in self.repo.messages_to_send: + await self.channel_layer.send( + channel, + { + "type": "message_from_component", + "command": command, + "kwargs": kwargs, + }, + ) diff --git a/reactor/event_transpiler.py b/reactor/event_transpiler.py new file mode 100644 index 0000000..9d4b38c --- /dev/null +++ b/reactor/event_transpiler.py @@ -0,0 +1,134 @@ +import json + +from lru import LRU as LRUDict + +from . import settings + + +def transpile(event_and_modifiers: str, command: str, kwargs: dict): + """Translates from from the tag `on` in to JavaScript""" + name, *modifiers = event_and_modifiers.split(".") + cache_key = f"{modifiers}.{command}.{kwargs}" + code = CODE_CACHE.get(cache_key) + if code is None: + modifiers.append("_reactor_code") + code = command + stack = [kwargs] + while modifiers: + modifier = modifiers.pop() + handler = getattr(Modifiers, modifier, None) + if handler: + code = handler(code, stack) + else: + stack.append(modifier) + + CODE_CACHE[cache_key] = code + return "on" + name, code + + +CODE_CACHE = LRUDict(settings.TRANSPILER_CACHE_SIZE) + + +class Modifiers: + @staticmethod + def _reactor_code(code, stack): + kwargs = json.dumps(stack.pop()) + return f"reactor.send(event.target, '{code}', {kwargs})" + + @staticmethod + def _add_curly(code, stack=None): + return "{" + code + "}" + + # Events + + @classmethod + def debounce(cls, code, stack): + name = stack.pop() + delay = stack.pop() + code = cls._add_curly(code) + return f"reactor.debounce('{name}', {delay})(() => {code})()" + + @staticmethod + def prevent(code, stack): + return "event.preventDefault(); " + code + + @staticmethod + def stop(code, stack): + return "event.stopPropagation(); " + code + + # Key modifiers + + @classmethod + def ctrl(cls, code, stack): + code = cls.add_curly(code) + return f"if (event.ctrlKey) {code}" + + @classmethod + def alt(cls, code, stack): + code = cls.add_curly(code) + return f"if (event.altKey) {code}" + + @classmethod + def shift(cls, code, stack): + code = cls.add_curly(code) + return f"if (event.shiftKey) {code}" + + @classmethod + def meta(cls, code, stack): + code = cls.add_curly(code) + return f"if (event.metaKey) {code}" + + # Key codes + + @classmethod + def key(cls, code, stack): + key = stack.pop() + code = cls._add_curly(code) + return f"if ((event.key + '').toLowerCase() == '{key}') {code}" + + @classmethod + def key_code(cls, code, stack): + keyCode = stack.pop() + code = cls._add_curly(code) + return f"if (event.keyCode == {keyCode}) {code}" + + # Key shortcuts + @classmethod + def enter(cls, code, stack): + return cls.key(code, ["enter"]) + + @classmethod + def tab(cls, code, stack): + return cls.key(code, ["tab"]) + + @classmethod + def delete(cls, code, stack): + return cls.key(code, ["delete"]) + + @classmethod + def backspace(cls, code, stack): + return cls.key(code, ["backspace"]) + + @classmethod + def esc(cls, code, stack): + return cls.key(code, ["escape"]) + + @classmethod + def space(cls, code, stack): + return cls.key(code, [" "]) + + @classmethod + def up(cls, code, stack): + return cls.key(code, ["arrowup"]) + + @classmethod + def down(cls, code, stack): + return cls.key(code, ["arrowdown"]) + + @classmethod + def left(cls, code, stack): + return cls.key(code, ["arrowleft"]) + + @classmethod + def right(cls, code, stack): + return cls.key(code, ["arrowright"]) diff --git a/reactor/json.py b/reactor/json.py deleted file mode 100644 index 8e9e74a..0000000 --- a/reactor/json.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Generator -import orjson - -try: - from pydantic import BaseModel -except ImportError: - BaseModel = None - -from django.core.serializers.json import DjangoJSONEncoder -from django.db import models - - -def loads(text_data): - return orjson.loads(text_data) - - -def default(o): - if isinstance(o, models.Model): - return o.pk - - if isinstance(o, models.QuerySet): - return list(o.values_list('pk', flat=True)) - - if isinstance(o, (Generator, set)): - return list(o) - - if BaseModel and isinstance(o, BaseModel): - return o.dict() - - if hasattr(o, '__json__'): - return o.__json__() - - return DjangoJSONEncoder().default(o) - - -def dumps(obj, indent=False, default=default): - option = ( - orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY - ) - if indent: - option |= orjson.OPT_INDENT_2 - return orjson.dumps(obj, default=default, option=option).decode() - - -class Encoder: - def __init__(self, *args, **kwargs): - pass - - def default(self, obj): - return default(obj) - - def encode(self, obj): - return dumps(obj, default=self.default) diff --git a/reactor/repository.py b/reactor/repository.py new file mode 100644 index 0000000..c4752b6 --- /dev/null +++ b/reactor/repository.py @@ -0,0 +1,49 @@ +from django.contrib.auth.models import AnonymousUser + +from .component import Component +from .utils import filter_parameters + + +class ComponentRepository: + def __init__(self, user=None, channel_name=None): + self.channel_name = channel_name + self.user = user or AnonymousUser() + self.components: dict[str, Component] = {} + + def build(self, name, state, parent_id=None) -> Component: + if (id := state.get("id")) and (component := self.components.get(id)): + component = component.copy(update=state) + else: + component = Component._build( + name, + state, + user=self.user, + channel_name=self.channel_name, + parent_id=parent_id, + ) + self.components[component.id] = component + return component + + def remove(self, id): + self.components.pop(id, None) + + def dispatch_event(self, id, command, kwargs): + assert not command.startswith("_") + component = self.components[id] + handler = getattr(component, command) + handler(**filter_parameters(handler, kwargs)) + return component + + def components_subscribed_to(self, channel): + # XXX: There is a list() here because the dict can change size during + # iteration + for component in list(self.components.values()): + if channel in component.reactor._subscriptions: + yield component + + @property + def messages_to_send(self): + for component in self.components.values(): + for message in component.reactor._messages_to_send: + yield message + component.reactor._messages_to_send = [] diff --git a/reactor/settings.py b/reactor/settings.py index 64aeef8..3080679 100644 --- a/reactor/settings.py +++ b/reactor/settings.py @@ -2,14 +2,14 @@ def get(name, default=None): - return getattr(settings, f'REACTOR_{name}', default) + return getattr(settings, f"REACTOR_{name}", default) LOGIN_URL = settings.LOGIN_URL -INCLUDE_TURBOLINKS = get('INCLUDE_TURBOLINKS', False) -USE_HTML_DIFF = get('USE_HTML_DIFF', True) -USE_HMIN = get('USE_HMIN', True) -AUTO_BROADCAST = get('AUTO_BROADCAST', False) +USE_HTML_DIFF = get("USE_HTML_DIFF", True) +USE_HMIN = get("USE_HMIN", True) +AUTO_BROADCAST = get("AUTO_BROADCAST", False) +TRANSPILER_CACHE_SIZE = get("TRANSPILER_CACHE_SIZE", 1024) if isinstance(AUTO_BROADCAST, bool): @@ -17,17 +17,14 @@ def get(name, default=None): # model_a # model_a.del # model_a.new - 'MODEL': AUTO_BROADCAST, - + "MODEL": AUTO_BROADCAST, # model_a.1234 - 'MODEL_PK': AUTO_BROADCAST, - + "MODEL_PK": AUTO_BROADCAST, # model_b.1234.model_a_set # model_b.1234.model_a_set.new # model_b.1234.model_a_set.del - 'RELATED': AUTO_BROADCAST, - + "RELATED": AUTO_BROADCAST, # model_b.1234.model_a_set # model_a.1234.model_b_set - 'M2M': AUTO_BROADCAST, + "M2M": AUTO_BROADCAST, } diff --git a/reactor/static/reactor/morphdom.min.js b/reactor/static/reactor/morphdom.min.js deleted file mode 100644 index bbec3fa..0000000 --- a/reactor/static/reactor/morphdom.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory():typeof define==="function"&&define.amd?define(factory):(global=global||self,global.morphdom=factory())})(this,function(){"use strict";var DOCUMENT_FRAGMENT_NODE=11;function morphAttrs(fromNode,toNode){var toNodeAttrs=toNode.attributes;var attr;var attrName;var attrNamespaceURI;var attrValue;var fromValue;if(toNode.nodeType===DOCUMENT_FRAGMENT_NODE||fromNode.nodeType===DOCUMENT_FRAGMENT_NODE){return}for(var i=toNodeAttrs.length-1;i>=0;i--){attr=toNodeAttrs[i];attrName=attr.name;attrNamespaceURI=attr.namespaceURI;attrValue=attr.value;if(attrNamespaceURI){attrName=attr.localName||attrName;fromValue=fromNode.getAttributeNS(attrNamespaceURI,attrName);if(fromValue!==attrValue){if(attr.prefix==="xmlns"){attrName=attr.name}fromNode.setAttributeNS(attrNamespaceURI,attrName,attrValue)}}else{fromValue=fromNode.getAttribute(attrName);if(fromValue!==attrValue){fromNode.setAttribute(attrName,attrValue)}}}var fromNodeAttrs=fromNode.attributes;for(var d=fromNodeAttrs.length-1;d>=0;d--){attr=fromNodeAttrs[d];attrName=attr.name;attrNamespaceURI=attr.namespaceURI;if(attrNamespaceURI){attrName=attr.localName||attrName;if(!toNode.hasAttributeNS(attrNamespaceURI,attrName)){fromNode.removeAttributeNS(attrNamespaceURI,attrName)}}else{if(!toNode.hasAttribute(attrName)){fromNode.removeAttribute(attrName)}}}}var range;var NS_XHTML="http://www.w3.org/1999/xhtml";var doc=typeof document==="undefined"?undefined:document;var HAS_TEMPLATE_SUPPORT=!!doc&&"content"in doc.createElement("template");var HAS_RANGE_SUPPORT=!!doc&&doc.createRange&&"createContextualFragment"in doc.createRange();function createFragmentFromTemplate(str){var template=doc.createElement("template");template.innerHTML=str;return template.content.childNodes[0]}function createFragmentFromRange(str){if(!range){range=doc.createRange();range.selectNode(doc.body)}var fragment=range.createContextualFragment(str);return fragment.childNodes[0]}function createFragmentFromWrap(str){var fragment=doc.createElement("body");fragment.innerHTML=str;return fragment.childNodes[0]}function toElement(str){str=str.trim();if(HAS_TEMPLATE_SUPPORT){return createFragmentFromTemplate(str)}else if(HAS_RANGE_SUPPORT){return createFragmentFromRange(str)}return createFragmentFromWrap(str)}function compareNodeNames(fromEl,toEl){var fromNodeName=fromEl.nodeName;var toNodeName=toEl.nodeName;var fromCodeStart,toCodeStart;if(fromNodeName===toNodeName){return true}fromCodeStart=fromNodeName.charCodeAt(0);toCodeStart=toNodeName.charCodeAt(0);if(fromCodeStart<=90&&toCodeStart>=97){return fromNodeName===toNodeName.toUpperCase()}else if(toCodeStart<=90&&fromCodeStart>=97){return toNodeName===fromNodeName.toUpperCase()}else{return false}}function createElementNS(name,namespaceURI){return!namespaceURI||namespaceURI===NS_XHTML?doc.createElement(name):doc.createElementNS(namespaceURI,name)}function moveChildren(fromEl,toEl){var curChild=fromEl.firstChild;while(curChild){var nextChild=curChild.nextSibling;toEl.appendChild(curChild);curChild=nextChild}return toEl}function syncBooleanAttrProp(fromEl,toEl,name){if(fromEl[name]!==toEl[name]){fromEl[name]=toEl[name];if(fromEl[name]){fromEl.setAttribute(name,"")}else{fromEl.removeAttribute(name)}}}var specialElHandlers={OPTION:function(fromEl,toEl){var parentNode=fromEl.parentNode;if(parentNode){var parentName=parentNode.nodeName.toUpperCase();if(parentName==="OPTGROUP"){parentNode=parentNode.parentNode;parentName=parentNode&&parentNode.nodeName.toUpperCase()}if(parentName==="SELECT"&&!parentNode.hasAttribute("multiple")){if(fromEl.hasAttribute("selected")&&!toEl.selected){fromEl.setAttribute("selected","selected");fromEl.removeAttribute("selected")}parentNode.selectedIndex=-1}}syncBooleanAttrProp(fromEl,toEl,"selected")},INPUT:function(fromEl,toEl){syncBooleanAttrProp(fromEl,toEl,"checked");syncBooleanAttrProp(fromEl,toEl,"disabled");if(fromEl.value!==toEl.value){fromEl.value=toEl.value}if(!toEl.hasAttribute("value")){fromEl.removeAttribute("value")}},TEXTAREA:function(fromEl,toEl){var newValue=toEl.value;if(fromEl.value!==newValue){fromEl.value=newValue}var firstChild=fromEl.firstChild;if(firstChild){var oldValue=firstChild.nodeValue;if(oldValue==newValue||!newValue&&oldValue==fromEl.placeholder){return}firstChild.nodeValue=newValue}},SELECT:function(fromEl,toEl){if(!toEl.hasAttribute("multiple")){var selectedIndex=-1;var i=0;var curChild=fromEl.firstChild;var optgroup;var nodeName;while(curChild){nodeName=curChild.nodeName&&curChild.nodeName.toUpperCase();if(nodeName==="OPTGROUP"){optgroup=curChild;curChild=optgroup.firstChild}else{if(nodeName==="OPTION"){if(curChild.hasAttribute("selected")){selectedIndex=i;break}i++}curChild=curChild.nextSibling;if(!curChild&&optgroup){curChild=optgroup.nextSibling;optgroup=null}}}fromEl.selectedIndex=selectedIndex}}};var ELEMENT_NODE=1;var DOCUMENT_FRAGMENT_NODE$1=11;var TEXT_NODE=3;var COMMENT_NODE=8;function noop(){}function defaultGetNodeKey(node){if(node){return node.getAttribute&&node.getAttribute("id")||node.id}}function morphdomFactory(morphAttrs){return function morphdom(fromNode,toNode,options){if(!options){options={}}if(typeof toNode==="string"){if(fromNode.nodeName==="#document"||fromNode.nodeName==="HTML"||fromNode.nodeName==="BODY"){var toNodeHtml=toNode;toNode=doc.createElement("html");toNode.innerHTML=toNodeHtml}else{toNode=toElement(toNode)}}var getNodeKey=options.getNodeKey||defaultGetNodeKey;var onBeforeNodeAdded=options.onBeforeNodeAdded||noop;var onNodeAdded=options.onNodeAdded||noop;var onBeforeElUpdated=options.onBeforeElUpdated||noop;var onElUpdated=options.onElUpdated||noop;var onBeforeNodeDiscarded=options.onBeforeNodeDiscarded||noop;var onNodeDiscarded=options.onNodeDiscarded||noop;var onBeforeElChildrenUpdated=options.onBeforeElChildrenUpdated||noop;var childrenOnly=options.childrenOnly===true;var fromNodesLookup=Object.create(null);var keyedRemovalList=[];function addKeyedRemoval(key){keyedRemovalList.push(key)}function walkDiscardedChildNodes(node,skipKeyedNodes){if(node.nodeType===ELEMENT_NODE){var curChild=node.firstChild;while(curChild){var key=undefined;if(skipKeyedNodes&&(key=getNodeKey(curChild))){addKeyedRemoval(key)}else{onNodeDiscarded(curChild);if(curChild.firstChild){walkDiscardedChildNodes(curChild,skipKeyedNodes)}}curChild=curChild.nextSibling}}}function removeNode(node,parentNode,skipKeyedNodes){if(onBeforeNodeDiscarded(node)===false){return}if(parentNode){parentNode.removeChild(node)}onNodeDiscarded(node);walkDiscardedChildNodes(node,skipKeyedNodes)}function indexTree(node){if(node.nodeType===ELEMENT_NODE||node.nodeType===DOCUMENT_FRAGMENT_NODE$1){var curChild=node.firstChild;while(curChild){var key=getNodeKey(curChild);if(key){fromNodesLookup[key]=curChild}indexTree(curChild);curChild=curChild.nextSibling}}}indexTree(fromNode);function handleNodeAdded(el){onNodeAdded(el);var curChild=el.firstChild;while(curChild){var nextSibling=curChild.nextSibling;var key=getNodeKey(curChild);if(key){var unmatchedFromEl=fromNodesLookup[key];if(unmatchedFromEl&&compareNodeNames(curChild,unmatchedFromEl)){curChild.parentNode.replaceChild(unmatchedFromEl,curChild);morphEl(unmatchedFromEl,curChild)}else{handleNodeAdded(curChild)}}else{handleNodeAdded(curChild)}curChild=nextSibling}}function cleanupFromEl(fromEl,curFromNodeChild,curFromNodeKey){while(curFromNodeChild){var fromNextSibling=curFromNodeChild.nextSibling;if(curFromNodeKey=getNodeKey(curFromNodeChild)){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=fromNextSibling}}function morphEl(fromEl,toEl,childrenOnly){var toElKey=getNodeKey(toEl);if(toElKey){delete fromNodesLookup[toElKey]}if(!childrenOnly){if(onBeforeElUpdated(fromEl,toEl)===false){return}morphAttrs(fromEl,toEl);onElUpdated(fromEl);if(onBeforeElChildrenUpdated(fromEl,toEl)===false){return}}if(fromEl.nodeName!=="TEXTAREA"){morphChildren(fromEl,toEl)}else{specialElHandlers.TEXTAREA(fromEl,toEl)}}function morphChildren(fromEl,toEl){var curToNodeChild=toEl.firstChild;var curFromNodeChild=fromEl.firstChild;var curToNodeKey;var curFromNodeKey;var fromNextSibling;var toNextSibling;var matchingFromEl;outer:while(curToNodeChild){toNextSibling=curToNodeChild.nextSibling;curToNodeKey=getNodeKey(curToNodeChild);while(curFromNodeChild){fromNextSibling=curFromNodeChild.nextSibling;if(curToNodeChild.isSameNode&&curToNodeChild.isSameNode(curFromNodeChild)){curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling;continue outer}curFromNodeKey=getNodeKey(curFromNodeChild);var curFromNodeType=curFromNodeChild.nodeType;var isCompatible=undefined;if(curFromNodeType===curToNodeChild.nodeType){if(curFromNodeType===ELEMENT_NODE){if(curToNodeKey){if(curToNodeKey!==curFromNodeKey){if(matchingFromEl=fromNodesLookup[curToNodeKey]){if(fromNextSibling===matchingFromEl){isCompatible=false}else{fromEl.insertBefore(matchingFromEl,curFromNodeChild);if(curFromNodeKey){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=matchingFromEl}}else{isCompatible=false}}}else if(curFromNodeKey){isCompatible=false}isCompatible=isCompatible!==false&&compareNodeNames(curFromNodeChild,curToNodeChild);if(isCompatible){morphEl(curFromNodeChild,curToNodeChild)}}else if(curFromNodeType===TEXT_NODE||curFromNodeType==COMMENT_NODE){isCompatible=true;if(curFromNodeChild.nodeValue!==curToNodeChild.nodeValue){curFromNodeChild.nodeValue=curToNodeChild.nodeValue}}}if(isCompatible){curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling;continue outer}if(curFromNodeKey){addKeyedRemoval(curFromNodeKey)}else{removeNode(curFromNodeChild,fromEl,true)}curFromNodeChild=fromNextSibling}if(curToNodeKey&&(matchingFromEl=fromNodesLookup[curToNodeKey])&&compareNodeNames(matchingFromEl,curToNodeChild)){fromEl.appendChild(matchingFromEl);morphEl(matchingFromEl,curToNodeChild)}else{var onBeforeNodeAddedResult=onBeforeNodeAdded(curToNodeChild);if(onBeforeNodeAddedResult!==false){if(onBeforeNodeAddedResult){curToNodeChild=onBeforeNodeAddedResult}if(curToNodeChild.actualize){curToNodeChild=curToNodeChild.actualize(fromEl.ownerDocument||doc)}fromEl.appendChild(curToNodeChild);handleNodeAdded(curToNodeChild)}}curToNodeChild=toNextSibling;curFromNodeChild=fromNextSibling}cleanupFromEl(fromEl,curFromNodeChild,curFromNodeKey);var specialElHandler=specialElHandlers[fromEl.nodeName];if(specialElHandler){specialElHandler(fromEl,toEl)}}var morphedNode=fromNode;var morphedNodeType=morphedNode.nodeType;var toNodeType=toNode.nodeType;if(!childrenOnly){if(morphedNodeType===ELEMENT_NODE){if(toNodeType===ELEMENT_NODE){if(!compareNodeNames(fromNode,toNode)){onNodeDiscarded(fromNode);morphedNode=moveChildren(fromNode,createElementNS(toNode.nodeName,toNode.namespaceURI))}}else{morphedNode=toNode}}else if(morphedNodeType===TEXT_NODE||morphedNodeType===COMMENT_NODE){if(toNodeType===morphedNodeType){if(morphedNode.nodeValue!==toNode.nodeValue){morphedNode.nodeValue=toNode.nodeValue}return morphedNode}else{morphedNode=toNode}}}if(morphedNode===toNode){onNodeDiscarded(fromNode)}else{if(toNode.isSameNode&&toNode.isSameNode(morphedNode)){return}morphEl(morphedNode,toNode,childrenOnly);if(keyedRemovalList){for(var i=0,len=keyedRemovalList.length;i { + if (options?.beforeReplace) options.beforeReplace(html) + document.body = html.querySelector("body") + boostAllLinks() + if (options?.afterReplace) options.afterReplace(html) + }) +} + +function hasSameOriginAsDocument(url) { + if (url.startsWith("http://") || url.startsWith("https://")) { + return (new URL(url)).origin === document.location.origin + } else { + return true + } +} + + +class HistoryCache { + /** @type {Array<{url: String, content: String, title: String, scroll: Number}>} */ + static maxSize = 10 + static cache = [] + static currentPath = window.location.pathname + window.location.search + + static async load(url) { + this._saveCurrentPage() + console.log(url) + if (hasSameOriginAsDocument(url)) { + this.push(url) + await this.restoreFromCurrentPath() + } else { + location.assign(url) + } + } + + static loadContent(url, content) { + console.log("Load content", url) + replaceBodyContent(content, { + beforeReplace(html) { + HistoryCache._saveCurrentPage() + HistoryCache.push(url) + document.title = html.querySelector("title")?.text ?? "" + } + }) + } + + static async restoreFromCurrentPath() { + this._saveCurrentPage() + let path = window.location.pathname + window.location.search + console.log("Restoring Page:", path) + let page = this._get(path) + if (page) { + replaceBodyContent(page.content, { + beforeReplace() { + document.title = page.title + }, + afterReplace() { + window.scrollTo(0, page.scroll) + } + }) + console.log("currentPath", path) + this.currentPath = path + } + + // I don't care if the page is restored or network + // This will pull the page from the server just in case of any change + + let response = await fetch(window.location.href) + let content = await response.text() + this.replace(response.url) + replaceBodyContent(content, { + beforeReplace(html) { + document.title = html.querySelector("title")?.text ?? "" + }, + afterReplace() { + HistoryCache._saveCurrentPage() + } + }) + } + + static back() { + window.history.back() + } + + static push(path) { + history.pushState({}, document.title, path) + this.currentPath = path + } + + static replace(path) { + history.replaceState({}, document.title, path) + this.currentPath = path + } + + static _saveCurrentPage() { + console.log("Saving page:", this.currentPath) + + // Remove path from the history cache if is already existed + this.cache = this.cache.filter(({ url }) => url != this.currentPath) + + // Put the new entry in the cache + this.cache.push({ + url: this.currentPath, + content: document.body.outerHTML, + title: document.title, + scroll: window.scrollY, + }) + + // Keep the history at a reasonable size + if (this.cache.length > this.maxSize) { + let page = this.cache.shift() + console.log("Evicted:", page.url) + } + + console.log("Currently cached:", this.cache.map(({ url }) => url)) + } + + static _get(path) { + return this.cache.find((page) => page.url == path) + } +} + + +window.addEventListener("popstate", (event) => { + HistoryCache.restoreFromCurrentPath() +}) + +window.addEventListener("load", () => { + boostAllLinks() +}) + +export default { + boostAllLinks, + boostElement, + HistoryCache: HistoryCache, +} diff --git a/reactor/static/reactor/reactor.css b/reactor/static/reactor/reactor.css deleted file mode 100644 index 2e49d47..0000000 --- a/reactor/static/reactor/reactor.css +++ /dev/null @@ -1,9 +0,0 @@ -.reactor-disconnected { - opacity: 0.8; - cursor: wait; -} - -.reactor-disconnected * { - opacity: 0.8; - pointer-events: none; -} diff --git a/reactor/static/reactor/reactor.js b/reactor/static/reactor/reactor.js index 79beeb8..2c42072 100644 --- a/reactor/static/reactor/reactor.js +++ b/reactor/static/reactor/reactor.js @@ -1,491 +1,284 @@ -// Generated by CoffeeScript 2.5.1 -(function() { - var ReactorChannel, TRANSPILER_CACHE, _timeouts, declare_components, origin, reactor, reactor_channel, transpile; - - origin = new Date(); - - ReactorChannel = class ReactorChannel { - constructor(url1 = '/__reactor__', retry_interval = 100) { - this.on = this.on.bind(this); - this.url = url1; - this.retry_interval = retry_interval; - this.online = false; - this.callbacks = {}; - this.original_retry_interval = this.retry_interval; - } +import morphdom from 'morphdom' +import ReconnectingWebSocket from 'reconnecting-websocket' +import boost from './reactor-boost' - on(event_name, callback) { - return this.callbacks[event_name] = callback; - } - trigger(event_name, ...args) { - var base; - return typeof (base = this.callbacks)[event_name] === "function" ? base[event_name](...args) : void 0; - } +window.morphdom = morphdom - open() { - var protocol, ref; - if (this.retry_interval < 10000) { - this.retry_interval += 1000; - } - if (navigator.onLine) { - if ((ref = this.websocket) != null) { - ref.close(); - } - if (window.location.protocol === 'https:') { - protocol = 'wss://'; - } else { - protocol = 'ws://'; - } - this.websocket = new WebSocket(`${protocol}${window.location.host}${this.url}`); - this.websocket.onopen = (event) => { - this.online = true; - this.trigger('open', event); - return this.retry_interval = this.original_retry_interval; - }; - this.websocket.onclose = (event) => { - this.online = false; - this.trigger('close', event); - return setTimeout((() => { - return this.open(); - }), this.retry_interval || 0); - }; - return this.websocket.onmessage = (event) => { - var data; - data = JSON.parse(event.data); - return this.trigger('message', data); - }; - } else { - return setTimeout((() => { - return this.open(); - }), this.retry_interval); - } + +// Connection + +class ServerConnection extends EventTarget { + open(path = '__reactor__') { + let protocol = location.protocol.replace("http", "ws") + this.socket = new ReconnectingWebSocket( + `${protocol}//${location.host}/${path}`, [], + { + maxEnqueuedMessages: 0, + } + ) + + this.socket.addEventListener( + "open", + () => { + console.log("WS: OPEN") + this.dispatchEvent(new Event("open")) + } + ) + + this.socket.addEventListener( + "message", + (event) => this._processMessage(event) + ) + + this.socket.addEventListener( + "close", + () => { + console.log("WS: CLOSE") + this.dispatchEvent(new Event("close")) + } + ) } - send(command, payload) { - var data; - data = { - command: command, - payload: payload - }; - if (this.online) { - return this.websocket.send(JSON.stringify(data)); - } + get isOpen() { + return this.socket?.readyState == ReconnectingWebSocket.OPEN } - send_join(tag_name, id, state) { - console.log('>>> JOIN', tag_name, state); - return this.send('join', { - tag_name: tag_name, - id: id, - state: state - }); + _processMessage(event) { + let { command, payload } = JSON.parse(event.data) + switch (command) { + case "render": + var { id, diff } = payload + console.log("<<< RENDER", id) + document.getElementById(id)?.applyDiff(diff) + break + case "remove": + var { id } = payload + console.log("<<< REMOVE", id) + document.getElementById(id)?.remove() + break + case "focus_on": + var { selector } = payload + console.log('<<< FOCUS-ON', `"${selector}"`) + document.querySelector(selector)?.focus() + break + case "visit": + var { url, replace } = payload + if (replace) { + console.log("<< REPLACE", url) + boost.HistoryCache.replace(url) + } else { + console.log("<< VISIT", url) + boost.HistoryCache.load(url) + } + break + case "page": + var { url, content } = payload + console.log("<< PAGE", `"${url}"`) + boost.HistoryCache.loadContent(url, content) + break + case "back": + boost.HistoryCache.back() + break + default: + console.error(`Unknown command "${command}"`, payload) + } + } - send_leave(id) { - console.log('>>> LEAVE', id); - return this.send('leave', { - id: id - }); + sendJoin(name, state) { + console.log(">>> JOIN", name) + this._send("join", { name, state }) } - send_user_event(element, name, implicit_args, explicit_args) { - console.log('>>> USER_EVENT', element.tag_name, name, implicit_args, explicit_args); - origin = new Date(); - if (this.online) { - return this.send('user_event', { - id: element.id, - name: name, - implicit_args: implicit_args, - explicit_args: explicit_args - }); - } + sendLeave(id) { + console.log(">>> LEAVE", id) + this._send("leave", { id }) } - reconnect() { - var ref; - this.retry_interval = 0; - return (ref = this.websocket) != null ? ref.close() : void 0; + sendUserEvent(id, command, implicit_args, explicit_args) { + console.log(">>> USER_EVENT", id, command, explicit_args) + this._send("user_event", { id, command, implicit_args, explicit_args }) } - close() { - var ref; - console.log('CLOSE'); - return (ref = this.websocket) != null ? ref.close() : void 0; + _send(command, payload) { + if (this.isOpen) { + this.socket.send(JSON.stringify({ command, payload })) + } } +} - }; +let connection = new ServerConnection() - reactor_channel = new ReactorChannel(); +for ({ dataset } of document.querySelectorAll("meta[name=reactor-component]")) { - reactor_channel.on('open', function() { - var el, i, len, ref, results; - console.log('ON-LINE'); - ref = document.querySelectorAll('[is]'); - results = []; - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - el.classList.remove('reactor-disconnected'); - results.push(typeof el.connect === "function" ? el.connect() : void 0); - } - return results; - }); - - reactor_channel.on('close', function() { - var el, i, len, ref, results; - console.log('OFF-LINE'); - ref = document.querySelectorAll('[is]'); - results = []; - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - results.push(el.classList.add('reactor-disconnected')); - } - return results; - }); - - reactor_channel.on('message', function({type, id, html_diff, url, action, component_types}) { - var ref; - console.log('<<<', type.toUpperCase(), id || url || component_types); - switch (type) { - case 'components': - return declare_components(component_types); - case 'visit': - return reactor.visit(url, { - action: action - }); - case 'render': - return (ref = document.getElementById(id)) != null ? typeof ref.apply_diff === "function" ? ref.apply_diff(html_diff) : void 0 : void 0; - case 'remove': - return window.requestAnimationFrame(function() { - var ref1; - return (ref1 = document.getElementById(id)) != null ? ref1.remove() : void 0; - }); - } - }); + let baseElement = document.createElement(dataset.extends) - TRANSPILER_CACHE = {}; + class ReactorComponent extends baseElement.constructor { - transpile = function(el) { - var _delay, _name, attr, cache_key, code, i, j, len, len1, method_args, method_name, modifier, modifiers, name, nu_attr, old_name, ref, replacements, results, start; - if (el.attributes === void 0) { - return; - } - replacements = []; - ref = el.attributes; - for (i = 0, len = ref.length; i < len; i++) { - attr = ref[i]; - if (attr.name.startsWith('@')) { - [name, ...modifiers] = attr.name.split('.'); - start = attr.value.indexOf(' '); - if (start !== -1) { - method_name = attr.value.slice(0, start); - method_args = attr.value.slice(start + 1); - } else { - method_name = attr.value; - method_args = 'null'; - } - cache_key = `${modifiers}.${method_name}.${method_args}`; - code = TRANSPILER_CACHE[cache_key]; - if (!code) { - if (method_name === '') { - code = ''; - } else { - code = `reactor.send(event.target, '${method_name}', ${method_args});`; - } - while (modifiers.length) { - modifier = modifiers.pop(); - modifier = modifier === 'space' ? ' ' : modifier; - switch (modifier) { - case 'inlinejs': - code = attr.value; - break; - case 'debounce': - _name = modifiers.pop(); - _delay = modifiers.pop(); - code = `reactor.debounce('${_name}', ${_delay})(function(){ ${code} })()`; - break; - case 'prevent': - code = "event.preventDefault(); " + code; - break; - case 'stop': - code = "event.stopPropagation(); " + code; - break; - case 'ctrl': - code = `if (event.ctrlKey) { ${code} }`; - break; - case 'alt': - code = `if (event.altKey) { ${code} }`; - break; - default: - code = `if (event.key.toLowerCase() == '${modifier}') { ${code} }; `; - } - } - TRANSPILER_CACHE[cache_key] = code; - } - replacements.push({ - old_name: attr.name, - name: 'on' + name.slice(1), - code: code - }); - } - } - results = []; - for (j = 0, len1 = replacements.length; j < len1; j++) { - ({old_name, name, code} = replacements[j]); - if (old_name) { - el.attributes.removeNamedItem(old_name); - } - nu_attr = document.createAttribute(name); - nu_attr.value = code; - results.push(el.attributes.setNamedItem(nu_attr)); - } - return results; - }; - - declare_components = function(component_types) { - var Component, base_element, base_html_element, component_name, results; - results = []; - for (component_name in component_types) { - base_html_element = component_types[component_name]; - if (customElements.get(component_name)) { - continue; - } - base_element = document.createElement(base_html_element); - Component = class Component extends base_element.constructor { constructor(...args) { - super(...args); - this.tag_name = this.getAttribute('is'); - this._last_received_html = []; - } - - state() { - return this.getAttribute('state'); + super(...args) + this._lastReceivedHtml = [] + this.joinBind = () => this.join() + this.wentOffline = () => this.classList.add("reactor-disconnected") } connectedCallback() { - eval(this.getAttribute('onreactor-init')); - this.deep_transpile(); - return this.connect(); + connection.addEventListener("open", this.joinBind) + connection.addEventListener("close", this.wentOffline) + this.join() } disconnectedCallback() { - eval(this.getAttribute('onreactor-leave')); - return reactor_channel.send_leave(this.id); + connection.removeEventListener("open", this.joinBind) + connection.removeEventListener("close", this.wentOffline) + connection.sendLeave(this.id) } - deep_transpile(element = null) { - var child, code, i, len, ref, results1; - if (element == null) { - transpile(this); - element = this; - } - ref = element.children; - results1 = []; - for (i = 0, len = ref.length; i < len; i++) { - child = ref[i]; - transpile(child); - code = child.getAttribute('onreactor-init'); - if (code) { - (function() { - return eval(code); - }).bind(child)(); + join() { + this.classList.remove("reactor-disconnected") + if (this.isRoot) { + connection.sendJoin(this.dataset.name, this.dataset.state) } - results1.push(this.deep_transpile(child)); - } - return results1; } - is_root() { - return !this.parent_component(); + applyDiff(diff) { + let html = this.getHtml(diff) + window.requestAnimationFrame(() => { + morphdom(this, html, { + onBeforeNodeAdded(node) { + boost.boostElement(node) + }, + onElUpdated(node) { + boost.boostElement(node) + }, + }) + }) } - parent_component() { - var ref; - return (ref = this.parentElement) != null ? ref.closest('[is]') : void 0; + getHtml(diff) { + let fragments = [] + let cursor = 0 + for (let fragment of diff) { + if (typeof fragment === "string") { + fragments.push(fragment) + } else if (fragment < 0) { + cursor -= fragment + } else { + fragments.push(...this._lastReceivedHtml.slice(cursor, cursor + fragment)) + cursor += fragment + } + } + this._lastReceivedHtml = fragments + return fragments.join(" ") } - connect() { - if (this.is_root()) { - return reactor_channel.send_join(this.tag_name, this.id, this.state()); - } + /** + * Returns true when this is a high level component and has no parent + * component + * + * @returns {boolean} + */ + get isRoot() { + return !this.parentComponent } - apply_diff(html_diff) { - var cursor, diff, html, i, len; - console.log(`${new Date() - origin}ms`); - html = []; - cursor = 0; - for (i = 0, len = html_diff.length; i < len; i++) { - diff = html_diff[i]; - if (typeof diff === 'string') { - html.push(diff); - } else if (diff < 0) { - cursor -= diff; - } else { - html.push(...this._last_received_html.slice(cursor, cursor + diff)); - cursor += diff; - } - } - this._last_received_html = html; - html = html.join(' '); - return window.requestAnimationFrame(() => { - var ref; - morphdom(this, html, { - onBeforeElUpdated: function(from_el, to_el) { - var ref, should_patch; - // Prevent object from being updated - if (from_el.hasAttribute(':once')) { - return false; - } - if (from_el.hasAttribute(':keep')) { - to_el.value = from_el.value; - to_el.checked = from_el.checked; - } - transpile(to_el); - should_patch = from_el === document.activeElement && ((ref = from_el.tagName) === 'INPUT' || ref === 'SELECT' || ref === 'TEXTAREA') && !from_el.hasAttribute(':override'); - if (should_patch) { - to_el.getAttributeNames().forEach(function(name) { - return from_el.setAttribute(name, to_el.getAttribute(name)); - }); - from_el.readOnly = to_el.readOnly; - return false; - } - return true; - }, - onElUpdated: function(el) { - var code; - code = typeof el.getAttribute === "function" ? el.getAttribute('onreactor-updated') : void 0; - if (code) { - return (function() { - return eval(code); - }).bind(el)(); - } - }, - onNodeAdded: function(el) { - var code; - transpile(el); - code = typeof el.getAttribute === "function" ? el.getAttribute('onreactor-added') : void 0; - if (code) { - return (function() { - return eval(code); - }).bind(el)(); - } - } - }); - return (ref = this.querySelector('[\\:focus]:not([disabled])')) != null ? ref.focus() : void 0; - }); + /** + * Returns the high parent reactor component if any is found component + * + * @returns {?ReactorComponent} + */ + get parentComponent() { + return this.parentElement?.closest("[reactor-component]") } - dispatch(name, form, args) { - return reactor_channel.send_user_event(this, name, this.serialize(form || this), args); + /** + * Dispatches a command to this component and sends it to the backend + * @param {String} command + * @param {Object} args + * @param {?HTMLFormElement} form + */ + dispatch(command, args, formScope) { + connection.sendUserEvent( + this.id, command, this.serialize(formScope), args + ) } - serialize(form) { - var el, el_type, i, len, option, ref, results1, value; - ref = form.querySelectorAll('[name]'); - results1 = []; - for (i = 0, len = ref.length; i < len; i++) { - el = ref[i]; - if (el.closest('[is]') !== this) { - continue; - } - el_type = el.type.toLowerCase(); - value = ((function() { - var j, len1, ref1, results2; - switch (el.type.toLowerCase()) { - case 'checkbox': - if (el.checked) { - return el.value || true; - } else { - return null; - } - break; - case 'radio': - if (el.checked) { - return el.value || true; - } else { - return null; - } - break; - case 'select-multiple': - ref1 = el.selectedOptions; - results2 = []; - for (j = 0, len1 = ref1.length; j < len1; j++) { - option = ref1[j]; - results2.push(option.value); - } - return results2; - case el.hasAttribute('contenteditable'): - if (el.hasAttribute(':as-text')) { - return el.innerText; - } else { - return el.innerHTML.trim(); - } - break; - default: - return el.value; - } - })()); - if (value === null) { - continue; + /** + * Serialize all elements inside `element` with a [name] attribute into + * a an array of `[element[name], element[value]]` + * @param {HTMLElement} element + */ + serialize(element) { + let result = {} + for (let el of element.querySelectorAll("[name]")) { + // Avoid serializing data of a nested component + if (el.closest("[reactor-component]") !== this) { + continue + } + + let value = null + switch (el.type.toLowerCase()) { + case "checkbox": + case "radio": + value = el.checked ? (el.value || true) : null + break + case "select-multiple": + value = el.selectedOptions.map(option => option.value) + break + default: + value = el.value + break + } + + if (value !== null) { + let key = el.getAttribute("name") + let values = result[key] ?? [] + values.push(value) + result[key] = values + } } - results1.push([el.getAttribute('name'), value]); - } - return results1; + return result } - - }; - results.push(customElements.define(component_name, Component, { - extends: base_html_element - })); } - return results; - }; - - window.reactor = reactor = {}; - - reactor.send = function(element, name, args) { - var component, form; - component = element.closest('[is]'); - form = element.closest('form'); - if (component != null) { - form = component.contains(form) ? form : null; - return component.dispatch(name, form, args); - } - }; - - _timeouts = {}; - - reactor.debounce = function(delay_name, delay) { - return function(f) { - return function(...args) { - clearTimeout(_timeouts[delay_name]); - return _timeouts[delay_name] = setTimeout((() => { - return f(...args); - }), delay); - }; - }; - }; - - reactor.visit = function(url, options) { - try { - switch (options.action) { - case 'replace': - return window.history.replaceState({}, document.title, url); - case 'advance': - if (typeof Turbo !== "undefined" && Turbo !== null) { - return Turbo.visit(url, options); - } else { - return window.history.pushState({}, document.title, url); - } - } - } catch (error) { - return window.location.assign(url); - } - }; - reactor_channel.open(); + customElements.define(dataset.tagName, ReactorComponent, { extends: dataset.extends }) + +} -}).call(this); -//# sourceMappingURL=reactor.js.map +connection.open() +debounceTimeouts = {} + +window.reactor = { + /** + * Forwards a user event to a component + * @param {HTMLElement} element + * @param {String} name + * @param {Object} args + */ + send(element, name, args) { + let component = element.closest("[reactor-component]") + if (component !== null) { + let form = element.closest("form") + let formScope = component.contains(form) ? form : component + component.dispatch(name, args, formScope) + } + + }, + + /** + * Debounce a function call + * @param {String} delayName + * @param {Number} delay + * @returns + */ + debounce(delayName, delay) { + return (f) => { + return (...args) => { + clearTimeout(debounceTimeouts[delayName]) + debounceTimeouts[delayName] = setTimeout(() => f(...args), delay) + } + } + } +} diff --git a/reactor/static/reactor/reactor.js.map b/reactor/static/reactor/reactor.js.map deleted file mode 100644 index ff65daf..0000000 --- a/reactor/static/reactor/reactor.js.map +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 3, - "file": "reactor.js", - "sourceRoot": "../../../../../src/reactor/reactor/static/reactor", - "sources": [ - "../../../../../kaiko/kaiko/reactor/static/reactor/reactor.coffee" - ], - "names": [], - "mappings": "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", - "sourcesContent": [ - "origin = new Date()\n\n\nclass ReactorChannel\n constructor: (@url='/__reactor__', @retry_interval=100) ->\n @online = false\n @callbacks = {}\n @original_retry_interval = @retry_interval\n\n on: (event_name, callback) =>\n @callbacks[event_name] = callback\n\n trigger: (event_name, args ...) ->\n @callbacks[event_name]?(args...)\n\n open: ->\n if @retry_interval < 10000\n @retry_interval += 1000\n\n if navigator.onLine\n @websocket?.close()\n\n if window.location.protocol is 'https:'\n protocol = 'wss://'\n else\n protocol = 'ws://'\n @websocket = new WebSocket \"#{protocol}#{window.location.host}#{@url}\"\n @websocket.onopen = (event) =>\n @online = true\n @trigger 'open', event\n @retry_interval = @original_retry_interval\n\n @websocket.onclose = (event) =>\n @online = false\n @trigger 'close', event\n setTimeout (=> @open()), @retry_interval or 0\n\n @websocket.onmessage = (event) =>\n data = JSON.parse event.data\n @trigger 'message', data\n else\n setTimeout (=> @open()), @retry_interval\n\n send: (command, payload) ->\n data =\n command: command\n payload: payload\n if @online\n @websocket.send JSON.stringify data\n\n send_join: (tag_name, id, state) ->\n console.log '>>> JOIN', tag_name, state\n @send 'join',\n tag_name: tag_name\n id: id\n state: state\n\n send_leave: (id) ->\n console.log '>>> LEAVE', id\n @send 'leave', id: id\n\n send_user_event: (element, name, implicit_args, explicit_args) ->\n console.log(\n '>>> USER_EVENT', element.tag_name, name, implicit_args, explicit_args\n )\n origin = new Date()\n if @online\n @send 'user_event',\n id: element.id\n name: name\n implicit_args: implicit_args\n explicit_args: explicit_args\n\n reconnect: ->\n @retry_interval = 0\n @websocket?.close()\n\n close: ->\n console.log 'CLOSE'\n @websocket?.close()\n\n\n\nreactor_channel = new ReactorChannel()\n\n\nreactor_channel.on 'open', ->\n console.log 'ON-LINE'\n for el in document.querySelectorAll '[is]'\n el.classList.remove('reactor-disconnected')\n el.connect?()\n\nreactor_channel.on 'close', ->\n console.log 'OFF-LINE'\n for el in document.querySelectorAll '[is]'\n el.classList.add('reactor-disconnected')\n\n\nreactor_channel.on 'message', ({type, id, html_diff, url, action, component_types}) ->\n console.log '<<<', type.toUpperCase(), id or url or component_types\n switch type\n when 'components' then declare_components(component_types)\n when 'visit' then reactor.visit url, action: action\n when 'render'\n document.getElementById(id)?.apply_diff?(html_diff)\n when 'remove'\n window.requestAnimationFrame ->\n document.getElementById(id)?.remove()\n\nTRANSPILER_CACHE = {}\n\ntranspile = (el) ->\n if el.attributes is undefined\n return\n\n replacements = []\n for attr in el.attributes\n if attr.name.startsWith('@')\n [name, ...modifiers] = attr.name.split('.')\n start = attr.value.indexOf(' ')\n if start isnt -1\n method_name = attr.value[...start]\n method_args = attr.value[start + 1...]\n else\n method_name = attr.value\n method_args = 'null'\n\n cache_key = \"#{modifiers}.#{method_name}.#{method_args}\"\n code = TRANSPILER_CACHE[cache_key]\n if not code\n if method_name is ''\n code = ''\n else\n code = \"reactor.send(event.target, '#{method_name}', #{method_args});\"\n\n while modifiers.length\n modifier = modifiers.pop()\n modifier = if modifier is 'space' then ' ' else modifier\n switch modifier\n when 'inlinejs'\n code = attr.value\n when 'debounce'\n _name = modifiers.pop()\n _delay = modifiers.pop()\n code = \"reactor.debounce('#{_name}', #{_delay})(function(){ #{code} })()\"\n when 'prevent'\n code = \"event.preventDefault(); \" + code\n when 'stop'\n code = \"event.stopPropagation(); \" + code\n when 'ctrl'\n code = \"if (event.ctrlKey) { #{code} }\"\n when 'alt'\n code = \"if (event.altKey) { #{code} }\"\n else\n code = \"if (event.key.toLowerCase() == '#{modifier}') { #{code} }; \"\n TRANSPILER_CACHE[cache_key] = code\n\n replacements.push {\n old_name: attr.name\n name: 'on' + name[1...]\n code: code\n }\n\n for {old_name, name, code} in replacements\n if old_name\n el.attributes.removeNamedItem old_name\n nu_attr = document.createAttribute name\n nu_attr.value = code\n el.attributes.setNamedItem nu_attr\n\n\ndeclare_components = (component_types) ->\n for component_name, base_html_element of component_types\n if customElements.get(component_name)\n continue\n\n base_element = document.createElement base_html_element\n class Component extends base_element.constructor\n constructor: (...args) ->\n super(...args)\n @tag_name = @getAttribute 'is'\n @_last_received_html = []\n\n state: -> @getAttribute 'state'\n\n connectedCallback: ->\n eval @getAttribute 'onreactor-init'\n @deep_transpile()\n @connect()\n\n disconnectedCallback: ->\n eval @getAttribute 'onreactor-leave'\n reactor_channel.send_leave @id\n\n deep_transpile: (element=null) ->\n if not element?\n transpile this\n element = this\n for child in element.children\n transpile child\n code = child.getAttribute 'onreactor-init'\n if code\n (-> eval code).bind(child)()\n @deep_transpile(child)\n\n is_root: -> not @parent_component()\n\n parent_component: ->\n return @parentElement?.closest('[is]')\n\n connect: ->\n if @is_root()\n reactor_channel.send_join @tag_name, @id, @state()\n\n apply_diff: (html_diff) ->\n console.log \"#{new Date() - origin}ms\"\n html = []\n cursor = 0\n for diff in html_diff\n if typeof diff is 'string'\n html.push diff\n else if diff < 0\n cursor -= diff\n else\n html.push(...@_last_received_html[cursor...cursor + diff])\n cursor += diff\n @_last_received_html = html\n html = html.join ' '\n window.requestAnimationFrame =>\n morphdom this, html,\n onBeforeElUpdated: (from_el, to_el) ->\n # Prevent object from being updated\n if from_el.hasAttribute(':once')\n return false\n\n if from_el.hasAttribute(':keep')\n to_el.value = from_el.value\n to_el.checked = from_el.checked\n\n transpile(to_el)\n\n should_patch = (\n from_el is document.activeElement and\n from_el.tagName in ['INPUT', 'SELECT', 'TEXTAREA'] and\n not from_el.hasAttribute(':override')\n )\n if should_patch\n to_el.getAttributeNames().forEach (name) ->\n from_el.setAttribute(name, to_el.getAttribute(name))\n from_el.readOnly = to_el.readOnly\n return false\n\n return true\n\n onElUpdated: (el) ->\n code = el.getAttribute?('onreactor-updated')\n if code\n (-> eval code).bind(el)()\n\n onNodeAdded: (el) ->\n transpile el\n code = el.getAttribute?('onreactor-added')\n if code\n (-> eval code).bind(el)()\n\n @querySelector('[\\\\:focus]:not([disabled])')?.focus()\n\n dispatch: (name, form, args) ->\n reactor_channel.send_user_event(\n this\n name\n @serialize(form or this)\n args\n )\n\n serialize: (form) ->\n for el in form.querySelectorAll('[name]')\n if el.closest('[is]') isnt this\n continue\n el_type = el.type.toLowerCase()\n value = (\n switch el.type.toLowerCase()\n when 'checkbox'\n if el.checked\n el.value or true\n else\n null\n when 'radio'\n if el.checked\n el.value or true\n else\n null\n when 'select-multiple'\n (option.value for option in el.selectedOptions)\n when el.hasAttribute 'contenteditable'\n if el.hasAttribute ':as-text'\n el.innerText\n else\n el.innerHTML.trim()\n else\n el.value\n )\n if value is null\n continue\n [el.getAttribute('name'), value]\n\n customElements.define(component_name, Component, extends: base_html_element)\n\nwindow.reactor = reactor = {}\n\nreactor.send = (element, name, args) ->\n component = element.closest('[is]')\n form = element.closest('form')\n if component?\n form = if component.contains(form) then form else null\n component.dispatch(name, form, args)\n\n_timeouts = {}\n\nreactor.debounce = (delay_name, delay) -> (f) -> (...args) ->\n clearTimeout _timeouts[delay_name]\n _timeouts[delay_name] = setTimeout (=> f(...args)), delay\n\nreactor.visit = (url, options) ->\n try\n switch options.action\n when 'replace' then window.history.replaceState {}, document.title, url\n when 'advance'\n if Turbo?\n Turbo.visit url, options\n else\n window.history.pushState {}, document.title, url\n catch\n window.location.assign url\n\n\nreactor_channel.open()\n\n" - ] -} \ No newline at end of file diff --git a/reactor/static/reactor/reactor.min.js b/reactor/static/reactor/reactor.min.js new file mode 100644 index 0000000..f85d3eb --- /dev/null +++ b/reactor/static/reactor/reactor.min.js @@ -0,0 +1,1352 @@ +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => { + __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + return value; + }; + + // node_modules/morphdom/dist/morphdom-esm.js + var DOCUMENT_FRAGMENT_NODE = 11; + function morphAttrs(fromNode, toNode) { + var toNodeAttrs = toNode.attributes; + var attr; + var attrName; + var attrNamespaceURI; + var attrValue; + var fromValue; + if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) { + return; + } + for (var i = toNodeAttrs.length - 1; i >= 0; i--) { + attr = toNodeAttrs[i]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + attrValue = attr.value; + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName); + if (fromValue !== attrValue) { + if (attr.prefix === "xmlns") { + attrName = attr.name; + } + fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue); + } + } else { + fromValue = fromNode.getAttribute(attrName); + if (fromValue !== attrValue) { + fromNode.setAttribute(attrName, attrValue); + } + } + } + var fromNodeAttrs = fromNode.attributes; + for (var d = fromNodeAttrs.length - 1; d >= 0; d--) { + attr = fromNodeAttrs[d]; + attrName = attr.name; + attrNamespaceURI = attr.namespaceURI; + if (attrNamespaceURI) { + attrName = attr.localName || attrName; + if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) { + fromNode.removeAttributeNS(attrNamespaceURI, attrName); + } + } else { + if (!toNode.hasAttribute(attrName)) { + fromNode.removeAttribute(attrName); + } + } + } + } + var range; + var NS_XHTML = "http://www.w3.org/1999/xhtml"; + var doc = typeof document === "undefined" ? void 0 : document; + var HAS_TEMPLATE_SUPPORT = !!doc && "content" in doc.createElement("template"); + var HAS_RANGE_SUPPORT = !!doc && doc.createRange && "createContextualFragment" in doc.createRange(); + function createFragmentFromTemplate(str) { + var template = doc.createElement("template"); + template.innerHTML = str; + return template.content.childNodes[0]; + } + function createFragmentFromRange(str) { + if (!range) { + range = doc.createRange(); + range.selectNode(doc.body); + } + var fragment = range.createContextualFragment(str); + return fragment.childNodes[0]; + } + function createFragmentFromWrap(str) { + var fragment = doc.createElement("body"); + fragment.innerHTML = str; + return fragment.childNodes[0]; + } + function toElement(str) { + str = str.trim(); + if (HAS_TEMPLATE_SUPPORT) { + return createFragmentFromTemplate(str); + } else if (HAS_RANGE_SUPPORT) { + return createFragmentFromRange(str); + } + return createFragmentFromWrap(str); + } + function compareNodeNames(fromEl, toEl) { + var fromNodeName = fromEl.nodeName; + var toNodeName = toEl.nodeName; + var fromCodeStart, toCodeStart; + if (fromNodeName === toNodeName) { + return true; + } + fromCodeStart = fromNodeName.charCodeAt(0); + toCodeStart = toNodeName.charCodeAt(0); + if (fromCodeStart <= 90 && toCodeStart >= 97) { + return fromNodeName === toNodeName.toUpperCase(); + } else if (toCodeStart <= 90 && fromCodeStart >= 97) { + return toNodeName === fromNodeName.toUpperCase(); + } else { + return false; + } + } + function createElementNS(name, namespaceURI) { + return !namespaceURI || namespaceURI === NS_XHTML ? doc.createElement(name) : doc.createElementNS(namespaceURI, name); + } + function moveChildren(fromEl, toEl) { + var curChild = fromEl.firstChild; + while (curChild) { + var nextChild = curChild.nextSibling; + toEl.appendChild(curChild); + curChild = nextChild; + } + return toEl; + } + function syncBooleanAttrProp(fromEl, toEl, name) { + if (fromEl[name] !== toEl[name]) { + fromEl[name] = toEl[name]; + if (fromEl[name]) { + fromEl.setAttribute(name, ""); + } else { + fromEl.removeAttribute(name); + } + } + } + var specialElHandlers = { + OPTION: function(fromEl, toEl) { + var parentNode = fromEl.parentNode; + if (parentNode) { + var parentName = parentNode.nodeName.toUpperCase(); + if (parentName === "OPTGROUP") { + parentNode = parentNode.parentNode; + parentName = parentNode && parentNode.nodeName.toUpperCase(); + } + if (parentName === "SELECT" && !parentNode.hasAttribute("multiple")) { + if (fromEl.hasAttribute("selected") && !toEl.selected) { + fromEl.setAttribute("selected", "selected"); + fromEl.removeAttribute("selected"); + } + parentNode.selectedIndex = -1; + } + } + syncBooleanAttrProp(fromEl, toEl, "selected"); + }, + INPUT: function(fromEl, toEl) { + syncBooleanAttrProp(fromEl, toEl, "checked"); + syncBooleanAttrProp(fromEl, toEl, "disabled"); + if (fromEl.value !== toEl.value) { + fromEl.value = toEl.value; + } + if (!toEl.hasAttribute("value")) { + fromEl.removeAttribute("value"); + } + }, + TEXTAREA: function(fromEl, toEl) { + var newValue = toEl.value; + if (fromEl.value !== newValue) { + fromEl.value = newValue; + } + var firstChild = fromEl.firstChild; + if (firstChild) { + var oldValue = firstChild.nodeValue; + if (oldValue == newValue || !newValue && oldValue == fromEl.placeholder) { + return; + } + firstChild.nodeValue = newValue; + } + }, + SELECT: function(fromEl, toEl) { + if (!toEl.hasAttribute("multiple")) { + var selectedIndex = -1; + var i = 0; + var curChild = fromEl.firstChild; + var optgroup; + var nodeName; + while (curChild) { + nodeName = curChild.nodeName && curChild.nodeName.toUpperCase(); + if (nodeName === "OPTGROUP") { + optgroup = curChild; + curChild = optgroup.firstChild; + } else { + if (nodeName === "OPTION") { + if (curChild.hasAttribute("selected")) { + selectedIndex = i; + break; + } + i++; + } + curChild = curChild.nextSibling; + if (!curChild && optgroup) { + curChild = optgroup.nextSibling; + optgroup = null; + } + } + } + fromEl.selectedIndex = selectedIndex; + } + } + }; + var ELEMENT_NODE = 1; + var DOCUMENT_FRAGMENT_NODE$1 = 11; + var TEXT_NODE = 3; + var COMMENT_NODE = 8; + function noop() { + } + function defaultGetNodeKey(node) { + if (node) { + return node.getAttribute && node.getAttribute("id") || node.id; + } + } + function morphdomFactory(morphAttrs2) { + return function morphdom2(fromNode, toNode, options) { + if (!options) { + options = {}; + } + if (typeof toNode === "string") { + if (fromNode.nodeName === "#document" || fromNode.nodeName === "HTML" || fromNode.nodeName === "BODY") { + var toNodeHtml = toNode; + toNode = doc.createElement("html"); + toNode.innerHTML = toNodeHtml; + } else { + toNode = toElement(toNode); + } + } + var getNodeKey = options.getNodeKey || defaultGetNodeKey; + var onBeforeNodeAdded = options.onBeforeNodeAdded || noop; + var onNodeAdded = options.onNodeAdded || noop; + var onBeforeElUpdated = options.onBeforeElUpdated || noop; + var onElUpdated = options.onElUpdated || noop; + var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop; + var onNodeDiscarded = options.onNodeDiscarded || noop; + var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop; + var childrenOnly = options.childrenOnly === true; + var fromNodesLookup = Object.create(null); + var keyedRemovalList = []; + function addKeyedRemoval(key) { + keyedRemovalList.push(key); + } + function walkDiscardedChildNodes(node, skipKeyedNodes) { + if (node.nodeType === ELEMENT_NODE) { + var curChild = node.firstChild; + while (curChild) { + var key = void 0; + if (skipKeyedNodes && (key = getNodeKey(curChild))) { + addKeyedRemoval(key); + } else { + onNodeDiscarded(curChild); + if (curChild.firstChild) { + walkDiscardedChildNodes(curChild, skipKeyedNodes); + } + } + curChild = curChild.nextSibling; + } + } + } + function removeNode(node, parentNode, skipKeyedNodes) { + if (onBeforeNodeDiscarded(node) === false) { + return; + } + if (parentNode) { + parentNode.removeChild(node); + } + onNodeDiscarded(node); + walkDiscardedChildNodes(node, skipKeyedNodes); + } + function indexTree(node) { + if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) { + var curChild = node.firstChild; + while (curChild) { + var key = getNodeKey(curChild); + if (key) { + fromNodesLookup[key] = curChild; + } + indexTree(curChild); + curChild = curChild.nextSibling; + } + } + } + indexTree(fromNode); + function handleNodeAdded(el) { + onNodeAdded(el); + var curChild = el.firstChild; + while (curChild) { + var nextSibling = curChild.nextSibling; + var key = getNodeKey(curChild); + if (key) { + var unmatchedFromEl = fromNodesLookup[key]; + if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) { + curChild.parentNode.replaceChild(unmatchedFromEl, curChild); + morphEl(unmatchedFromEl, curChild); + } else { + handleNodeAdded(curChild); + } + } else { + handleNodeAdded(curChild); + } + curChild = nextSibling; + } + } + function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) { + while (curFromNodeChild) { + var fromNextSibling = curFromNodeChild.nextSibling; + if (curFromNodeKey = getNodeKey(curFromNodeChild)) { + addKeyedRemoval(curFromNodeKey); + } else { + removeNode(curFromNodeChild, fromEl, true); + } + curFromNodeChild = fromNextSibling; + } + } + function morphEl(fromEl, toEl, childrenOnly2) { + var toElKey = getNodeKey(toEl); + if (toElKey) { + delete fromNodesLookup[toElKey]; + } + if (!childrenOnly2) { + if (onBeforeElUpdated(fromEl, toEl) === false) { + return; + } + morphAttrs2(fromEl, toEl); + onElUpdated(fromEl); + if (onBeforeElChildrenUpdated(fromEl, toEl) === false) { + return; + } + } + if (fromEl.nodeName !== "TEXTAREA") { + morphChildren(fromEl, toEl); + } else { + specialElHandlers.TEXTAREA(fromEl, toEl); + } + } + function morphChildren(fromEl, toEl) { + var curToNodeChild = toEl.firstChild; + var curFromNodeChild = fromEl.firstChild; + var curToNodeKey; + var curFromNodeKey; + var fromNextSibling; + var toNextSibling; + var matchingFromEl; + outer: + while (curToNodeChild) { + toNextSibling = curToNodeChild.nextSibling; + curToNodeKey = getNodeKey(curToNodeChild); + while (curFromNodeChild) { + fromNextSibling = curFromNodeChild.nextSibling; + if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) { + curToNodeChild = toNextSibling; + curFromNodeChild = fromNextSibling; + continue outer; + } + curFromNodeKey = getNodeKey(curFromNodeChild); + var curFromNodeType = curFromNodeChild.nodeType; + var isCompatible = void 0; + if (curFromNodeType === curToNodeChild.nodeType) { + if (curFromNodeType === ELEMENT_NODE) { + if (curToNodeKey) { + if (curToNodeKey !== curFromNodeKey) { + if (matchingFromEl = fromNodesLookup[curToNodeKey]) { + if (fromNextSibling === matchingFromEl) { + isCompatible = false; + } else { + fromEl.insertBefore(matchingFromEl, curFromNodeChild); + if (curFromNodeKey) { + addKeyedRemoval(curFromNodeKey); + } else { + removeNode(curFromNodeChild, fromEl, true); + } + curFromNodeChild = matchingFromEl; + } + } else { + isCompatible = false; + } + } + } else if (curFromNodeKey) { + isCompatible = false; + } + isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild); + if (isCompatible) { + morphEl(curFromNodeChild, curToNodeChild); + } + } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) { + isCompatible = true; + if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) { + curFromNodeChild.nodeValue = curToNodeChild.nodeValue; + } + } + } + if (isCompatible) { + curToNodeChild = toNextSibling; + curFromNodeChild = fromNextSibling; + continue outer; + } + if (curFromNodeKey) { + addKeyedRemoval(curFromNodeKey); + } else { + removeNode(curFromNodeChild, fromEl, true); + } + curFromNodeChild = fromNextSibling; + } + if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) { + fromEl.appendChild(matchingFromEl); + morphEl(matchingFromEl, curToNodeChild); + } else { + var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild); + if (onBeforeNodeAddedResult !== false) { + if (onBeforeNodeAddedResult) { + curToNodeChild = onBeforeNodeAddedResult; + } + if (curToNodeChild.actualize) { + curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc); + } + fromEl.appendChild(curToNodeChild); + handleNodeAdded(curToNodeChild); + } + } + curToNodeChild = toNextSibling; + curFromNodeChild = fromNextSibling; + } + cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey); + var specialElHandler = specialElHandlers[fromEl.nodeName]; + if (specialElHandler) { + specialElHandler(fromEl, toEl); + } + } + var morphedNode = fromNode; + var morphedNodeType = morphedNode.nodeType; + var toNodeType = toNode.nodeType; + if (!childrenOnly) { + if (morphedNodeType === ELEMENT_NODE) { + if (toNodeType === ELEMENT_NODE) { + if (!compareNodeNames(fromNode, toNode)) { + onNodeDiscarded(fromNode); + morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI)); + } + } else { + morphedNode = toNode; + } + } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { + if (toNodeType === morphedNodeType) { + if (morphedNode.nodeValue !== toNode.nodeValue) { + morphedNode.nodeValue = toNode.nodeValue; + } + return morphedNode; + } else { + morphedNode = toNode; + } + } + } + if (morphedNode === toNode) { + onNodeDiscarded(fromNode); + } else { + if (toNode.isSameNode && toNode.isSameNode(morphedNode)) { + return; + } + morphEl(morphedNode, toNode, childrenOnly); + if (keyedRemovalList) { + for (var i = 0, len = keyedRemovalList.length; i < len; i++) { + var elToRemove = fromNodesLookup[keyedRemovalList[i]]; + if (elToRemove) { + removeNode(elToRemove, elToRemove.parentNode, false); + } + } + } + } + if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) { + if (morphedNode.actualize) { + morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc); + } + fromNode.parentNode.replaceChild(morphedNode, fromNode); + } + return morphedNode; + }; + } + var morphdom = morphdomFactory(morphAttrs); + var morphdom_esm_default = morphdom; + + // node_modules/reconnecting-websocket/dist/reconnecting-websocket-mjs.js + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) { + d2.__proto__ = b2; + } || function(d2, b2) { + for (var p in b2) + if (b2.hasOwnProperty(p)) + d2[p] = b2[p]; + }; + return extendStatics(d, b); + }; + function __extends(d, b) { + extendStatics(d, b); + function __() { + this.constructor = d; + } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + function __values(o) { + var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0; + if (m) + return m.call(o); + return { + next: function() { + if (o && i >= o.length) + o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + } + function __read(o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) + return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) + ar.push(r.value); + } catch (error) { + e = { error }; + } finally { + try { + if (r && !r.done && (m = i["return"])) + m.call(i); + } finally { + if (e) + throw e.error; + } + } + return ar; + } + function __spread() { + for (var ar = [], i = 0; i < arguments.length; i++) + ar = ar.concat(__read(arguments[i])); + return ar; + } + var Event2 = function() { + function Event3(type, target) { + this.target = target; + this.type = type; + } + return Event3; + }(); + var ErrorEvent = function(_super) { + __extends(ErrorEvent2, _super); + function ErrorEvent2(error, target) { + var _this = _super.call(this, "error", target) || this; + _this.message = error.message; + _this.error = error; + return _this; + } + return ErrorEvent2; + }(Event2); + var CloseEvent = function(_super) { + __extends(CloseEvent2, _super); + function CloseEvent2(code, reason, target) { + if (code === void 0) { + code = 1e3; + } + if (reason === void 0) { + reason = ""; + } + var _this = _super.call(this, "close", target) || this; + _this.wasClean = true; + _this.code = code; + _this.reason = reason; + return _this; + } + return CloseEvent2; + }(Event2); + var getGlobalWebSocket = function() { + if (typeof WebSocket !== "undefined") { + return WebSocket; + } + }; + var isWebSocket = function(w) { + return typeof w !== "undefined" && !!w && w.CLOSING === 2; + }; + var DEFAULT = { + maxReconnectionDelay: 1e4, + minReconnectionDelay: 1e3 + Math.random() * 4e3, + minUptime: 5e3, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 4e3, + maxRetries: Infinity, + maxEnqueuedMessages: Infinity, + startClosed: false, + debug: false + }; + var ReconnectingWebSocket = function() { + function ReconnectingWebSocket2(url, protocols, options) { + var _this = this; + if (options === void 0) { + options = {}; + } + this._listeners = { + error: [], + message: [], + open: [], + close: [] + }; + this._retryCount = -1; + this._shouldReconnect = true; + this._connectLock = false; + this._binaryType = "blob"; + this._closeCalled = false; + this._messageQueue = []; + this.onclose = null; + this.onerror = null; + this.onmessage = null; + this.onopen = null; + this._handleOpen = function(event) { + _this._debug("open event"); + var _a = _this._options.minUptime, minUptime = _a === void 0 ? DEFAULT.minUptime : _a; + clearTimeout(_this._connectTimeout); + _this._uptimeTimeout = setTimeout(function() { + return _this._acceptOpen(); + }, minUptime); + _this._ws.binaryType = _this._binaryType; + _this._messageQueue.forEach(function(message) { + return _this._ws.send(message); + }); + _this._messageQueue = []; + if (_this.onopen) { + _this.onopen(event); + } + _this._listeners.open.forEach(function(listener) { + return _this._callEventListener(event, listener); + }); + }; + this._handleMessage = function(event) { + _this._debug("message event"); + if (_this.onmessage) { + _this.onmessage(event); + } + _this._listeners.message.forEach(function(listener) { + return _this._callEventListener(event, listener); + }); + }; + this._handleError = function(event) { + _this._debug("error event", event.message); + _this._disconnect(void 0, event.message === "TIMEOUT" ? "timeout" : void 0); + if (_this.onerror) { + _this.onerror(event); + } + _this._debug("exec error listeners"); + _this._listeners.error.forEach(function(listener) { + return _this._callEventListener(event, listener); + }); + _this._connect(); + }; + this._handleClose = function(event) { + _this._debug("close event"); + _this._clearTimeouts(); + if (_this._shouldReconnect) { + _this._connect(); + } + if (_this.onclose) { + _this.onclose(event); + } + _this._listeners.close.forEach(function(listener) { + return _this._callEventListener(event, listener); + }); + }; + this._url = url; + this._protocols = protocols; + this._options = options; + if (this._options.startClosed) { + this._shouldReconnect = false; + } + this._connect(); + } + Object.defineProperty(ReconnectingWebSocket2, "CONNECTING", { + get: function() { + return 0; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2, "OPEN", { + get: function() { + return 1; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2, "CLOSING", { + get: function() { + return 2; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2, "CLOSED", { + get: function() { + return 3; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "CONNECTING", { + get: function() { + return ReconnectingWebSocket2.CONNECTING; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "OPEN", { + get: function() { + return ReconnectingWebSocket2.OPEN; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "CLOSING", { + get: function() { + return ReconnectingWebSocket2.CLOSING; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "CLOSED", { + get: function() { + return ReconnectingWebSocket2.CLOSED; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "binaryType", { + get: function() { + return this._ws ? this._ws.binaryType : this._binaryType; + }, + set: function(value) { + this._binaryType = value; + if (this._ws) { + this._ws.binaryType = value; + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "retryCount", { + get: function() { + return Math.max(this._retryCount, 0); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "bufferedAmount", { + get: function() { + var bytes = this._messageQueue.reduce(function(acc, message) { + if (typeof message === "string") { + acc += message.length; + } else if (message instanceof Blob) { + acc += message.size; + } else { + acc += message.byteLength; + } + return acc; + }, 0); + return bytes + (this._ws ? this._ws.bufferedAmount : 0); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "extensions", { + get: function() { + return this._ws ? this._ws.extensions : ""; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "protocol", { + get: function() { + return this._ws ? this._ws.protocol : ""; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "readyState", { + get: function() { + if (this._ws) { + return this._ws.readyState; + } + return this._options.startClosed ? ReconnectingWebSocket2.CLOSED : ReconnectingWebSocket2.CONNECTING; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(ReconnectingWebSocket2.prototype, "url", { + get: function() { + return this._ws ? this._ws.url : ""; + }, + enumerable: true, + configurable: true + }); + ReconnectingWebSocket2.prototype.close = function(code, reason) { + if (code === void 0) { + code = 1e3; + } + this._closeCalled = true; + this._shouldReconnect = false; + this._clearTimeouts(); + if (!this._ws) { + this._debug("close enqueued: no ws instance"); + return; + } + if (this._ws.readyState === this.CLOSED) { + this._debug("close: already closed"); + return; + } + this._ws.close(code, reason); + }; + ReconnectingWebSocket2.prototype.reconnect = function(code, reason) { + this._shouldReconnect = true; + this._closeCalled = false; + this._retryCount = -1; + if (!this._ws || this._ws.readyState === this.CLOSED) { + this._connect(); + } else { + this._disconnect(code, reason); + this._connect(); + } + }; + ReconnectingWebSocket2.prototype.send = function(data) { + if (this._ws && this._ws.readyState === this.OPEN) { + this._debug("send", data); + this._ws.send(data); + } else { + var _a = this._options.maxEnqueuedMessages, maxEnqueuedMessages = _a === void 0 ? DEFAULT.maxEnqueuedMessages : _a; + if (this._messageQueue.length < maxEnqueuedMessages) { + this._debug("enqueue", data); + this._messageQueue.push(data); + } + } + }; + ReconnectingWebSocket2.prototype.addEventListener = function(type, listener) { + if (this._listeners[type]) { + this._listeners[type].push(listener); + } + }; + ReconnectingWebSocket2.prototype.dispatchEvent = function(event) { + var e_1, _a; + var listeners = this._listeners[event.type]; + if (listeners) { + try { + for (var listeners_1 = __values(listeners), listeners_1_1 = listeners_1.next(); !listeners_1_1.done; listeners_1_1 = listeners_1.next()) { + var listener = listeners_1_1.value; + this._callEventListener(event, listener); + } + } catch (e_1_1) { + e_1 = { error: e_1_1 }; + } finally { + try { + if (listeners_1_1 && !listeners_1_1.done && (_a = listeners_1.return)) + _a.call(listeners_1); + } finally { + if (e_1) + throw e_1.error; + } + } + } + return true; + }; + ReconnectingWebSocket2.prototype.removeEventListener = function(type, listener) { + if (this._listeners[type]) { + this._listeners[type] = this._listeners[type].filter(function(l) { + return l !== listener; + }); + } + }; + ReconnectingWebSocket2.prototype._debug = function() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + if (this._options.debug) { + console.log.apply(console, __spread(["RWS>"], args)); + } + }; + ReconnectingWebSocket2.prototype._getNextDelay = function() { + var _a = this._options, _b = _a.reconnectionDelayGrowFactor, reconnectionDelayGrowFactor = _b === void 0 ? DEFAULT.reconnectionDelayGrowFactor : _b, _c = _a.minReconnectionDelay, minReconnectionDelay = _c === void 0 ? DEFAULT.minReconnectionDelay : _c, _d = _a.maxReconnectionDelay, maxReconnectionDelay = _d === void 0 ? DEFAULT.maxReconnectionDelay : _d; + var delay = 0; + if (this._retryCount > 0) { + delay = minReconnectionDelay * Math.pow(reconnectionDelayGrowFactor, this._retryCount - 1); + if (delay > maxReconnectionDelay) { + delay = maxReconnectionDelay; + } + } + this._debug("next delay", delay); + return delay; + }; + ReconnectingWebSocket2.prototype._wait = function() { + var _this = this; + return new Promise(function(resolve) { + setTimeout(resolve, _this._getNextDelay()); + }); + }; + ReconnectingWebSocket2.prototype._getNextUrl = function(urlProvider) { + if (typeof urlProvider === "string") { + return Promise.resolve(urlProvider); + } + if (typeof urlProvider === "function") { + var url = urlProvider(); + if (typeof url === "string") { + return Promise.resolve(url); + } + if (!!url.then) { + return url; + } + } + throw Error("Invalid URL"); + }; + ReconnectingWebSocket2.prototype._connect = function() { + var _this = this; + if (this._connectLock || !this._shouldReconnect) { + return; + } + this._connectLock = true; + var _a = this._options, _b = _a.maxRetries, maxRetries = _b === void 0 ? DEFAULT.maxRetries : _b, _c = _a.connectionTimeout, connectionTimeout = _c === void 0 ? DEFAULT.connectionTimeout : _c, _d = _a.WebSocket, WebSocket2 = _d === void 0 ? getGlobalWebSocket() : _d; + if (this._retryCount >= maxRetries) { + this._debug("max retries reached", this._retryCount, ">=", maxRetries); + return; + } + this._retryCount++; + this._debug("connect", this._retryCount); + this._removeListeners(); + if (!isWebSocket(WebSocket2)) { + throw Error("No valid WebSocket class provided"); + } + this._wait().then(function() { + return _this._getNextUrl(_this._url); + }).then(function(url) { + if (_this._closeCalled) { + return; + } + _this._debug("connect", { url, protocols: _this._protocols }); + _this._ws = _this._protocols ? new WebSocket2(url, _this._protocols) : new WebSocket2(url); + _this._ws.binaryType = _this._binaryType; + _this._connectLock = false; + _this._addListeners(); + _this._connectTimeout = setTimeout(function() { + return _this._handleTimeout(); + }, connectionTimeout); + }); + }; + ReconnectingWebSocket2.prototype._handleTimeout = function() { + this._debug("timeout event"); + this._handleError(new ErrorEvent(Error("TIMEOUT"), this)); + }; + ReconnectingWebSocket2.prototype._disconnect = function(code, reason) { + if (code === void 0) { + code = 1e3; + } + this._clearTimeouts(); + if (!this._ws) { + return; + } + this._removeListeners(); + try { + this._ws.close(code, reason); + this._handleClose(new CloseEvent(code, reason, this)); + } catch (error) { + } + }; + ReconnectingWebSocket2.prototype._acceptOpen = function() { + this._debug("accept open"); + this._retryCount = 0; + }; + ReconnectingWebSocket2.prototype._callEventListener = function(event, listener) { + if ("handleEvent" in listener) { + listener.handleEvent(event); + } else { + listener(event); + } + }; + ReconnectingWebSocket2.prototype._removeListeners = function() { + if (!this._ws) { + return; + } + this._debug("removeListeners"); + this._ws.removeEventListener("open", this._handleOpen); + this._ws.removeEventListener("close", this._handleClose); + this._ws.removeEventListener("message", this._handleMessage); + this._ws.removeEventListener("error", this._handleError); + }; + ReconnectingWebSocket2.prototype._addListeners = function() { + if (!this._ws) { + return; + } + this._debug("addListeners"); + this._ws.addEventListener("open", this._handleOpen); + this._ws.addEventListener("close", this._handleClose); + this._ws.addEventListener("message", this._handleMessage); + this._ws.addEventListener("error", this._handleError); + }; + ReconnectingWebSocket2.prototype._clearTimeouts = function() { + clearTimeout(this._connectTimeout); + clearTimeout(this._uptimeTimeout); + }; + return ReconnectingWebSocket2; + }(); + var reconnecting_websocket_mjs_default = ReconnectingWebSocket; + + // reactor/static/reactor/reactor-boost.js + function boostAllLinks() { + for (let link of document.querySelectorAll("a[href]")) { + boostElement(link); + } + } + function boostElement(element) { + if (element.tagName?.toLowerCase() === "a" && element.hasAttribute("href") && !(element.boosted || element.hasAttribute("onclick") || element.hasAttribute(":no-boost"))) { + element.boosted = true; + element.addEventListener("click", _load); + } + } + function _load(event) { + event.preventDefault(); + HistoryCache.load(event.target.href); + } + function replaceBodyContent(withHtmlContent, options) { + console.log("replaceBodyContent", options); + let html = document.createElement("html"); + html.innerHTML = withHtmlContent; + window.requestAnimationFrame(() => { + if (options?.beforeReplace) + options.beforeReplace(html); + document.body = html.querySelector("body"); + boostAllLinks(); + if (options?.afterReplace) + options.afterReplace(html); + }); + } + function hasSameOriginAsDocument(url) { + if (url.startsWith("http://") || url.startsWith("https://")) { + return new URL(url).origin === document.location.origin; + } else { + return true; + } + } + var _HistoryCache = class { + static async load(url) { + this._saveCurrentPage(); + console.log(url); + if (hasSameOriginAsDocument(url)) { + this.push(url); + await this.restoreFromCurrentPath(); + } else { + location.assign(url); + } + } + static loadContent(url, content) { + console.log("Load content", url); + replaceBodyContent(content, { + beforeReplace(html) { + _HistoryCache._saveCurrentPage(); + _HistoryCache.push(url); + document.title = html.querySelector("title")?.text ?? ""; + } + }); + } + static async restoreFromCurrentPath() { + this._saveCurrentPage(); + let path = window.location.pathname + window.location.search; + console.log("Restoring Page:", path); + let page = this._get(path); + if (page) { + replaceBodyContent(page.content, { + beforeReplace() { + document.title = page.title; + }, + afterReplace() { + window.scrollTo(0, page.scroll); + } + }); + console.log("currentPath", path); + this.currentPath = path; + } + let response = await fetch(window.location.href); + let content = await response.text(); + this.replace(response.url); + replaceBodyContent(content, { + beforeReplace(html) { + document.title = html.querySelector("title")?.text ?? ""; + }, + afterReplace() { + _HistoryCache._saveCurrentPage(); + } + }); + } + static back() { + window.history.back(); + } + static push(path) { + history.pushState({}, document.title, path); + this.currentPath = path; + } + static replace(path) { + history.replaceState({}, document.title, path); + this.currentPath = path; + } + static _saveCurrentPage() { + console.log("Saving page:", this.currentPath); + this.cache = this.cache.filter(({ url }) => url != this.currentPath); + this.cache.push({ + url: this.currentPath, + content: document.body.outerHTML, + title: document.title, + scroll: window.scrollY + }); + if (this.cache.length > this.maxSize) { + let page = this.cache.shift(); + console.log("Evicted:", page.url); + } + console.log("Currently cached:", this.cache.map(({ url }) => url)); + } + static _get(path) { + return this.cache.find((page) => page.url == path); + } + }; + var HistoryCache = _HistoryCache; + __publicField(HistoryCache, "maxSize", 10); + __publicField(HistoryCache, "cache", []); + __publicField(HistoryCache, "currentPath", window.location.pathname + window.location.search); + window.addEventListener("popstate", (event) => { + HistoryCache.restoreFromCurrentPath(); + }); + window.addEventListener("load", () => { + boostAllLinks(); + }); + var reactor_boost_default = { + boostAllLinks, + boostElement, + HistoryCache + }; + + // reactor/static/reactor/reactor.js + window.morphdom = morphdom_esm_default; + var ServerConnection = class extends EventTarget { + open(path = "__reactor__") { + let protocol = location.protocol.replace("http", "ws"); + this.socket = new reconnecting_websocket_mjs_default(`${protocol}//${location.host}/${path}`, [], { + maxEnqueuedMessages: 0 + }); + this.socket.addEventListener("open", () => { + console.log("WS: OPEN"); + this.dispatchEvent(new Event("open")); + }); + this.socket.addEventListener("message", (event) => this._processMessage(event)); + this.socket.addEventListener("close", () => { + console.log("WS: CLOSE"); + this.dispatchEvent(new Event("close")); + }); + } + get isOpen() { + return this.socket?.readyState == reconnecting_websocket_mjs_default.OPEN; + } + _processMessage(event) { + let { command, payload } = JSON.parse(event.data); + switch (command) { + case "render": + var { id, diff } = payload; + console.log("<<< RENDER", id); + document.getElementById(id)?.applyDiff(diff); + break; + case "remove": + var { id } = payload; + console.log("<<< REMOVE", id); + document.getElementById(id)?.remove(); + break; + case "focus_on": + var { selector } = payload; + console.log("<<< FOCUS-ON", `"${selector}"`); + document.querySelector(selector)?.focus(); + break; + case "visit": + var { url, replace } = payload; + if (replace) { + console.log("<< REPLACE", url); + reactor_boost_default.HistoryCache.replace(url); + } else { + console.log("<< VISIT", url); + reactor_boost_default.HistoryCache.load(url); + } + break; + case "page": + var { url, content } = payload; + console.log("<< PAGE", `"${url}"`); + reactor_boost_default.HistoryCache.loadContent(url, content); + break; + case "back": + reactor_boost_default.HistoryCache.back(); + break; + default: + console.error(`Unknown command "${command}"`, payload); + } + } + sendJoin(name, state) { + console.log(">>> JOIN", name); + this._send("join", { name, state }); + } + sendLeave(id) { + console.log(">>> LEAVE", id); + this._send("leave", { id }); + } + sendUserEvent(id, command, implicit_args, explicit_args) { + console.log(">>> USER_EVENT", id, command, explicit_args); + this._send("user_event", { id, command, implicit_args, explicit_args }); + } + _send(command, payload) { + if (this.isOpen) { + this.socket.send(JSON.stringify({ command, payload })); + } + } + }; + var connection = new ServerConnection(); + for ({ dataset } of document.querySelectorAll("meta[name=reactor-component]")) { + let baseElement = document.createElement(dataset.extends); + class ReactorComponent extends baseElement.constructor { + constructor(...args) { + super(...args); + this._lastReceivedHtml = []; + this.joinBind = () => this.join(); + this.wentOffline = () => this.classList.add("reactor-disconnected"); + } + connectedCallback() { + connection.addEventListener("open", this.joinBind); + connection.addEventListener("close", this.wentOffline); + this.join(); + } + disconnectedCallback() { + connection.removeEventListener("open", this.joinBind); + connection.removeEventListener("close", this.wentOffline); + connection.sendLeave(this.id); + } + join() { + this.classList.remove("reactor-disconnected"); + if (this.isRoot) { + connection.sendJoin(this.dataset.name, this.dataset.state); + } + } + applyDiff(diff) { + let html = this.getHtml(diff); + window.requestAnimationFrame(() => { + morphdom_esm_default(this, html, { + onBeforeNodeAdded(node) { + reactor_boost_default.boostElement(node); + }, + onElUpdated(node) { + reactor_boost_default.boostElement(node); + } + }); + }); + } + getHtml(diff) { + let fragments = []; + let cursor = 0; + for (let fragment of diff) { + if (typeof fragment === "string") { + fragments.push(fragment); + } else if (fragment < 0) { + cursor -= fragment; + } else { + fragments.push(...this._lastReceivedHtml.slice(cursor, cursor + fragment)); + cursor += fragment; + } + } + this._lastReceivedHtml = fragments; + return fragments.join(" "); + } + get isRoot() { + return !this.parentComponent; + } + get parentComponent() { + return this.parentElement?.closest("[reactor-component]"); + } + dispatch(command, args, formScope) { + connection.sendUserEvent(this.id, command, this.serialize(formScope), args); + } + serialize(element) { + let result = {}; + for (let el of element.querySelectorAll("[name]")) { + if (el.closest("[reactor-component]") !== this) { + continue; + } + let value = null; + switch (el.type.toLowerCase()) { + case "checkbox": + case "radio": + value = el.checked ? el.value || true : null; + break; + case "select-multiple": + value = el.selectedOptions.map((option) => option.value); + break; + default: + value = el.value; + break; + } + if (value !== null) { + let key = el.getAttribute("name"); + let values = result[key] ?? []; + values.push(value); + result[key] = values; + } + } + return result; + } + } + customElements.define(dataset.tagName, ReactorComponent, { extends: dataset.extends }); + } + connection.open(); + debounceTimeouts = {}; + window.reactor = { + send(element, name, args) { + let component = element.closest("[reactor-component]"); + if (component !== null) { + let form = element.closest("form"); + let formScope = component.contains(form) ? form : component; + component.dispatch(name, args, formScope); + } + }, + debounce(delayName, delay) { + return (f) => { + return (...args) => { + clearTimeout(debounceTimeouts[delayName]); + debounceTimeouts[delayName] = setTimeout(() => f(...args), delay); + }; + }; + } + }; +})(); +/*! + * Reconnecting WebSocket + * by Pedro Ladaria + * https://github.com/pladaria/reconnecting-websocket + * License MIT + */ +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +//# sourceMappingURL=reactor.min.js.map diff --git a/reactor/static/reactor/transpiler.js b/reactor/static/reactor/transpiler.js new file mode 100644 index 0000000..f529adb --- /dev/null +++ b/reactor/static/reactor/transpiler.js @@ -0,0 +1,176 @@ + + + +/** + * Transpiles the reactor event DSL into JavaScript + * @param {HTMLElement} el + */ +function transpile(el) { + if (el.attributes === undefined) return el + + // find the attrs that need transpilation + let replacements = [] + for (let attr of el.attributes) { + if (attr.name.startsWith("@")) { + console.log(attr.name, attr.value) + replacements.push(transpileAttribute(attr)) + } + } + + // replace the transpiled attrs + for (let { oldName, name, code } of replacements) { + el.attributes.removeNamedItem(oldName) + let newAttr = document.createAttribute(name) + newAttr.value = code + el.attributes.setNamedItem(newAttr) + } + + return el +} + +// Attribute transpiler + +let transpilerCache = {} + + + +class Modifiers { + /** + * Transforms the code in the event handler into JavaScript + * + * onclick="do_something" + * -> {command: "do_something", "args": "null"} + * + * onclick="do_something {argument1: 42, argument2: 'a'}" + * -> {command: "do_something", "args": "{argument1: 42, argument2: 'a'}"} + * + * @param {String} code + */ + static __reactorCode(code) { + let splitBetweenCommandAndArguments = code.indexOf(" ") + let name = code + let args = "null" + if (splitBetweenCommandAndArguments !== -1) { // has arguments + name = code.slice(0, splitBetweenCommandAndArguments) + args = code.slice(splitBetweenCommandAndArguments + 1) + } + return `reactor.send(event.target, '${name}', ${args});` + } + + // Events + + static debounce(code, stack) { + let name = stack.pop() + let delay = stack.pop() + return `reactor.debounce('${name}', ${delay})(() => {${code}})()` + } + + static prevent(code) { + return "event.preventDefault(); " + code + } + + static stop(code) { + return "event.stopPropagation(); " + code + } + + // Key modifiers + + static ctrl(code) { + return `if (event.ctrlKey) { ${code} }` + } + + static alt(code) { + return `if (event.altKey) { ${code} }` + } + + static shift(code) { + return `if (event.shiftKey) { ${code} }` + } + + static meta(code) { + return `if (event.metaKey) { ${code} }` + } + + // Key codes + + static key(code, stack) { + let key = stack.pop() + return `if ((event.key + "").toLowerCase() == "${key}") { ${code} }` + } + + static keyCode(code, stack) { + let keyCode = stack.pop() + return `if (event.keyCode == ${keyCode}) { ${code} }` + } + + // Key shortcuts + + static enter(code) { + return Modifiers.key(code, ["enter"]) + } + + static tab(code) { + return Modifiers.key(code, ["tab"]) + } + + static delete(code) { + return Modifiers.key(code, ["delete"]) + } + + static backspace(code) { + return Modifiers.key(code, ["backspace"]) + } + + static esc(code) { + return Modifiers.key(code, ["escape"]) + } + + static space(code) { + return Modifiers.key(code, [" "]) + } + + static up(code) { + return Modifiers.key(code, ["arrowup"]) + } + + static down(code) { + return Modifiers.key(code, ["arrowdown"]) + } + + static left(code) { + return Modifiers.key(code, ["arrowleft"]) + } + + static right(code) { + return Modifiers.key(code, ["arrowright"]) + } +} + + +function transpileAttribute(attribute) { + let [name, ...modifiers] = attribute.name.split(".") + modifiers.push("__reactorCode") + let cacheKey = `${modifiers}.${attribute.value}` + + let code = transpilerCache[cacheKey] + console.log(">", code !== undefined) + if (code === undefined) { + code = attribute.value + let stack = [] + while (modifiers.length) { + let modifier = modifiers.pop() + let handler = Modifiers[modifier] + if (handler === undefined) { + stack.push(modifiers) + continue + } else { + code = handler(code, stack) + } + } + transpilerCache[cacheKey] = code + } + return { oldName: attribute.name, name: `on${name.slice(1)}`, code } +} + + +export default transpile diff --git a/reactor/templates/reactor_header.html b/reactor/templates/reactor_header.html index 867adc2..7547106 100644 --- a/reactor/templates/reactor_header.html +++ b/reactor/templates/reactor_header.html @@ -1,7 +1,9 @@ {% load static %} +{% for c in components %} + +{% endfor %} - - + diff --git a/reactor/templatetags/reactor.py b/reactor/templatetags/reactor.py index 33e00bb..99be039 100644 --- a/reactor/templatetags/reactor.py +++ b/reactor/templatetags/reactor.py @@ -1,94 +1,111 @@ +import typing as t + from django import template -from django.template.base import Token, Parser, Node from django.core.signing import Signer +from django.template.base import Node, Parser, Token from django.utils.html import format_html - -from .. import json from ..component import Component +from ..event_transpiler import transpile +from ..repository import ComponentRepository register = template.Library() -@register.inclusion_tag('reactor_header.html') +@register.inclusion_tag("reactor_header.html") def reactor_header(): - return {} + return { + "components": [ + {"tag_name": component._tag_name, "extends": component._extends} + for component in Component._all.values() + ] + } @register.simple_tag(takes_context=True) def tag_header(context): - component = context['this'] + component = context["this"] return format_html( - 'is="{tag_name}" id="{id}" state="{state}"', + 'is="{tag_name}" ' + 'id="{id}" ' + 'data-name="{name}" ' + 'data-state="{state}"' + "reactor-component", tag_name=component._tag_name, id=component.id, - state=Signer().sign(component._state_json), + name=component._name, + state=Signer().sign(component.json(exclude={"reactor"})), ) @register.simple_tag(takes_context=True) -def component(context, _name, id=None, **kwargs): - parent = context.get('this') # type: Component - if parent: - component = parent._root_component.get_or_create( - _name, - _parent_id=parent.id, - id=id, - **kwargs - ) - else: - component = Component._build( - _name, - request=context['request'], - id=id, - **kwargs - ) - return component._render() +def component(context, _name, **kwargs): + parent: t.Optional[Component] = context.get("this") + parent_id = parent.id if parent else None + repo: ComponentRepository = context.get( + "reactor_repository", + ComponentRepository(user=context.get("user")), + ) -@register.filter -def concat(value, arg): - return f'{value}-{arg}' + component = repo.build(_name, state=kwargs, parent_id=parent_id) + return component.render(repo) or "" -@register.filter() -def tojson(value, indent=None): - return json.dumps(value, indent=indent) +@register.simple_tag(takes_context=True) +def on(context, _event_and_modifiers, _command, **kwargs): + component: t.Optional[Component] = context.get("this") + assert component, "Can't find a component in this context" + handler = getattr(component, _command, None) + assert handler, f"Missing handler: {component._name}.{_command}" + assert callable(handler), f"Not callable: {component._name}.{_command}" -@register.filter -def eq(value, other): - if value == other: - return 'yes' - else: - return '' + event, code = transpile(_event_and_modifiers, _command, kwargs) + return format_html('{event}="{code}"', event=event, code=code) -@register.filter -def then(value, true_result): - if value: - return true_result - else: - return '' +@register.filter(name="str") +def to_string(value): + return str(value) @register.filter -def ifnot(value, false_result): - if not value: - return false_result - else: - return '' +def concat(value, arg): + return f"{value}{arg}" + + +# Shortcuts and helpers @register.tag() def cond(parser: Parser, token: Token): - dict_expression = token.contents[len('cond '):] + """Prints some text conditionally + + ```html + {% cond {'works': True, 'does not work': 1 == 2} %} + ``` + Will output 'works'. + """ + dict_expression = token.contents[len("cond ") :] return CondNode(dict_expression) -@register.tag(name='class') +@register.tag(name="class") def class_cond(parser: Parser, token: Token): - dict_expression = token.contents[len('class '):] + """Prints classes conditionally + + ```html +
+ ``` + + If `loading` is `True` will print: + + ```html +
+ ``` + """ + dict_expression = token.contents[len("class ") :] return ClassNode(dict_expression) @@ -98,11 +115,7 @@ def __init__(self, dict_expression): def render(self, context): terms = eval(self.dict_expression, context.flatten()) - return ' '.join( - term - for term, ok in terms.items() - if ok - ) + return " ".join(term for term, ok in terms.items() if ok) class ClassNode(CondNode): diff --git a/reactor/tests.py b/reactor/tests.py deleted file mode 100644 index 0229eec..0000000 --- a/reactor/tests.py +++ /dev/null @@ -1,153 +0,0 @@ -import json -from uuid import uuid4 -from contextlib import asynccontextmanager - -from pyquery import PyQuery as q - -from django.contrib.auth.models import AnonymousUser -from django.core.serializers.json import DjangoJSONEncoder -from channels.testing import WebsocketCommunicator - -from .channels import ReactorConsumer - - -class ReactorCommunicator(WebsocketCommunicator): - MAX_WAIT = 2 # seconds - - def __init__(self, user=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.scope['user'] = user or AnonymousUser() - self._component_types = {} - self._components = {} - self.redirected_to = None - self.loop_timeout = None - - async def connect(self, *args, **kwargs): - connected, subprotocol = await super().connect(*args, **kwargs) - if connected: - response = await self.receive_json_from() - assert response['type'] == 'components' - self._component_types = response['component_types'] - return connected, subprotocol - - async def auto_join(self, response): - doc = q(response.content) - for component in doc('[id][state]'): - tag_name = component.get('is') or component.tag - assert tag_name in self._component_types - state = json.loads(component.get('state')) - component_id = self.add_component(tag_name, state) - await self.send_join(component_id) - - def add_component(self, tag_name: str, *args, **kwargs): - assert tag_name in self._component_types - component = Component(tag_name, *args, **kwargs) - self._components[component.id] = component - return component.id - - def __getitem__(self, _id): - return self._components[_id] - - def get_by_name(self, name): - for component in self._components.values(): - if component.tag_name == name: - return component - - async def send_join(self, component_id): - component = self._components[component_id] - await self.send_json_to({ - 'command': 'join', - 'payload': { - 'tag_name': component.tag_name, - 'state': component.state, - }, - }) - await self.loop_over_messages() - return component.doc - - async def send(self, _id, _name, **args): - assert _id in self._components - await self.send_json_to({ - 'command': 'user_event', - 'payload': { - 'id': _id, - 'name': _name, - 'args': args, - } - }) - await self.loop_over_messages(reset_timeout=True) - return self._components[_id].doc - - async def send_json_to(self, data): - await self.send_to(text_data=json.dumps(data, cls=DjangoJSONEncoder)) - - async def loop_over_messages(self, reset_timeout=False): - if reset_timeout or not self.loop_timeout: - self.loop_timeout = 0.1 - while await self.receive_nothing(timeout=self.loop_timeout): - self.loop_timeout *= 2 - if self.loop_timeout > self.MAX_WAIT: - break - - while not await self.receive_nothing(timeout=self.loop_timeout): - response = await self.receive_json_from() - if response['type'] in ('redirect', 'push_state'): - self.redirected_to = response['url'] - else: - component = self._components[response['id']] - if response['type'] == 'render': - component.apply_diff(response['html_diff']) - elif response['type'] == 'remove': - component.apply_remove() - - -@asynccontextmanager -async def reactor(consumer=ReactorConsumer, path='/__reactor__', user=None): - comm = ReactorCommunicator(application=consumer, path=path, user=user) - connected, subprotocol = await comm.connect() - assert connected - try: - yield comm - finally: - await comm.disconnect() - - -class Component: - def __init__(self, tag_name: str, state: dict = None): - state = state or {} - state.setdefault('id', str(uuid4())) - self.tag_name = tag_name - self.state = state - self.last_received_html = [] - self.doc = None - self.removed = False - - @property - def id(self): - return self.state['id'] - - def apply_diff(self, html_diff): - html = [] - cursor = 0 - for diff in html_diff: - if isinstance(diff, str): - html.append(diff) - elif diff < 0: - cursor -= diff - else: - html.extend(self.last_received_html[cursor:cursor + diff]) - cursor += diff - self.last_received_html = html - self.doc = q(' '.join(self.last_received_html)) - - state = self.doc.attr['state'] - if state: - self.state = json.loads(state) - - def apply_remove(self): - self.removed = True - - def __str__(self): - return f'<{self.tag_name} {self.state}>' - - __repr__ = __str__ diff --git a/reactor/types.py b/reactor/types.py new file mode 100644 index 0000000..726e2a4 --- /dev/null +++ b/reactor/types.py @@ -0,0 +1,39 @@ +__all__ = ["Model", "QuerySet"] + + +class ModelLoader: + def __getitem__(self, ModelClass): + if not hasattr(ModelClass, "__get_validators__"): + + def validate(value, field): + if not isinstance(value, field.type_): + value = field.type_.objects.filter(pk=value).first() + return value + + def __get_validators__(cls): + yield validate + + ModelClass.__get_validators__ = classmethod(__get_validators__) + return ModelClass + + +class QuerySetLoader: + def __getitem__(self, ModelClass): + QSClass = ModelClass.objects._queryset_class + if not hasattr(QSClass, "__get_validators__"): + + def validate(value, field): + if not isinstance(value, field.type_): + value = ModelClass.objects.filter(pk__in=value) + return value + + def __get_validators__(cls): + yield validate + + QSClass.__get_validators__ = classmethod(__get_validators__) + + return QSClass[ModelClass] + + +Model = ModelLoader() +QuerySet = QuerySetLoader() diff --git a/reactor/urls.py b/reactor/urls.py index 9a80e8d..fdec739 100644 --- a/reactor/urls.py +++ b/reactor/urls.py @@ -1,9 +1,10 @@ +import json -import orjson -from django.urls import path from django.http.response import HttpResponse -from .channels import ReactorConsumer +from django.urls import path + from .component import Component +from .consumer import ReactorConsumer def build_component(request, component): @@ -13,23 +14,21 @@ def build_component(request, component): def user_event(request, component, event): component = _get_component(request, component) - args = orjson.loads(request.GET.get('args', '{}')) - html = ' '.join(component._dispatch(event, args)) + args = json.loads(request.GET.get("args", "{}")) + html = " ".join(component._dispatch(event, args)) return HttpResponse(html) def _get_component(request, component) -> Component: - component = Component._build(component, _context={'user': request.user}) - kwargs = orjson.loads(request.GET.get('state', '{}')) + component = Component._build(component, _context={"user": request.user}) + kwargs = orjson.loads(request.GET.get("state", "{}")) component.mount(**kwargs) return component urlpatterns = [ - path('__reactor__/', build_component), - path('__reactor__//', user_event), + path("__reactor__/", build_component), + path("__reactor__//", user_event), ] -websocket_urlpatterns = [ - path('__reactor__', ReactorConsumer.as_asgi()) -] +websocket_urlpatterns = [path("__reactor__", ReactorConsumer.as_asgi())] diff --git a/reactor/utils.py b/reactor/utils.py index 9df3255..035dc4c 100644 --- a/reactor/utils.py +++ b/reactor/utils.py @@ -1,124 +1,102 @@ import inspect import logging -import typing from functools import wraps -import pydantic from asgiref.sync import async_to_sync - -from django.http import QueryDict from channels.layers import get_channel_layer +from django.utils.datastructures import MultiValueDict -log = logging.getLogger('reactor') +log = logging.getLogger("reactor") def broadcast(*names, **kwargs): for name in names: - log.debug(f'<-> {name}') - send_to_group(name, 'update', **kwargs) + log.debug(f"<-> {name}") + send_to_group(name, "model_mutation", **kwargs) def on_commit(f): @wraps(f) def wrapper(*args, **kwargs): from django.db.transaction import on_commit - on_commit(lambda: f(*args, **kwargs)) - return wrapper + on_commit(lambda: f(*args, **kwargs)) -def send_to_channel(_channel_name, type, **kwargs): - if _channel_name: - @on_commit - def send_message(): - async_to_sync(get_channel_layer().send)( - _channel_name, dict(type=type, **kwargs) - ) - send_message() + return wrapper def send_to_group(_whom, type, **kwargs): if _whom: + @on_commit def send_message(): async_to_sync(get_channel_layer().group_send)( _whom, dict(type=type, origin=_whom, **kwargs) ) + send_message() # Introspection -def get_model(f, ignore=()): - params = list(inspect.signature(f).parameters.values()) - fields = {} - for param in params: - if param.name in ignore: - continue - if param.kind is not inspect.Parameter.VAR_KEYWORD: - default = param.default - if default is inspect._empty: - default = ... - annotation = param.annotation - if annotation is inspect._empty: - if default is ...: - field = (typing.Any, ...) - else: - field = default - else: - field = (annotation, default) - - if field is None: - continue - fields[param.name] = field - return pydantic.create_model(f.__name__, **fields) +def filter_parameters(f, kwargs): + has_kwargs = any( + param.kind == inspect.Parameter.VAR_KEYWORD + for param in inspect.signature(f).parameters.values() + ) + if has_kwargs: + return kwargs + else: + return { + param: value + for param, value in kwargs.items() + if param in f.model.__fields__ + } # Decoder for client requests -def extract_data(arguments): - query = QueryDict(mutable=True) - for key, value in arguments: - query.appendlist(key, value) - kwargs = {} - for key in set(query): - if key.endswith('[]'): - value = query.getlist(key) +def parse_request_data(data: MultiValueDict): + output = {} + for key in set(data): + if key.endswith("[]"): + value = data.getlist(key) else: - value = query.get(key) - _set_value_on_path(kwargs, key, value) - return kwargs + value = data.get(key) + _set_value_on_path(output, key, value) + return output def _set_value_on_path(target, path, value): initial = target - parts = path.split('.') - for part in parts[:-1]: - part, default, index = _get_default_value(part) - target.setdefault(part, default) - target = target[part] + fragments = path.split(".") + for fragment in fragments[:-1]: + fragment, default, index = _get_default_value(fragment) + target.setdefault(fragment, default) + target = target[fragment] if index is not None: i_need_this_length = index + 1 - len(target) if i_need_this_length > 0: target.extend({} for _ in range(i_need_this_length)) target = target[index] - part, default, index = _get_default_value(parts[-1]) - target[part] = value + fragment, default, index = _get_default_value(fragments[-1]) + target[fragment] = value return initial -def _get_default_value(part): - if part.endswith('[]'): - part = part[:-2] +def _get_default_value(fragment): + if fragment.endswith("[]"): + fragment = fragment[:-2] default = [] index = None - if part.endswith(']'): - index = int(part[part.index('[') + 1:-1]) - part = part[:part.index('[')] + if fragment.endswith("]"): + index = int(fragment[fragment.index("[") + 1 : -1]) + fragment = fragment[: fragment.index("[")] default = [] else: default = {} index = None - return part, default, index + return fragment, default, index diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..74df626 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,8 @@ + +[flake8] +max-line-length = 80 +show-soure = true +ignore = + B011 # 'assert False' is fine. + E203 # black puts spaces before ':' + W503 # line break before binary (and/or) operator diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f01ad99 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +from pathlib import Path + +from setuptools import find_packages, setup + +HERE = Path(__file__).absolute().parent +README = open(HERE / "README.md", encoding="utf8").read() + +setup( + name="djangoreactor", + version="3.0.0b", + url="https://github.com/edelvalle/reactor", + author="Eddy Ernesto del Valle Pino", + author_email="eddy@edelvalle.me", + long_description=README, + long_description_content_type="text/markdown", + description="Brings LiveView from Phoenix framework into Django", + license="BSD", + packages=find_packages(exclude=["tests"]), + include_package_data=True, + zip_safe=False, + python_requires=">=3.6", + install_requires=[ + "channels>=3.0.4,<4", + "pydantic>=1.8.0,<2", + "lru-dict>=1.1.7,<1.2", + ], + extras_require={ + "dev": [ + "black", + "flake8", + "ipython", + "whitenoise", + "channels-redis", + ] + }, + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Internet :: WWW/HTTP", + ], +) diff --git a/tests/fision/settings.py b/tests/fision/settings.py index 7ffa61b..2e10de6 100644 --- a/tests/fision/settings.py +++ b/tests/fision/settings.py @@ -11,6 +11,7 @@ """ import os + up = os.path.dirname # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -25,89 +26,88 @@ # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'b7!j@8gk-vdq3tona^(i(qg#xiir*%r-@u1f&fw@@(ccwy^ijb' +SECRET_KEY = "b7!j@8gk-vdq3tona^(i(qg#xiir*%r-@u1f&fw@@(ccwy^ijb" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ - 'fision.todo', - 'reactor', - 'channels', - 'whitenoise.runserver_nostatic', - - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "fision.todo", + "reactor", + "channels", + "whitenoise.runserver_nostatic", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'fision.urls' +ROOT_URLCONF = "fision.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'fision.wsgi.application' -ASGI_APPLICATION = 'fision.asgi.application' +WSGI_APPLICATION = "fision.wsgi.application" +ASGI_APPLICATION = "fision.asgi.application" # In memory CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels.layers.InMemoryChannelLayer', + "default": { + "BACKEND": "channels.layers.InMemoryChannelLayer", }, } -# # With redis -# CHANNEL_LAYERS = { -# 'default': { -# 'BACKEND': 'channels_redis.core.RedisChannelLayer', -# 'CONFIG': { -# "hosts": [('127.0.0.1', 6379)], -# }, -# }, -# } +# With redis +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - 'ATOMIC_REQUESTS': True, - 'OPTIONS': { - 'timeout': 20, + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "ATOMIC_REQUESTS": True, + "OPTIONS": { + "timeout": 20, }, "TEST": { "NAME": os.path.join(BASE_DIR, "db_test.sqlite3"), @@ -121,16 +121,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa }, ] @@ -138,9 +138,9 @@ # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -149,35 +149,33 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' -STATIC_ROOT = os.path.join(BASE_DIR, 'static') +STATIC_URL = "/static/" +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" +STATIC_ROOT = os.path.join(BASE_DIR, "static") LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'django.server', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "django.server", }, }, - 'formatters': { - 'django.server': { - '()': 'reactor.log.ServerFormatter', - 'format': '[{server_time}] {message}', - 'style': '{', + "formatters": { + "django.server": { + "()": "reactor.log.ServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", } }, - 'loggers': { - 'reactor': { - 'handlers': ['console'], - 'level': 'DEBUG', - + "loggers": { + "reactor": { + "handlers": ["console"], + "level": "DEBUG", }, }, } diff --git a/tests/fision/todo/apps.py b/tests/fision/todo/apps.py deleted file mode 100644 index 6c85b8d..0000000 --- a/tests/fision/todo/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class TodoConfig(AppConfig): - name = 'todo' diff --git a/tests/fision/todo/live.py b/tests/fision/todo/live.py index 8a9d7c0..c1fe467 100644 --- a/tests/fision/todo/live.py +++ b/tests/fision/todo/live.py @@ -1,17 +1,20 @@ from django.db.transaction import atomic +from pydantic import Field + from reactor.component import Component +from reactor.types import Model, QuerySet from .models import Item class XTodoList(Component): - template_name = 'todo/list.html' + _template_name = "todo/list.html" - def __init__(self, showing='all', new_item='', **kwargs): - super().__init__(**kwargs) - self.showing = showing - self.new_item = new_item - self._subscribe('item.new') + showing: str = "all" + new_item: str = "" + + def mounted(self): + self.reactor.subscribe("item.new") @property def items(self): @@ -24,14 +27,14 @@ def all_items_are_completed(self): @atomic def add(self, new_item: str): Item.objects.create(text=new_item) - self.new_item = '' + self.new_item = "" def show(self, showing: str): self.showing = showing @atomic - def toggle_all(self, toggle_all: bool): - self.items.update(completed=toggle_all) + def toggle_all(self, toggle_all: bool = False): + self.reactor.redirect_to("/to-index") @atomic def clear_completed(self): @@ -39,38 +42,39 @@ def clear_completed(self): class XTodoCounter(Component): - template_name = 'todo/counter.html' + _template_name = "todo/counter.html" + + items: QuerySet[Item] = Field(default_factory=Item.objects.all) - def __init__(self, items=None, **kwargs): - super().__init__(**kwargs) - self.items = items or Item.objects.all() - self._subscribe('item') + def mounted(self): + self.reactor.subscribe("item") class XTodoItem(Component): - template_name = 'todo/item.html' + _template_name = "todo/item.html" - def __init__(self, item=None, editing=False, showing='all', **kwargs): - super().__init__(**kwargs) - self.editing = editing - self.showing = showing - self.item = item or Item.objects.filter(id=self.id).first() + item: Model[Item] + editing: bool = False + showing: str = "all" + + def mounted(self): if self.item: - self._subscribe(f'item.{self.item.id}') + self.reactor.subscribe(f"item.{self.item.id}") else: - super().destroy() + self.destroy() + @property def is_visible(self): return ( - self.showing == 'all' or - self.showing == 'completed' and self.item.completed or - self.showing == 'active' and not self.item.completed + self.showing == "all" + or (self.showing == "completed" and self.item.completed) + or (self.showing == "active" and not self.item.completed) ) @atomic - def destroy(self): + def delete(self): self.item.delete() - super().destroy() + self.destroy() @atomic def completed(self, completed: bool = False): @@ -80,6 +84,8 @@ def completed(self, completed: bool = False): def toggle_editing(self): if not self.item.completed: self.editing = not self.editing + if self.editing: + self.focus_on(f"#{self.id} input[name=text]") @atomic def save(self, text): diff --git a/tests/fision/todo/templates/base.html b/tests/fision/todo/templates/base.html index 35b366e..7d0de99 100644 --- a/tests/fision/todo/templates/base.html +++ b/tests/fision/todo/templates/base.html @@ -4,7 +4,7 @@ - Template • TodoMVC + {{ title }} • TodoMVC {% reactor_header %} diff --git a/tests/fision/todo/templates/index.html b/tests/fision/todo/templates/index.html index e913920..6b35b69 100644 --- a/tests/fision/todo/templates/index.html +++ b/tests/fision/todo/templates/index.html @@ -5,6 +5,7 @@

todo lists

{% endblock body %} diff --git a/tests/fision/todo/templates/todo.html b/tests/fision/todo/templates/todo.html index b5f1a21..7821ab8 100644 --- a/tests/fision/todo/templates/todo.html +++ b/tests/fision/todo/templates/todo.html @@ -2,5 +2,5 @@ {% load reactor %} {% block body %} - {% component 'x-todo-list' %} + {% component 'XTodoList' %} {% endblock body %} diff --git a/tests/fision/todo/templates/todo/counter.html b/tests/fision/todo/templates/todo/counter.html index fc05f1b..b6b80fc 100644 --- a/tests/fision/todo/templates/todo/counter.html +++ b/tests/fision/todo/templates/todo/counter.html @@ -4,4 +4,5 @@ {% with amount=items.active.count %} {{ amount }} item{{ amount|pluralize }} left {% endwith %} + diff --git a/tests/fision/todo/templates/todo/item.html b/tests/fision/todo/templates/todo/item.html index 06d1700..2335c3a 100644 --- a/tests/fision/todo/templates/todo/item.html +++ b/tests/fision/todo/templates/todo/item.html @@ -1,29 +1,25 @@ {% load reactor %}
-
  • - - + +
  • diff --git a/tests/fision/todo/templates/todo/list.html b/tests/fision/todo/templates/todo/list.html index acdb320..a18073c 100644 --- a/tests/fision/todo/templates/todo/list.html +++ b/tests/fision/todo/templates/todo/list.html @@ -8,9 +8,8 @@

    todos

    placeholder="What needs to be done?" name="new_item" value="{{ this.new_item }}" - :override autofocus - @keypress.enter="add" + {% on 'keypress.enter' 'add' %} > @@ -21,15 +20,15 @@

    todos

    class="toggle-all" type="checkbox" name="toggle_all" - {{ this.all_items_are_completed|then:'checked' }} - @change="toggle_all" + {% cond {"checked": all_items_are_completed} %} + {% on 'change' 'toggle_all' %} >
      {% for item in this.items %} - {% component 'x-todo-item' id=item.id item=item showing=this.showing %} + {% component 'XTodoItem' id='item-'|concat:item.id item=item showing=this.showing %} {% endfor %}
    {% endif %} @@ -38,16 +37,14 @@

    todos

    {% if this.items %} {% endif %} diff --git a/tests/fision/todo/urls.py b/tests/fision/todo/urls.py index 7ce9bbc..d830cf2 100644 --- a/tests/fision/todo/urls.py +++ b/tests/fision/todo/urls.py @@ -1,7 +1,9 @@ from django.urls import path + from . import views urlpatterns = [ - path('', views.index, name='index'), - path('todo', views.todo, name='todo'), + path("", views.index, name="index"), + path("todo", views.todo, name="todo"), + path("to-index", views.redirect_to_index, name="redirect_to_index"), ] diff --git a/tests/fision/todo/views.py b/tests/fision/todo/views.py index 6f8839b..574b9eb 100644 --- a/tests/fision/todo/views.py +++ b/tests/fision/todo/views.py @@ -1,9 +1,14 @@ -from django.shortcuts import render +from django.shortcuts import redirect, render def index(request): - return render(request, 'index.html') + print("GET", request.GET) + return render(request, "index.html", context={"title": "index"}) def todo(request): - return render(request, 'todo.html') + return render(request, "todo.html", context={"title": "todo"}) + + +def redirect_to_index(request): + return redirect("/?frombackend=1") diff --git a/tests/fision/urls.py b/tests/fision/urls.py index 52070a2..cd38fb5 100644 --- a/tests/fision/urls.py +++ b/tests/fision/urls.py @@ -14,15 +14,16 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include -from reactor.channels import ReactorConsumer +from django.urls import include, path + +from reactor.consumer import ReactorConsumer urlpatterns = [ - path('', include('fision.todo.urls')), + path("", include("fision.todo.urls")), # path('', include('fision.frontend.urls')), - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), ] websocket_urlpatterns = [ - path('__reactor__', ReactorConsumer), + path("__reactor__", ReactorConsumer.as_asgi()), ] diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..e4f18e6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,121 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +esbuild-android-arm64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.9.tgz#6cc4a0c623332c0830a311ddd8242b1f496ff940" + integrity sha512-Ty0hKldtjJVLHwUwbKR4GFPiXBo5iQ3aE1OLBar9lh3myaRkUGEb+Ypl74LEKa0+t/9lS3Ev1N5+5P2Sq6UvNQ== + +esbuild-darwin-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.9.tgz#df44297c2438032cda2b21548a82bb007e2105cc" + integrity sha512-Ay0/b98v0oYp3ApXNQ7QPbaSkCT9WjBU6h8bMB1SYrQ/PmHgwph91fb9V0pfOLKK1rYWypfrNbI0MyT2tWN+rQ== + +esbuild-darwin-arm64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.9.tgz#704ef404a6a38eda190d40ed354e7f2c1c839081" + integrity sha512-nJB8chaJdWathCe6EyIiMIqfyEzbuXPyNsPlL3bYRB1zFCF8feXT874D4IHbJ/w8B6BpY3sM1Clr/I/DK8E4ow== + +esbuild-freebsd-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.9.tgz#fbbf22c99e15f27d0f8a1a040d7961a86f0d3a4e" + integrity sha512-ktaBujf12XLkVXLGx7WjFcmh1tt34tm7gP4pHkhvbzbHrq+BbXwcl4EsW+5JT9VNKl7slOGf4Qnua/VW7ZcnIw== + +esbuild-freebsd-arm64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.9.tgz#809fff4c43653dbbf071ffce9f80a030b278098e" + integrity sha512-vVa5zps4dmwpXwv/amxVpIWvFJuUPWQkpV+PYtZUW9lqjXsQ3LBHP51Q1cXZZBIrqwszLsEyJPa5GuDOY15hzQ== + +esbuild-linux-32@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.9.tgz#f9fd3423481e51674e9817d5eea25689889a5f5a" + integrity sha512-HxoW9QNqhO8VW1l7aBiYQH4lobeHq85+blZ4nlZ7sg5CNhGRRwnMlV6S08VYKz6V0YKnHb5OqJxx2HZuTZ7tgQ== + +esbuild-linux-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.9.tgz#9d7f66866dae1abaff7cbc3749f2847d5fb72fd5" + integrity sha512-L+eAR8o1lAUr9g64RXnBLuWZjAItAOWSUpvkchpa6QvSnXFA/nG6PgGsOBEqhDXl9qYEpGI0ReDrFkf8ByapvQ== + +esbuild-linux-arm64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.9.tgz#669202e71b9ced4d285bfd1d69de948e013ac28f" + integrity sha512-IjbhZpW5VQYK4nVI4dj/mLvH5oXAIf57OI8BYVkCqrdVXJwR8nVrSqux3zJSY+ElrkOK3DtG9iTPpmqvBXaU0g== + +esbuild-linux-arm@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.9.tgz#c3ceb56ec0e3dbd1a3a89dca6cb7fc0ca360bcc8" + integrity sha512-DT0S+ufCVXatPZHjkCaBgZSFIV8FzY4GEHz/BlkitTWzUvT1dIUXjPIRPnqBUVa+0AyS1bZSfHzv9hTT4LHz7A== + +esbuild-linux-mips64le@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.9.tgz#bf4bd389ee14b67c5c77669952f2de6b2cc8a003" + integrity sha512-ec9RgAM4r+fe1ZmG16qeMwEHdcIvqeW8tpnpkfSQu9T4487KtQF6lg3TQasTarrLLEe7Qpy+E+r4VwC8eeZySQ== + +esbuild-linux-ppc64le@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.9.tgz#465b7bdc70577da606b3b5d463028292b6d834ad" + integrity sha512-7b2/wg8T1n/L1BgCWlMSez0aXfGkNjFuOqMBQdnTti3LRuUwzGJcrhRf/FdZGJ5/evML9mqu60vLRuXW1TdXCg== + +esbuild-netbsd-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.9.tgz#94f2dabe83520066cc1e1fae3ecff78695a8ebb1" + integrity sha512-PiZu3h4+Szj0iZPgvuD2Y0isOXnlNetmF6jMcOwW54BScwynW24/baE+z7PfDyNFgjV04Ga2THdcpbKBDhgWQw== + +esbuild-openbsd-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.9.tgz#b47f6a641ca37358aeedb2b7c4bb73dd0682c6d5" + integrity sha512-SJKN4Ez+ilY7mu+1gAdGQ9N6dktBfbEkiOAvw+hT7xHrNnTnrTGH0FT4qx9dazB9HX6D04L4PXmVOyynqi+oEQ== + +esbuild-sunos-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.9.tgz#b0df4a316b7c98eb490f4bd0db381cf2c391ae73" + integrity sha512-9N0RjZ7cElE8ifrS0nBrLQgBMQNPiIIKO2GzLXy7Ms8AM3KjfLiV2G2+9O0B9paXjRAHchIwazTeOyeWb1vyWA== + +esbuild-windows-32@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.9.tgz#e229563e134e634f9748cc8315c691e2013259ef" + integrity sha512-awxWs1kns+RfjhqBbTbdlePjqZrAE2XMaAQJNg9dtu+C7ghC3QKsqXbu0C26OuF5YeAdJcq9q+IdG6WPLjvj9w== + +esbuild-windows-64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.9.tgz#103ad3f13e1a0e44934b91f358e81dd201b86b34" + integrity sha512-VmA9GQMCzOr8rFfD72Dum1+AWhJui7ZO6sYwp6rBHYu4vLmWITTSUsd/zgXXmZuHBPkkvxLJLF8XsKFCRKflJA== + +esbuild-windows-arm64@0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.9.tgz#545bb58848008258b339b1b00fcfe92c85bc7251" + integrity sha512-P/jPY2JwmTpgEPh9BkXpCe690tcDSSo0K9BHTniSeEAEz26kPpqldVa4XDm0R+hNnFA7ecEgNskr4QAxE1ry0w== + +esbuild@^0.13.9: + version "0.13.9" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.9.tgz#aafc4b3375ac443ae7b223c26c4e58d10d2d535b" + integrity sha512-8bYcckmisXjGvBMeylp1PRtu21uOoCDFAgXGGF2BR241zYQDN6ZLNvcmQlnQ7olG0p6PRWmJI8WVH3ca8viPuw== + optionalDependencies: + esbuild-android-arm64 "0.13.9" + esbuild-darwin-64 "0.13.9" + esbuild-darwin-arm64 "0.13.9" + esbuild-freebsd-64 "0.13.9" + esbuild-freebsd-arm64 "0.13.9" + esbuild-linux-32 "0.13.9" + esbuild-linux-64 "0.13.9" + esbuild-linux-arm "0.13.9" + esbuild-linux-arm64 "0.13.9" + esbuild-linux-mips64le "0.13.9" + esbuild-linux-ppc64le "0.13.9" + esbuild-netbsd-64 "0.13.9" + esbuild-openbsd-64 "0.13.9" + esbuild-sunos-64 "0.13.9" + esbuild-windows-32 "0.13.9" + esbuild-windows-64 "0.13.9" + esbuild-windows-arm64 "0.13.9" + +morphdom@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.6.1.tgz#e868e24f989fa3183004b159aed643e628b4306e" + integrity sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA== + +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==