Metadata-Version: 2.4
Name: g-updatekit
Version: 0.1.0
Summary: A lightweight, importable update checker for Python desktop applications
Author: Ghostals
Keywords: updates,updater,version-checker,tkinter
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# UpdateKit

UpdateKit is a small, importable update checker for Python desktop applications.
It compares numeric version strings served as plain text, asks the user whether to
open an update page, and opens that page in the system browser when accepted. It
does **not** download, install, replace, or restart your application.

The project is named **UpdateKit** and is distributed as the `g-updatekit`
Python distribution. Import it in code as `updatekit`; the name used by `pip`
does not have to match the Python import name.

> **Publishing status:** this repository is ready to build as a Python package,
> but `pip install g-updatekit` from PyPI will only work after a release
> has been published there. Until then, install it from this checkout using the
> local-install commands below.

## How the pieces fit together

```mermaid
flowchart LR
    A["Your Python app"] -->|"import updatekit"| B["check_for_updates()"]
    B --> C["latest.txt<br/>2.4.0"]
    B --> D{"Is latest newer?"}
    D -->|"No"| E["Continue app"]
    D -->|"Yes"| F["Ask the user"]
    F -->|"Accept"| G["Read download.txt"]
    G --> H["Open HTTP(S) page in browser"]
    F -->|"Decline"| E
```

The endpoint files are ordinary UTF-8 text files. For example:

| URL file | Exact file contents | Purpose |
|---|---|---|
| `latest.txt` | `2.4.0` | Version available to install |
| `download.txt` | `https://example.com/downloads/my-app` | Page opened after user acceptance |

Host both over HTTP or HTTPS. Prefer HTTPS. The download target is deliberately a
human-facing page; your application remains in control of the actual installation.

## Requirements and behavior

- Python 3.10 or newer.
- No third-party runtime dependencies; the implementation uses the standard
  library, including Tkinter for the default prompt and `webbrowser` for opening
  the download page.
- Importing the module requires the Python Tkinter bindings because the default
  prompt and splash use them. GUI prompts additionally require a working
  desktop/Tk session. On many Linux systems, Tkinter is an optional OS package
  (often named `python3-tk`); install the OS package even if you plan to use a
  custom prompt callback.
- Network requests have a 10-second timeout and read at most 4 KiB from each
  text endpoint.
- Versions are numeric dot-separated components: `1.9 < 1.10`,
  `v2.0 == 2`, and trailing zeroes do not affect equality (`1.2 == 1.2.0`).
- Pre-release labels such as `2.0-rc1`, calendar versions containing letters,
  and arbitrary semantic-version syntax are not supported.
- If the server cannot be reached, the exact message
  `Servers unreachable. Unable to check for updates.` is printed, and the user
  is asked whether to continue without updating. The result records the choice.
- Invalid URLs, invalid version strings, malformed endpoint contents, and a
  browser-open failure are reported as exceptions rather than silently treated
  as a successful check.

## Install

### Install from a local checkout (recommended while developing)

Open a terminal in the directory containing `pyproject.toml` and run:

```console
python -m pip install -e .
```

`-e` means *editable*: Python imports the source in this checkout, so code edits
take effect without reinstalling. For a normal local installation that copies
the built module into the environment, omit `-e`:

```console
python -m pip install .
```

Run these commands in the same virtual environment that runs your application.
For example, create and activate one first:

```console
python -m venv .venv
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
# macOS/Linux
source .venv/bin/activate
python -m pip install -e .
```

### Install a published release

After the package has been uploaded to PyPI, users will install it with:

```console
python -m pip install g-updatekit
```

Until publication, the local-checkout command is the usable installation path.

## Import it in your application

The canonical import is:

```python
from updatekit import check_for_updates
```

Or import all supported public API names:

```python
from updatekit import (
    SERVERS_UNREACHABLE_MESSAGE,
    ServerUnreachableError,
    UpdateCheckResult,
    check_for_updates,
    compare_versions,
)
```

`UpdateCheckResult`, `check_for_updates`, `compare_versions`,
`SERVERS_UNREACHABLE_MESSAGE`, and `ServerUnreachableError` are exported names.
The high-level `check_for_updates` function handles endpoint reachability and
returns the outcome in its result, so applications normally do not need to catch
`ServerUnreachableError` themselves.

