PMBlue updater

Distributable source: modules/pmblue_update.py · version 0.3.6

Download and maintain individual Python modules from https://update.pmblue.us/. The updater shares one bulk version response between module checks, validates downloaded Python, and replaces files atomically. Signed downloads can be required by configuring a trusted public key.

Import has side effects. Importing pmblue_update calls self_update() for the updater itself. It may contact the server, write _update_cache.json in the current working directory, and replace its own source file. The newly downloaded updater takes effect on a subsequent import in a new process or an explicit reload.

Installation and checking several modules

The client requires requests; all other imported libraries and verification primitives are available in Python 3.7. Use a requests release compatible with your Python interpreter. Signature verification does not require cryptography on clients. Obtain the updater from your trusted deployment and make it importable alongside the modules you maintain.

import pmblue_update

# Updates files in this directory without importing those modules.
for name in ("toolbox", "dialpad", "aiosqliteObj"):
    pmblue_update.update_str(name, path="modules", adv_info=True)

Use update_str when preparing files for another process. Use update(module) when you already hold an imported module and want it reloaded after a download. Use import_mod for a module that may need its first download; its path must also be importable by Python.

import pmblue_update
import toolbox

pmblue_update.update(toolbox, adv_info=True)

# Inside a module that declares __version__:
pmblue_update.self_update()

Bulk versions and caches

remote_version(name, timeout=3) requests /update/versions once, caches the complete mapping in memory for REFRESH_TIME (30 seconds), and returns a Version object for the requested module. A signed client uses /update/manifest instead, obtaining all versions, hashes, and signatures together. Downloads remain one request per changed module.

# Example /update/versions response; values follow installed server files.
{
  "pmblue_update": "0.3.6",
  "toolbox": "1.5.1.1",
  "dialpad": "1.3",
  "normalize": null
}

The unsigned client falls back to /update/{name}/version when the bulk endpoint returns 404. It remembers that fallback for the process lifetime. Missing modules and modules without a declared version cause LookupError through the bulk lookup. In legacy fallback, a missing module's HTTP 404 raises requests.HTTPError before the response text is checked.

self_update, update, and update_str also consult a disk cache of per-module check times. CACHE=False disables this disk cache, but not the shared in-memory version response. update_str(refresh_time=...) overrides only the disk interval. Disk timestamps are currently written before network checks, so failed requests can suppress another check until that interval passes.

Installation and rollback boundaries

  1. The download must have a successful HTTP status and compile as Python.
  2. When a trusted key is configured, the content hash and Ed25519 signature must match the manifest.
  3. The updater writes a temporary file in the destination directory, flushes and synchronizes it, preserves an existing file's permission bits, then replaces the destination using os.replace.
  4. update reloads the imported module; import_mod imports it. If that operation raises an ordinary exception, the previous source bytes are restored, or a newly created file is removed. The reload rollback does not catch SystemExit or KeyboardInterrupt.

The transaction covers the source file. It cannot undo side effects already performed by imported code or fully restore a partially mutated in-memory module. self_update and update_str do not import the downloaded code. import_mod uses importlib.import_module, which can return an already loaded module without reloading it. Atomic replacement does not provide a transaction across several modules.

Optional Ed25519 verification

Before starting a signed client, configure PMBLUE_TRUSTED_PUBLIC_KEY with the base64-encoded 32-byte Ed25519 public key supplied by your server administrator. Public keys must be distributed through a trusted channel. The server must already be configured to sign manifests.

# Shell configuration, before importing pmblue_update:
export PMBLUE_TRUSTED_PUBLIC_KEY='<base64 public key>'

The client checks the SHA-256 digest of the exact downloaded bytes and verifies a signature over this UTF-8 message (with no final newline):

pmblue-module-v1
<module name>
<module version>
<lowercase SHA-256 hex digest>

