visualdynamics.update¶
update
¶
Is there a newer version than this one?
A check, not an updater. It asks one URL for one small JSON file, and the answer goes in a status line: 0.1.0 is available. Nothing is downloaded, nothing is replaced, nothing runs with privileges.
That was a deliberate stopping point, and it still is everywhere but
the packaged macOS app. A real updater has to verify what it downloaded
before running it — an unverified one is a remote code execution
feature with a friendly name. The macOS app does it through Sparkle
since 2026-09-28 (gui/updater.py): every update is signed with an
EdDSA key held in the release machine's keychain, and the app refuses
anything that does not verify against SPARKLE_PUBLIC_KEY. Windows
waits for its signing (WinSparkle), and a pip install updates with pip.
The manifest lives on visualdynamics.org, a static host with no
server to run — the release workflow writes it from each published
release and deploys it with the site (web/public/latest.json is the
holding page's copy). Its shape:
{"version": "0.1.0", "url": "https://…/releases", "notes": "…"}
Functions:
| Name | Description |
|---|---|
parse_version |
'0.10.2' -> (0, 10, 2, 1, 0), for comparing rather than for showing. |
newer |
Is |
trust_store |
The TLS context the check verifies visualdynamics.org with: what |
reason_for |
Why the manifest could not be had, in words for a status line. |
attempt |
(manifest, None), or (None, why not) in words. |
fetch |
The manifest as published, or None when it cannot be had. |
check |
The manifest when it offers something newer, else None. |
upgrade_command |
The one line that updates this copy, when pip is how it updates. |
Functions:¶
parse_version
¶
'0.10.2' -> (0, 10, 2, 1, 0), for comparing rather than for showing.
Numeric, so 0.10 sorts above 0.9 — a string compare puts them the other way round and would offer a downgrade as an upgrade. Anything unparseable becomes zero rather than raising: a malformed manifest should mean no update offered, never a traceback in front of somebody who only opened the application.
A pre-release sorts below its release: '0.1.0a1' is (0, 1, 0, 0, 1) and '0.1.0' is (0, 1, 0, 3, 0), so the first non-alpha 0.1.0 is offered to an alpha as the upgrade it is (2026-09-11). The stage is a for alpha, b for beta, rc for a candidate — Python's own spelling, and the one the version string carries — and a release is stage 3, above them all.
Source code in src/visualdynamics/update.py
newer
¶
trust_store
¶
The TLS context the check verifies visualdynamics.org with: what the operating system trusts.
truststore (MIT; what pip itself uses) verifies against the
system's own store — the Keychain on macOS, the certificate store
on Windows, OpenSSL's system bundle on Linux. That is where a
corporate network's own certificate authority lives: a network that
inspects HTTPS re-signs every site with it, IT installs it on the
machine, and curl and the browser trust it while a Python bundle
of Mozilla's roots has never heard of it. Brandon's work machine
reported exactly that, "unable to get local issuer certificate", on
a network where curl got a 200 (2026-09-25).
Without truststore — a bare checkout without it installed — the
older arrangement stands: a packaged build carries its own OpenSSL,
and the one the wheels bring was built elsewhere with a compiled-in
certificate directory that exists on nobody else's machine (read
off the library with strings, 2026-09-15), so the default context
trusts nothing; when it is empty the Mozilla bundle certifi ships
is loaded instead.
Source code in src/visualdynamics/update.py
reason_for
¶
Why the manifest could not be had, in words for a status line.
One sentence, naming the cause rather than the exception. Folding every failure into "could not reach visualdynamics.org" was the original behavior and it cost a long diagnosis by hand: a proxy, a filtered domain, an unverifiable certificate, a slow network and a site that is down all read identically, and none of them can be told apart from outside the program (Brandon, 2026-09-23).
urlopen wraps the cause. A refused connection, a timeout and a
certificate that will not verify all arrive as a URLError whose
reason is the real exception — measured against a self-signed
host, 2026-09-23. The first version tested for the certificate
error before unwrapping, so a real one never matched and read as
"could not be reached", which is the one message that hides the
likeliest cause on an inspecting network. Its test passed only
because it raised the certificate error bare, a shape urllib never
produces. So HTTPError first (it is a URLError, and its
status is the news), then unwrap, then read the cause once.
Source code in src/visualdynamics/update.py
attempt
¶
(manifest, None), or (None, why not) in words.
The reading fetch is built on. It exists because a status line
that says only "could not reach visualdynamics.org" cannot be
acted on: the answer a person needs is which of the half-dozen
things went wrong.
Source code in src/visualdynamics/update.py
fetch
¶
The manifest as published, or None when it cannot be had.
check folds "nothing newer" and "could not ask" into one answer
for a script that only wants to know whether to act. Where the two
deserve different words — up to date is not the same news as
offline — attempt carries the reason, and the menu uses that.
Source code in src/visualdynamics/update.py
check
¶
The manifest when it offers something newer, else None.
Every failure is None: no network, a captive portal, a 404, a truncated file, a version that does not parse. An update check is the least important thing the application does and must never be the reason it is slow to start or noisy on launch — which is why this takes a timeout and why the caller runs it off the main thread.
Source code in src/visualdynamics/update.py
upgrade_command
¶
The one line that updates this copy, when pip is how it updates.
A pip install downloads only what changed — the package, a few MB,
not a 480 MB application — so the news that a version is available
comes with the command to take it, spelled with this interpreter's
own path so it lands in this environment rather than whichever
pip the shell finds first (Brandon, 2026-09-28: show the command,
with a copy button; the app does not run it).
None where pip is not the way: the packaged application (a download,
or Sparkle on macOS), and an editable install — a checkout, where
pip install --upgrade would swap the working tree for the PyPI
release and git pull is the update.