Do not copy `updatekit.py` into your application or import `sandbox.py`. Install
the distribution in the application's environment and import the module by its
module name. The root `main.py` is a compatibility shim for old source-checkout
examples and a launcher for this repository's developer sandbox; it is not the
package import name.

## Minimal integration

Publish `latest.txt` and `download.txt` as described above, then call the checker
near the start of your app's normal startup path:

```python
from updatekit import check_for_updates

VERSION = "1.0.0"
LATEST_VERSION_URL = "https://example.com/my-app/latest.txt"
DOWNLOAD_PAGE_FILE_URL = "https://example.com/my-app/download.txt"

result = check_for_updates(
    current_version=VERSION,
    latest_version_url=LATEST_VERSION_URL,
    download_url_file=DOWNLOAD_PAGE_FILE_URL,
    app_name="My application",
)

if not result.continue_application:
    raise SystemExit(0)

# Continue creating/showing your application here.
```

The default mode does not show a splash window. The update dialog is displayed
when one is needed; the connectivity-failure decision is also prompted. If an
update is accepted, the configured page opens in the browser and the function
returns. The function does not block your application until the user downloads
or installs anything.

For a command-line program, the default prompt still uses Tkinter. If the
application has no desktop session, supply `prompt_user` to adapt the decision
to your own interface or policy (see the callback example below).

## API reference

### `check_for_updates(...)`

```python
check_for_updates(
    current_version,
    latest_version_url,
    download_url_file,
    app_name="Application",
    prompt_user=None,
    open_download=None,
    show_splash=False,
)
```

| Parameter | Type | Meaning |
|---|---|---|
| `current_version` | `str` | Version of the running application, such as `"1.4.2"`. |
| `latest_version_url` | `str` | HTTP(S) URL whose response body is the latest numeric version. |
| `download_url_file` | `str` | HTTP(S) URL whose response body is the final HTTP(S) page URL. It is fetched only if an update is available and accepted. |
| `app_name` | `str` | Friendly name used in default dialog titles and messages. |
| `prompt_user` | `Callable[[str, str], bool] \| None` | Optional decision callback receiving `(title, message)` and returning `True` to accept/continue or `False` to decline/stop. |
| `open_download` | `Callable[[str], bool] \\| None` | Optional callback for opening a validated download URL. Return `True` on success; a false result raises `RuntimeError`. |
| `show_splash` | `bool` | Show the built-in checking window and in-window decision UI. Defaults to `False`. |

The function returns an immutable `UpdateCheckResult` with these fields:

| Field | Meaning |
|---|---|
| `current_version` | Version passed into the check. |
| `latest_version` | Version fetched from the server, or `""` if that first request could not be reached. |
| `update_available` | Whether the published version compares greater than the current version. |
| `opened_download` | Whether the update was accepted and the browser callback succeeded. |
| `servers_reachable` | Whether the necessary endpoint(s) could be reached. |
| `continue_application` | Whether the user's response to an unreachable-server prompt permits the app to continue. It is `True` for ordinary update/no-update results. |

Examples of interpreting the result:

```python
if not result.servers_reachable:
    log_warning("Update check could not reach its server")

if result.update_available and result.opened_download:
    log_info(f"Opened the page for version {result.latest_version}")

if not result.continue_application:
    close_application()
```

### `compare_versions(current_version, latest_version)`

Returns `-1` when the first version is older, `0` when they compare equal, and
`1` when it is newer. Numeric components are compared as integers, not strings.
Invalid values raise `ValueError`.

```python
from updatekit import compare_versions

assert compare_versions("1.9", "1.10") == -1
assert compare_versions("v2.0", "2") == 0
assert compare_versions("3.0.1", "3.0") == 1
```

### `UpdateCheckResult`

This frozen dataclass is returned by the main function. Read its fields as shown
above; it is not intended to be modified.

## Common integration patterns

### Start an existing Tkinter application

Call before `mainloop()` so the update prompt can be answered before the main
window appears. The checker owns and closes its temporary prompt/splash windows.