The verifier uses integer arithmetic and hashlib.sha512, validates canonical encodings and the public key subgroup, and accepts the same keys and signatures as the server's Ed25519 implementation. When a key is configured, a missing manifest or invalid signature prevents installation; there is no automatic downgrade to an unsigned download. Without a configured key, legacy HTTPS downloads remain available. See server signing setup for key generation.

Publishing a module

Declare a numeric dotted __version__ and call self_update(primary_mod=True) from that module. If the bulk lookup reports the module absent, the updater offers to add it. A missing module on an older server's legacy endpoint instead follows the connection-error path and does not show that prompt. If its local version is newer, it offers to replace the server copy. It asks for confirmation and the server upload password, then posts base64 source bytes. Running the updater file directly also uses this publisher mode.

The server changes the exact bytes primary_mod=True to primary_mod=False in distributed downloads. It also applies this transformation before calculating hashes and signatures. The server's separate top-level updater copy may differ from the published modules/pmblue_update.py.

Upload reporting limitation: the client currently checks only status 401 after a POST. The server returns 403 for a wrong password on an existing module, so the client can print a false success message. Confirm publishing with the server's version endpoint. POST calls currently have no explicit timeout.

Choosing an API

FunctionBehavior and result
self_update(...)Checks the calling file; returns True after replacement, False on many no-update/error paths, and None on a cache hit or successful add. Does not reload the caller.
update(mod, ...)Checks and reloads an imported module. Returns the module after a download or handled connection failure; returns None on a cache hit and normally when current with adv_info=False.
update_str(name, path="", ...)Checks and replaces a file without importing it. Returns None. Supports a disk-cache refresh_time override.
import_mod(name, path="", ...)Downloads if absent, then imports by dotted path. Include a trailing separator for a nonempty path; use paths inside an importable package. It can return False on a version-request connection failure.
update_check(mod, ...)Compares the bulk server version to mod.__version__; returns a boolean. Uses the full mod.__name__, unlike update, which uses its last component.
update_reload(mod, ...)Checks disk version and reloads locally; makes no direct version request. The current string-versus-Version comparison can trigger a reload even for the same version.
maintain / maintenanceConvenience wrappers that print errors. For existing files, maintain currently omits path when it calls update_str; use the explicit update_str loop above for another directory.
get_version / VersionReads __version__ without importing a file; parses up to four numeric components. This is not a general PEP 440 or semantic-version parser.
remote_version / install_moduleLower-level helpers. A signed installation needs metadata previously fetched through remote_version. install_module returns previous source bytes or None.

Objects, parameters, and results

Version: a parsed local or remote version

Version(value) converts its input to text, extracts a numeric dotted value, and records up to four components. Instances are returned by get_version and remote_version. Construct them yourself when comparing a local declaration to a server result. Use strings for input: a float such as 1.10 has already lost the distinction between 1.10 and 1.1.

MemberMeaning and use
rawThe extracted numeric text, not the original unprocessed input. For a conventional quoted declaration, quotes and surrounding text are removed.
major, minor, patch, extraInteger components. Omitted components are None; an empty numeric extraction uses a major component of zero.
shortLegacy major/minor representation, often a float. It cannot represent all dotted versions faithfully; prefer the component fields for display and avoid using it as a release identifier.
str(version)Joins the stored components as dotted text. repr(version) produces a debugging form such as <Version 1.2.3>.
left < right, left > right, left == rightUse two Version objects. Equality compares their dotted text, so Version("1.2") and Version("1.2.0") are not equal. The legacy comparison implementation has additional ordering edge cases; see compatibility notes below.
from pmblue_update import Version, get_version

release = Version("1.2.3")
print(release.major, release.minor, release.patch, release.extra)
# 1 2 3 None
print(str(release))                 # 1.2.3
print(release < Version("1.2.4"))   # True for this comparison

local = get_version("toolbox.py", path="modules")
if local is not None:
    print("Local toolbox:", str(local))

