my eye

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])&shy;$(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]
+        &ensp;<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]
+            &ensp;<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__>&#x1f6c8;</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()
+                &ensp;<small>$docstring <!--<strong><a class=controller
+                href=/projects/$project/api/$project_mod#$controller.handler.__name__>&#x1f6c8;</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>&ensp;by&ensp;</small><a
+                href="https://$app.metadata['Author-email']">$app.metadata["Author"]
+                <code>&lt;$app.metadata["Author-email"]&gt;</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>