Bootstrap
Committed a1bacd
index 0000000..3b2cf34
--- /dev/null
+name: Run Tests and Analysis
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build-linux:
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+ runs-on: "ubuntu-latest"
+ steps:
+ - name: Install graphviz
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y graphviz
+
+ - uses: actions/checkout@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Remove development dependencies
+ run: sed -i '/\[tool.poetry.group.dev.dependencies\]/,/\[/d' pyproject.toml
+
+ - name: Install Poetry
+ uses: snok/install-poetry@v1
+ with:
+ version: 1.2.2
+ virtualenvs-in-project: true
+
+ - name: Install dependencies
+ run: poetry install --no-interaction --no-root
+
+ - name: Install library
+ run: poetry install --no-interaction
+
+ - name: Install development tools
+ run: poetry add gmpg
+
+ - uses: psf/black@stable
+ with:
+ options: "--check --verbose"
+ src: "."
+ version: "23.7"
+
+ - uses: isort/isort-action@v1
+ with:
+ configuration: "--profile black"
+
+ - name: Run tests
+ run: poetry run gmpg test
+
+ - name: Run analysis
+ run: poetry run gmpg analyze
+
+ - name: Generate dependency graph
+ run: poetry run gmpg graph
+
+ - uses: actions/upload-artifact@v3
+ with:
+ name: analysis
+ path: |
+ test_coverage.xml
+ test_results.xml
+ api_python.json
+ deps.svg
index 0000000..3c1c5c2
--- /dev/null
+[tool.poetry]
+name = "webint-system"
+version = "0.0.23"
+description = "manage the system"
+keywords = ["webint"]
+homepage = "https://ragt.ag/code/projects/webint-system"
+repository = "https://ragt.ag/code/projects/webint-system.git"
+documentation = "https://ragt.ag/code/projects/webint-system/api"
+authors = ["Angelo Gladding <angelo@ragt.ag>"]
+license = "BSD-2-Clause"
+packages = [{include="webint_system"}]
+
+[tool.poetry.plugins."webapps"]
+system = "webint_system:app"
+
+[tool.poetry.dependencies]
+python = ">=3.10,<3.11"
+pygments = "^2.14.0"
+webint = ">=0.0"
+webagt = ">=0.0"
+bgq = ">=0.0"
+
+[tool.poetry.group.dev.dependencies]
+bgq = {path="../bgq", develop=true}
+gmpg = {path="../gmpg", develop=true}
+webint = {path="../webint", develop=true}
+webagt = {path="../webagt", develop=true}
+newmath = {path="../newmath", develop=true}
+sqlyte = {path="../sqlyte", develop=true}
+microformats = {path="../python-microformats", develop=true}
+
+# [[tool.poetry.source]]
+# name = "main"
+# url = "https://ragt.ag/code/pypi"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
index 0000000..f67e9ba
--- /dev/null
+import webint_system
+
+
+def test_app():
+ def get_headers(h):
+ return [_.text for _ in webint_system.app.get("").dom.select(f"h{h}")]
+
+ assert get_headers(1) == ["System"]
+ assert get_headers(2) == ["Addresses", "Software", "Settings"]
+ assert get_headers(3) == ["Clearnet", "Darknet", "Routes", "Wrappers"]
index 0000000..76b4b2b
--- /dev/null
+"""
+Manage code on your website.
+
+- Supports [PEP 503 -- Simple Repository API][0] managing Python packages.
+
+[0]: https://www.python.org/dev/peps/pep-0503/
+
+"""
+
+# TODO PEP 592 -- Adding "Yank" Support to the Simple API
+# TODO PEP 658 -- Serve Distribution Metadata in the Simple Repository API
+
+import importlib.metadata
+import pathlib
+import re
+import shutil
+import subprocess
+import time
+
+import pkg_resources
+import semver
+import web
+import webagt
+
+app = web.application(__name__, prefix="system")
+
+code_dir = pathlib.Path.home() / "code"
+meta_dir = code_dir / "meta"
+working_dir = code_dir / "working"
+
+
+def get_ip():
+ return subprocess.check_output(["hostname", "-I"]).split()[0].decode()
+
+
+def update_system(*packages):
+ print(
+ subprocess.run(
+ ["/home/admin/runinenv", "/home/admin/app", "pip", "install", "-U"]
+ + list(packages),
+ capture_output=True,
+ )
+ )
+ print(
+ subprocess.run(
+ ["sudo", "service", "supervisor", "restart"], capture_output=True
+ )
+ )
+
+
+def get_versions(package):
+ """Return the latest version if currently installed `package` is out of date."""
+ current_version = pkg_resources.get_distribution(package).version
+ current_version = current_version.partition("a")[0] # TODO FIXME strips alpha/beta
+ update_available = False
+ versions_rss = webagt.get(
+ f"https://pypi.org/rss/project/{package}/releases.xml"
+ ).xml
+ latest_version = [
+ child.getchildren()[0].text
+ for child in versions_rss.getchildren()[0].getchildren()
+ if child.tag == "item"
+ ][0]
+ if semver.compare(current_version, latest_version) == -1:
+ update_available = latest_version
+ return current_version, update_available
+
+
+@app.wrap
+def set_working_dir(handler, main_app):
+ web.tx.host.working_dir = working_dir
+ yield
+
+
+@app.control("")
+class System:
+ """The system that runs your website."""
+
+ owner_only = ["get"]
+
+ def get(self):
+ """"""
+ try:
+ with open("/home/admin/app/run/onion") as fp:
+ onion = fp.read().strip()
+ except FileNotFoundError:
+ onion = None
+ try:
+ with open("domains") as fp:
+ domains = fp.readlines()
+ except FileNotFoundError:
+ domains = []
+ webint_metadata = importlib.metadata.metadata("webint")
+ webint_versions = get_versions("webint")
+ return app.view.index(
+ get_ip(),
+ onion,
+ domains,
+ web.tx.app.cfg,
+ web.tx.app,
+ web.get_apps(),
+ webint_metadata,
+ webint_versions,
+ )
+
+
+@app.control("addresses")
+class Addresses:
+ """System addresses."""
+
+ def post(self):
+ """"""
+ return "addresses have been updated"
+
+
+@app.control("addresses/domains")
+class Domains:
+ """System addresses."""
+
+ def get(self):
+ """"""
+ form = web.form("domain")
+ records = {}
+ for record in ("A", "CNAME", "MX", "NS"):
+ try:
+ records[record] = web.dns.resolve(form.domain, record)
+ except (web.dns.NoAnswer, web.dns.NXDOMAIN):
+ pass
+ return app.view.addresses.domains(get_ip(), form.domain, records)
+
+ def post(self):
+ form = web.form("domain")
+ web.enqueue(add_domain, form.domain)
+ return "adding domain"
+
+
+def add_domain(domain):
+ """Begin handling requests at given domain."""
+ ip = get_ip()
+ onions = []
+ with open("onion") as fp:
+ onions.append(fp.read())
+
+ domain_path = pathlib.Path("domains")
+ with domain_path.open() as fp:
+ domains = {domain: True for domain in fp.readlines()}
+ domains[domain] = False
+
+ run_dir = "/home/admin/app/run"
+ nginx_conf_path = pathlib.Path("/etc/nginx/nginx.conf")
+ with nginx_conf_path.open("w") as fp:
+ fp.write(str(web.host.templates.nginx(run_dir, ip, onions, domains)))
+ subprocess.call(["sudo", "service", "nginx", "restart"])
+
+ web.generate_cert(domain)
+
+ domains[domain] = True
+ with nginx_conf_path.open("w") as fp:
+ fp.write(str(web.host.templates.nginx(run_dir, ip, onions, domains)))
+ subprocess.call(["sudo", "service", "nginx", "restart"])
+
+ with domain_path.open("w") as fp:
+ for domain in domains:
+ print(domain, file=fp)
+
+
+@app.control("software")
+class Software:
+ """System software."""
+
+ def post(self):
+ """"""
+ web.enqueue(
+ update_system,
+ "canopy-platform",
+ "webint",
+ *[a.project_name for a in web.get_apps().keys()],
+ )
+ return "software update has been started"
+
+
+@app.control("settings")
+class Settings:
+ """System settings."""
+
+ def post(self):
+ """"""
+ form = web.form("key", "value")
+ web.tx.app.update_config(form.key, form.value)
+ return "settings have been updated"
+
+
+@app.control("robots.txt", prefixed=False)
+class RobotsTXT:
+ """A robots.txt file."""
+
+ def get(self):
+ """Return a robots.txt file."""
+ all_bots = ["User-agent: *"]
+ for project in web.application("webint_code").model.get_projects():
+ all_bots.append(f"Disallow: /code/{project}/releases/")
+ return "\n".join(all_bots)
index 0000000..53f8a57
--- /dev/null
+import re
+from inspect import getsourcefile
+from pprint import pformat
+
+from web import tx
+
+__all__ = ["getsourcefile", "pformat", "tx", "re"]
index 0000000..e69de29
index 0000000..6643290
--- /dev/null
+$def with (ip_address, domain, records)
+$var title: $domain
+
+$# ip_address == records["A"][0]:
+$if True:
+ <form method=post action=/addresses/domains>
+ <input type=hidden name=domain value=$domain>
+ <button>Add domain</button>
+ </form>
+$else:
+ <p>This machine's IP address does not match this domain's IP address.</p>
+
+$# <p>This machine's address: $ip_address</p>
+$# <p>Domain's address: $records["A"][0]</p>
+
+$if "CNAME" in records:
+ <p>$records["CNAME"][0]</p>
+$# <p>$records["MX"][0]</p>
+$# <p>$records["NS"][0]</p>
index 0000000..c2cf215
--- /dev/null
+$def with (ip_address, onion, domains, settings, main_app, apps, webint_metadata, webint_versions)
+$# , understory_version, applications)
+$var title: System
+
+<h2 id=addresses>Addresses</h2>
+
+<h3>Clearnet</h3>
+<p>$ip_address</p>
+<h4>Domain Name System (DNS)</h4>
+$if domains:
+ <p>$", ".join(domains)</p>
+$else:
+ <p><strong><em>It is recommended that you add a domain name.</em></strong></p>
+<form method=get action=/addresses/domains>
+<label>Domain Name<br>
+<input name=domain></label>
+<div><button>Add</button></div>
+</form>
+
+<h3>Darknet</h3>
+<h4>The Onion Router (Tor)</h4>
+$if onion:
+ <p style=font-family:monospace>$(onion[:28])­$(onion[28:])</p>
+<form method=post action=/addresses/tor/miner>
+<label>Onion<br>
+<input name=prefix></label>
+<div><button>Add</button></div>
+</form>
+
+<h2 id=software>Software</h2>
+<form action=/software method=post><button>Update</button></form>
+<p>Primary application: <code>$main_app.name</code></p>
+<p><code>webint</code> version: $webint_versions[0]
+$if webint_versions[1]:
+ out of date. current version is $webint_versions[1].
+</p>
+
+$# <ul style=font-size:.8em>\
+$# $for package, package_apps in sorted(apps.items(), key=lambda x: x[0].metadata["Name"]):
+$# <li>
+$# $package.metadata["Name"]
+$# <small><code>$package.metadata["Version"]</code>
+$# $for mounted_prefix, mount_app in main_app.mounts:
+$# $for _, package_app, _, _ in package_apps:
+$# $if mount_app == package_app:
+$# mounted at <code>/$mounted_prefix</code>
+$# </small>
+$# $# $#app_name
+$# $# <small><a href=/$app.project_name>$app.project_name</a>
+$# $# <a href=/$app.project_name/$app.version><small>$app.version</small></a></small>
+$# </li>
+$# </ul>
+
+$def render_parent_controllers(location, controllers):
+ <ul id="$(location)_controllers">
+ $for route, controller in controllers:
+ <li>
+ <details><summary>
+ $ parts = re.split(r"\(\?P<(.+?)>(.+?)\)", route)
+ $if len(parts) == 1:
+ <a href=$tx.origin/$route>$controller.__web__[0]</a>\
+ $else:
+ $controller.__web__[0]\
+ $if len(controller.__web__[0]) == 0:
+ <a href=/>$tx.request.uri.host</a>\
+ $ project_mod = "/".join(controller.handler.__module__.split("."))
+ $ project = getsourcefile(controller.handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+  <small>$:str(mkdn((controller.__doc__.strip() + "\n").splitlines()[0])).removeprefix("<p>").removesuffix("</p>")</small>
+ </summary>
+ <!--small><strong>
+ <a class=controller href=/projects/$project/api/$project_mod#$controller.handler.__name__>$controller.handler.__name__</a>
+ </strong></small-->
+ </details>
+ </li>
+ </ul>
+
+<h3 id=routes>Routes</h3>
+
+$:render_parent_controllers("before", main_app.controllers)
+
+<ul id=mounts>
+$for prefix, subapp in sorted(main_app.mounts):
+ $if subapp.controllers:
+ <li>
+ <details><summary>
+ <a href=$tx.origin/$prefix>$prefix</a>\
+ $ root = subapp.controllers[0]
+ $if root[0] == "":
+ $ project_mod = "/".join(root[1].handler.__module__.split("."))
+ $ project = getsourcefile(root[1].handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+  <small>$:str(mkdn((root[1].__doc__.strip() + "\n").splitlines()[0])).removeprefix("<p>").removesuffix("</p>").rstrip()</small>
+ <!--strong><small><a class=controller
+ href=/projects/$project/api/$project_mod#$root[1].handler.__name__>🛈</a>
+ </small></strong-->
+ </summary>
+ $if len(subapp.controllers) > 1:
+ <ul>
+ $for route, controller in subapp.controllers[1:]:
+ $ parts = re.split(r"\(\?P<(.+?)>(.+?)\)", route)
+ <li>\
+ $if len(parts) == 1:
+ $if parts[0]:
+ <a href=$tx.origin/$prefix/$parts[0]>$parts[0]</a>\
+ $else:
+ $for a, b, c in zip(parts[0::3], parts[1::3], parts[2::3]):
+ $:a.replace("(", '<span class="optional">')\
+ <span title="$c">{$b}</span>\
+ $:parts[-1].replace(")?", "</span>")\
+ $ project_mod = "/".join(controller.handler.__module__.split("."))
+ $ project = getsourcefile(controller.handler).removeprefix(str(tx.host.working_dir)).lstrip("/").partition("/")[0]
+ $ docstring = str(mkdn((controller.__doc__.strip() + "\n").splitlines()[0])).removeprefix("<p>").removesuffix("</p>").rstrip()
+  <small>$docstring <!--<strong><a class=controller
+ href=/projects/$project/api/$project_mod#$controller.handler.__name__>🛈</a>
+ </strong--></small>
+ </li>
+ </ul>
+ <p style=text-align:right>
+ $for app, app_details in apps.items():
+ $if prefix == app_details[0][0]:
+ $app.metadata["Summary"]<br>
+ <small><code>\
+ $if "Home-page" in app.metadata:
+ <a href=$app.metadata["Home-page"]>\
+ $app.project_name\
+ $if "Home-page" in app.metadata:
+ </a>
+ $app.version</code><small> by </small><a
+ href="https://$app.metadata['Author-email']">$app.metadata["Author"]
+ <code><$app.metadata["Author-email"]></code></a>
+ </small>
+ </p>
+ </details>
+ </li>
+</ul>
+
+$:render_parent_controllers("after", main_app.after_controllers)
+$:render_parent_controllers("after", main_app.unprefixed_controllers)
+
+<div id=wrappers>
+<h3>Wrappers</h3>
+<ol style=columns:3;line-height:1.25;list-style:none;padding:0>
+$for wrapper in main_app.wrappers:
+ <li>$wrapper.__name__<br>
+ <small>$wrapper.__module__</small>
+ </li>
+</ol>
+</div>
+
+<h2 id=settings>Settings</h2>
+<ul>
+$for key, value in settings.items():
+ <li><strong>$key:</strong> <code>$value</code></li>
+</ul>
+<form method=post action=/settings>
+<label>Key <input name=key></label><br>
+<label>Value <input name=value></label><br>
+<button>Add</button>
+</form>
+
+$# $def aside():
+$# $# <h4><a href=/applications>Applications</a></h4>
+$# $# <ul id=applications>
+$# $# $for application in applications:
+$# $# <li>$application.project_name<br>
+$# $# <small>$application.version</small></li>
+$# $# </ul>
+$# $# <form method=post action=/applications>
+$# $# <label><small>Application URL</small><br>
+$# $# <label class=bounding><input type=text name=application_url
+$# $# placeholder="github path, pypi path or .git url"></label></label>
+$# $# <div class=buttons><button>Install</button></div>
+$# $# </form>
+$# $var aside = aside
+
+$# XXX <ul>
+$# XXX $for prefix, subapp in main_app.mounts:
+$# XXX $if subapp.wrappers:
+$# XXX $for wrapper in subapp.wrappers:
+$# XXX <li>$wrapper</li>
+$# XXX </ul>
+
+<style>
+ul#before_controllers, ul#after_controllers, ul#mounts, div#wrappers ul {
+ list-style: none;
+ padding-left: 0; }
+ul#mounts > li {
+ margin: .25em 0; }
+ul#mounts > li li {
+ margin: .5em 0; }
+em {
+ color: #586e75; }
+select {
+ border: 0; }
+#mounts label.bounding {
+ display: inline; }
+a.controller {
+ color: #b58900;
+ font-family: mono;
+ font-size: .9em;
+ text-decoration: none; }
+.optional {
+ border-style: dotted;
+ border-width: .2em; }
+@media (prefers-color-scheme: light) {
+ .optional {
+ border-color: #93a1a1; }
+}
+@media (prefers-color-scheme: dark) {
+ .optional {
+ border-color: #586e75; }
+}
+
+#applications {
+ font-size: .8em;
+ list-style: none;
+ padding-left: 0; }
+</style>
index 0000000..cc5b23c
--- /dev/null
+$def with (resource)
+<!doctype html>
+<html>
+<head>
+<title>$resource.title</title>
+</head>
+<body>
+<h1>$resource.title</h1>
+$:resource
+</body>
+</html>