Downloaded response and manifest records

install_module(name, path, response) expects a response object with raise_for_status() and byte-valued content; a normal requests.Response provides these. It returns the old file's bytes, or None if no previous file existed. The name selects signed metadata, while path is the actual destination file, including .py.

A manifest record is a dictionary containing version, sha256, and, when signed, signature. The updater manages the cached records internally. Fetch through remote_version before a low-level signed installation; a response object alone does not supply trusted metadata.

Common function options

OptionMeaning
adv_infoPrint extra progress information. It also affects an existing return-value branch in update, so retain your original module reference instead of always assigning its result.
ignore_errSuppresses errors on documented wrapper paths. It does not bypass HTTP, Python syntax, checksum, or signature validation before replacement.
timeoutVersion-request timeout passed to requests. Most module downloads use timeout + 2; the update branch of import_mod still uses 5 seconds, and publisher POST calls do not set a timeout.
primary_modEnables interactive upload prompts in self_update. Leave false for unattended consumer processes.
pathA directory for file-based helpers. update_str normalizes a trailing separator; import_mod also uses the path to derive an import name and currently needs a trailing separator in nonempty input.
refresh_timeupdate_str's per-module disk-cache interval. It does not alter the 30-second shared server-version cache.

Worked examples

Inspect installed versions without updating those modules

This compares source declarations to server metadata. It does not import or install toolbox or Dialpad. Importing the updater itself still performs its normal startup check.

from pathlib import Path
import requests
import pmblue_update

for name in ("toolbox", "dialpad"):
    local = pmblue_update.get_version(name + ".py", path="modules")
    try:
        remote = pmblue_update.remote_version(name, timeout=5)
    except (requests.RequestException, LookupError, ValueError) as error:
        print(name, "could not be checked:", error)
        continue
    print(name, "installed:", str(local) if local else "not versioned",
          "available:", str(remote))
# The two remote_version calls normally share one bulk response.

Prepare a directory and report each module separately

Use explicit update_str calls when the destination is another directory. They avoid the current path-handling limitation in maintain. Inspect the file after each call because connection errors can be handled internally without raising.

from pathlib import Path
import pmblue_update

destination = Path("modules")
destination.mkdir(exist_ok=True)
for name in ("toolbox", "aiosqliteObj"):
    try:
        pmblue_update.update_str(
            name, path=str(destination), adv_info=True,
            timeout=5, refresh_time=0,
        )
        installed = pmblue_update.get_version(name + ".py", str(destination))
        print(name, "file version:", str(installed) if installed else "unavailable")
    except Exception as error:
        print(name, "installation was not completed:", error)

Perform a verified low-level installation

Set PMBLUE_TRUSTED_PUBLIC_KEY in the process environment before startup. This example requires that setting, fetches signed metadata, downloads the module, and installs it without executing the downloaded module. Validation failures propagate and leave the existing file in place.

import os
from pathlib import Path
import requests
import pmblue_update

if not os.environ.get("PMBLUE_TRUSTED_PUBLIC_KEY"):
    raise RuntimeError("Configure a trusted public key before installing")

name = "toolbox"
available = pmblue_update.remote_version(name, timeout=5)
download = requests.get(
    pmblue_update.URL + "update/" + name + "/module", timeout=10,
)
destination = Path("modules")
destination.mkdir(exist_ok=True)
previous = pmblue_update.install_module(
    name, str(destination / (name + ".py")), download,
)
print("Installed", str(available), "replaced existing file:", previous is not None)

Compatibility and current limits

Server routes, signing setup, and deployment · All guides

Complete source API

Generated from modules/pmblue_update.py; version 0.3.6. Includes public functions, classes, directly declared methods, properties, and Python protocol methods. Conditional APIs may require optional dependencies. Inherited members and dynamically assigned attributes are explained in the guide where relevant.