```python
import tkinter as tk
from updatekit import check_for_updates

result = check_for_updates(
    "1.4.2",
    "https://example.com/app/latest.txt",
    "https://example.com/app/download.txt",
    app_name="My Tk app",
    show_splash=True,
)
if not result.continue_application:
    raise SystemExit(0)

root = tk.Tk()
root.title("My Tk app")
root.mainloop()
```

### Use your own prompt UI

Provide a callback that returns a boolean. This is useful for applications with
an existing dialog framework or for tests. The callback is used both for an
available update and an unreachable-server decision. Inspect its `title` and
`message` to choose the appropriate UI:

```python
from tkinter import messagebox
from updatekit import check_for_updates

def ask_user(title: str, message: str) -> bool:
    return messagebox.askyesno(title, message)

result = check_for_updates(
    "1.0.0",
    LATEST_VERSION_URL,
    DOWNLOAD_PAGE_FILE_URL,
    app_name="My application",
    prompt_user=ask_user,
)
```

When a custom `prompt_user` is supplied with `show_splash=True`, the splash
continues to display the checking status while the custom callback handles the
decision. If you want the built-in in-window buttons, omit `prompt_user`.

### Inject a browser opener (tests or custom shell)

```python
def open_page(url: str) -> bool:
    # Replace with your app's browser integration.
    return my_shell_open(url)

result = check_for_updates(
    "1.0.0",
    LATEST_VERSION_URL,
    DOWNLOAD_PAGE_FILE_URL,
    open_download=open_page,
)
```

The target URL is validated as HTTP(S) before the callback runs. The callback
must return a truthy success value; returning `False` raises `RuntimeError`.

### Schedule around your application lifecycle

The checker is synchronous from the caller's perspective. It performs network
requests and waits for the user's decision before returning. Call it before
showing your main window or schedule it using the application's existing
background-task/event-loop pattern if startup must remain non-blocking. Do not
call Tkinter UI methods from a worker thread; Tkinter is generally expected to
be used on its owning UI thread.

## Endpoint publishing and validation

1. Create a UTF-8 plain-text `latest.txt`, for example containing exactly
   `1.4.2` (a trailing newline is fine).
2. Create a UTF-8 plain-text `download.txt` containing one complete HTTP(S) URL,
   for example `https://downloads.example.com/my-app`.
3. Host both files at stable HTTPS URLs with publicly readable responses.
4. Check that the version file is numeric and dot-separated; avoid labels such
   as `latest: 1.4.2`, `v1.4.2-beta`, or JSON.
5. Check that the download file contains the final page URL, not another text
   file URL. The checker follows this explicit two-file contract.
6. Use the developer sandbox (`python sandbox.py` from the checkout) to test
   reachable, unreachable, latest-version, and update-available cases before
   wiring it into a release.

The request implementation accepts HTTP and HTTPS endpoint URLs, caps response
size at 4096 bytes, decodes UTF-8 (including a UTF-8 BOM), strips surrounding
whitespace, and times out after 10 seconds. It does not currently support
authentication headers, JSON feeds, proxies configured by the library, or
background auto-installation.

## Local development and tests

```console
python -m pip install -e .
python -m unittest discover -v
python sandbox.py
```

Tests replace network and UI functions with fakes; they do not require a real
update server or actual user interaction.

## Build and publish

The package configuration is in `pyproject.toml`. The importable source is the
single top-level module `updatekit.py`; setuptools includes that module in the
wheel. The sandbox, tests, profile data, and the compatibility launcher are
development files and are not part of the installed distribution.

To create distribution archives:

```console
python -m pip install build twine
python -m build
python -m twine check dist/*
```

Before publishing, confirm that `g-updatekit` is available on PyPI, update the
version, project URLs, license choice, and maintainer information in
`pyproject.toml`. Uploading to PyPI makes the package public;
use a trusted publishing workflow or a securely configured token, never commit
credentials. Then publish the validated archives:

```console
python -m twine upload dist/*
```

After a release is published, users can install it with `python -m pip install
g-updatekit` and import it with `from updatekit import
check_for_updates`.
