Skip to content

jmcore.version

jmcore.version

Centralized version management for JoinMarket NG.

This is the single source of truth for the project version. All components inherit their version from here.

Attributes

GITHUB_RELEASES_URL = 'https://api.github.com/repos/joinmarket-ng/joinmarket-ng/releases/latest' module-attribute

VERSION = __version__ module-attribute

__version__ = '0.32.0' module-attribute

logger = logging.getLogger(__name__) module-attribute

Classes

UpdateCheckResult dataclass

Result of a GitHub update check.

Source code in jmcore/src/jmcore/version.py
140
141
142
143
144
145
@dataclass(frozen=True)
class UpdateCheckResult:
    """Result of a GitHub update check."""

    latest_version: str
    is_newer: bool
Attributes
is_newer: bool instance-attribute
latest_version: str instance-attribute

Functions

check_for_updates_from_github(socks_proxy: str | None = None, timeout: float = 30.0) -> UpdateCheckResult | None async

Check GitHub for the latest release and compare with the local version.

This function makes an HTTP request to the GitHub API. When socks_proxy is provided, the request is routed through the given SOCKS5 proxy (e.g. Tor).

Privacy note: This contacts GitHub and reveals your IP (or Tor exit node). Only call this when the user has explicitly opted in via check_for_updates.

Args: socks_proxy: Optional SOCKS5 proxy URL (e.g. "socks5h://127.0.0.1:9050"). timeout: HTTP request timeout in seconds.

Returns: UpdateCheckResult with the latest version and whether it is newer, or None if the check failed for any reason.

Source code in jmcore/src/jmcore/version.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
async def check_for_updates_from_github(
    socks_proxy: str | None = None,
    timeout: float = 30.0,
) -> UpdateCheckResult | None:
    """Check GitHub for the latest release and compare with the local version.

    This function makes an HTTP request to the GitHub API. When socks_proxy is
    provided, the request is routed through the given SOCKS5 proxy (e.g. Tor).

    **Privacy note**: This contacts GitHub and reveals your IP (or Tor exit node).
    Only call this when the user has explicitly opted in via ``check_for_updates``.

    Args:
        socks_proxy: Optional SOCKS5 proxy URL (e.g. "socks5h://127.0.0.1:9050").
        timeout: HTTP request timeout in seconds.

    Returns:
        UpdateCheckResult with the latest version and whether it is newer,
        or None if the check failed for any reason.
    """
    import httpx

    client_kwargs: dict[str, Any] = {}
    if socks_proxy:
        try:
            from httpx_socks import AsyncProxyTransport

            from jmcore.tor_isolation import normalize_proxy_url

            # python-socks does not support the socks5h:// scheme directly.
            # normalize_proxy_url converts socks5h:// -> socks5:// + rdns=True
            # so that .onion addresses are resolved by Tor.
            normalized = normalize_proxy_url(socks_proxy)

            transport = AsyncProxyTransport.from_url(normalized.url, rdns=normalized.rdns)
            client_kwargs["transport"] = transport
            logger.debug(
                "Update check configured with SOCKS proxy: %s (rdns=%s)",
                socks_proxy,
                normalized.rdns,
            )
        except ImportError:
            logger.warning("httpx-socks not available, update check without proxy")
        except Exception:
            logger.warning("Failed to configure SOCKS proxy for update check", exc_info=True)

    try:
        async with httpx.AsyncClient(
            timeout=timeout,
            follow_redirects=True,
            **client_kwargs,
        ) as client:
            response = await client.get(
                GITHUB_RELEASES_URL,
                headers={"Accept": "application/vnd.github+json"},
            )
            response.raise_for_status()

        data = response.json()
        tag_name: str = data["tag_name"]
        latest = _parse_version_tag(tag_name)
        current = get_version_tuple()
        latest_str = f"{latest[0]}.{latest[1]}.{latest[2]}"

        logger.debug("Update check: current=%s, latest=%s", __version__, latest_str)
        return UpdateCheckResult(latest_version=latest_str, is_newer=latest > current)

    except Exception:
        logger.warning("Failed to check for updates from GitHub", exc_info=True)
        return None

get_build_ref() -> str | None

Return the branch or tag the package was built from, if known.

Populated by setup.py from the JOINMARKET_BUILD_REF environment variable (set by install.sh) or, as a fallback, the local git branch/tag when building from a working tree. Non-editable installs that lack both leave this as None; callers should treat that as "unknown" rather than "stable".

Source code in jmcore/src/jmcore/version.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def get_build_ref() -> str | None:
    """Return the branch or tag the package was built from, if known.

    Populated by ``setup.py`` from the ``JOINMARKET_BUILD_REF`` environment
    variable (set by ``install.sh``) or, as a fallback, the local ``git``
    branch/tag when building from a working tree. Non-editable installs
    that lack both leave this as ``None``; callers should treat that as
    "unknown" rather than "stable".
    """
    build_info = _get_build_info_module()
    if build_info is not None:
        ref = getattr(build_info, "REF", "") or ""
        ref = ref.strip()
        if ref:
            return ref
    return None

get_commit_hash() -> str | None

Return the short git commit hash, or None if unavailable.

Resolution order:

  1. jmcore._build_info.COMMIT -- written at wheel build time by setup.py. This is the only source that survives non-editable installs (pip install git+..., Docker, release wheels) where the package directory has no .git.
  2. Live git rev-parse --short HEAD from the package directory. Works for editable installs (pip install -e) where the source tree retains its working .git.
  3. None when neither source produced a hash.
Source code in jmcore/src/jmcore/version.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def get_commit_hash() -> str | None:
    """Return the short git commit hash, or None if unavailable.

    Resolution order:

    1. ``jmcore._build_info.COMMIT`` -- written at wheel build time by
       ``setup.py``. This is the only source that survives non-editable
       installs (``pip install git+...``, Docker, release wheels) where
       the package directory has no ``.git``.
    2. Live ``git rev-parse --short HEAD`` from the package directory.
       Works for editable installs (``pip install -e``) where the source
       tree retains its working ``.git``.
    3. ``None`` when neither source produced a hash.
    """
    build_info = _get_build_info_module()
    if build_info is not None:
        commit = getattr(build_info, "COMMIT", "") or ""
        commit = commit.strip()
        if commit:
            return commit

    import subprocess
    from pathlib import Path

    try:
        result = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            capture_output=True,
            text=True,
            timeout=5,
            cwd=Path(__file__).parent,
        )
        if result.returncode == 0:
            return result.stdout.strip() or None
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        pass
    return None

get_version() -> str

Return the current version string.

Source code in jmcore/src/jmcore/version.py
28
29
30
def get_version() -> str:
    """Return the current version string."""
    return __version__

get_version_info() -> dict[str, str | int]

Return version information as a dictionary.

Source code in jmcore/src/jmcore/version.py
116
117
118
119
120
121
122
123
124
def get_version_info() -> dict[str, str | int]:
    """Return version information as a dictionary."""
    major, minor, patch = get_version_tuple()
    return {
        "version": __version__,
        "major": major,
        "minor": minor,
        "patch": patch,
    }

get_version_tuple() -> tuple[int, int, int]

Return the version as a tuple of (major, minor, patch).

Source code in jmcore/src/jmcore/version.py
110
111
112
113
def get_version_tuple() -> tuple[int, int, int]:
    """Return the version as a tuple of (major, minor, patch)."""
    parts = __version__.split(".")
    return (int(parts[0]), int(parts[1]), int(parts[2]))