Signatures and docstrings are extracted without importing or running the module. A property or AsyncProperty decorator changes how a member is accessed; see its decorators and the guide.

Classes and objects

Module functions

install_module(name, path, response)

Source line 138

Verify a download before replacing the installed Python file.
ParameterPassing conventionDefault / required
namepositional or keywordrequired
pathpositional or keywordrequired
responsepositional or keywordrequired
remote_version(name, timeout=3)

Source line 165

Get a module version, sharing one server response across checks.
ParameterPassing conventionDefault / required
namepositional or keywordrequired
timeoutpositional or keyword3
class Version

Source line 209

Construct: Version(v_str)

Fields assigned by the constructor: extra, major, minor, patch, raw, short. Some assignments may be conditional; see the object guide for meaning and lifecycle.

Declared functions, properties, and nested objects:

Version.__init__(self, v_str)

Source line 210

ParameterPassing conventionDefault / required
v_strpositional or keywordrequired
Version.__lt__(self, other)

Source line 255

ParameterPassing conventionDefault / required
otherpositional or keywordrequired
Version.__gt__(self, other)

Source line 281

ParameterPassing conventionDefault / required
otherpositional or keywordrequired
Version.__eq__(self, other: object) -> bool

Source line 283

ParameterPassing conventionDefault / required
other: objectpositional or keywordrequired

Return annotation: bool.

Version.__str__(self)

Source line 288

No caller-supplied parameters are declared.

Version.__repr__(self)

Source line 290

No caller-supplied parameters are declared.

get_version(mod_name, path='')

Source line 310

ParameterPassing conventionDefault / required
mod_namepositional or keywordrequired
pathpositional or keyword''
current_module()

Source line 330

No caller-supplied parameters are declared.

self_update(adv_info=False, ignore_err=False, timeout=3, primary_mod=False)

Source line 333

ParameterPassing conventionDefault / required
adv_infopositional or keywordFalse
ignore_errpositional or keywordFalse
timeoutpositional or keyword3
primary_modpositional or keywordFalse
update(mod, adv_info=False, ignore_err=False, timeout=3, extra='')

Source line 425

ParameterPassing conventionDefault / required
modpositional or keywordrequired
adv_infopositional or keywordFalse
ignore_errpositional or keywordFalse
timeoutpositional or keyword3
extrapositional or keyword''
import_mod(mod_name, path='', adv_info=False, info=True, ignore_err=False, timeout=3)

Source line 496

ParameterPassing conventionDefault / required
mod_namepositional or keywordrequired
pathpositional or keyword''
adv_infopositional or keywordFalse
infopositional or keywordTrue
ignore_errpositional or keywordFalse
timeoutpositional or keyword3
update_check(mod, ignore_err=False, timeout=3)

Source line 606

ParameterPassing conventionDefault / required
modpositional or keywordrequired
ignore_errpositional or keywordFalse
timeoutpositional or keyword3
update_reload(mod, info=True, ignore_err=False)

Source line 629

ParameterPassing conventionDefault / required
modpositional or keywordrequired
infopositional or keywordTrue
ignore_errpositional or keywordFalse
update_str(mod_name, path='', adv_info=False, ignore_err=False, timeout=3, refresh_time=None)

Source line 672

ParameterPassing conventionDefault / required
mod_namepositional or keywordrequired
pathpositional or keyword''
adv_infopositional or keywordFalse
ignore_errpositional or keywordFalse
timeoutpositional or keyword3
refresh_timepositional or keywordNone
maintain(mod_name, path='', adv_info=False)

Source line 739

ParameterPassing conventionDefault / required
mod_namepositional or keywordrequired
pathpositional or keyword''
adv_infopositional or keywordFalse
maintenance(*mod_names, path='', adv_info=False)

Source line 763

ParameterPassing conventionDefault / required
mod_namesextra positional arguments (*args)optional collection
pathkeyword only''
adv_infokeyword onlyFalse