Metadata-Version: 2.3
Name: edwh
Version: 1.17.4
Summary: Education Warehouse maintenance tools
Keywords: 
Author: Remco Boerma, Robin van der Noord, Romy Schöller, Sven Keimpema
Author-email: Remco Boerma <remco.b@educationwarehouse.nl>, Robin van der Noord <robin.vdn@educationwarehouse.nl>, Romy Schöller <romy.s@educationwarehouse.nl>, Sven Keimpema <sven.k@educationwarehouse.nl>
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Dist: anyio<5
Requires-Dist: click<9
Requires-Dist: diceware<2
Requires-Dist: ewok>=0.5.3,<1
Requires-Dist: humanize<5
Requires-Dist: more-itertools
Requires-Dist: docstring-parser<1
Requires-Dist: keyring<26
Requires-Dist: packaging
Requires-Dist: plumbum<2
Requires-Dist: python-dateutil<3
Requires-Dist: python-dotenv<2
Requires-Dist: pyyaml<7
Requires-Dist: rapidfuzz<4
Requires-Dist: requests<3
Requires-Dist: rich<16
Requires-Dist: ruff==0.16.8
Requires-Dist: ssh-agent-keyring>=0.2,<1
Requires-Dist: tabulate>=0.9,<0.10
Requires-Dist: termcolor<3
Requires-Dist: ty==0.0.82
Requires-Dist: threadful>=0.5,<1
Requires-Dist: tomlkit<1
Requires-Dist: tqdm<5
Requires-Dist: typing-extensions<5
Requires-Dist: uv<1
Requires-Dist: yayarl<3
Requires-Dist: edwh-b2-plugin ; extra == 'b2'
Requires-Dist: edwh-bundler-plugin ; extra == 'bundle'
Requires-Dist: edwh-bundler-plugin ; extra == 'bundler'
Requires-Dist: hatch ; extra == 'dev'
Requires-Dist: pytest ; extra == 'dev'
Requires-Dist: pytest-cov ; extra == 'dev'
Requires-Dist: types-requests ; extra == 'dev'
Requires-Dist: types-python-dateutil ; extra == 'dev'
Requires-Dist: types-redis ; extra == 'dev'
Requires-Dist: types-tabulate ; extra == 'dev'
Requires-Dist: vommit>=0.2.0,<1 ; extra == 'dev'
Requires-Dist: edwh-devdb-plugin ; extra == 'devdb'
Requires-Dist: edwh-files-plugin ; extra == 'files'
Requires-Dist: edwh-git-plugin ; extra == 'git'
Requires-Dist: edwh-locust-plugin ; extra == 'locust'
Requires-Dist: edwh-multipass-plugin ; extra == 'multipass'
Requires-Dist: edwh-passbolt-plugin ; extra == 'passbolt'
Requires-Dist: edwh-pipcompile-plugin ; extra == 'pip'
Requires-Dist: edwh-pipcompile-plugin ; extra == 'pip-compile'
Requires-Dist: edwh-b2-plugin>=0.2.5 ; extra == 'plugins'
Requires-Dist: edwh-bundler-plugin ; extra == 'plugins'
Requires-Dist: edwh-demo-plugin ; extra == 'plugins'
Requires-Dist: edwh-devdb-plugin ; extra == 'plugins'
Requires-Dist: edwh-files-plugin ; extra == 'plugins'
Requires-Dist: edwh-git-plugin ; extra == 'plugins'
Requires-Dist: edwh-locust-plugin ; extra == 'plugins'
Requires-Dist: edwh-multipass-plugin ; extra == 'plugins'
Requires-Dist: edwh-passbolt-plugin ; extra == 'plugins'
Requires-Dist: edwh-pipcompile-plugin ; extra == 'plugins'
Requires-Dist: edwh-restic-plugin ; extra == 'plugins'
Requires-Dist: edwh-server-provisioning-plugin ; extra == 'plugins'
Requires-Dist: edwh-sshfs-plugin ; extra == 'plugins'
Requires-Dist: edwh-sshkey-plugin ; extra == 'plugins'
Requires-Dist: edwh-uptime-plugin ; extra == 'plugins'
Requires-Dist: vommit ; extra == 'plugins'
Requires-Dist: edwh-restic-plugin ; extra == 'restic'
Requires-Dist: edwh-b2-plugin>=0.2.5 ; extra == 'server-plugins'
Requires-Dist: edwh-bundler-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-devdb-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-files-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-pipcompile-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-restic-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-uptime-plugin ; extra == 'server-plugins'
Requires-Dist: edwh-server-provisioning-plugin ; extra == 'server-provisioning'
Requires-Dist: edwh-sshfs-plugin ; extra == 'sshfs'
Requires-Dist: edwh-sshkey-plugin ; extra == 'sshkey'
Requires-Dist: pytest ; extra == 'test'
Requires-Dist: pytest-cov ; extra == 'test'
Requires-Dist: edwh-uptime-plugin ; extra == 'uptime'
Requires-Dist: vommit>=0.2.0,<1 ; extra == 'vommit'
Requires-Python: >=3.12
Project-URL: Documentation, https://github.com/educationwarehouse/edwh#readme
Project-URL: Issues, https://github.com/educationwarehouse/edwh/issues
Project-URL: Source, https://github.com/educationwarehouse/edwh
Provides-Extra: b2
Provides-Extra: bundle
Provides-Extra: bundler
Provides-Extra: dev
Provides-Extra: devdb
Provides-Extra: files
Provides-Extra: git
Provides-Extra: locust
Provides-Extra: multipass
Provides-Extra: omgeving
Provides-Extra: passbolt
Provides-Extra: pip
Provides-Extra: pip-compile
Provides-Extra: plugins
Provides-Extra: restic
Provides-Extra: server-plugins
Provides-Extra: server-provisioning
Provides-Extra: sshfs
Provides-Extra: sshkey
Provides-Extra: test
Provides-Extra: uptime
Provides-Extra: vommit
Description-Content-Type: text/markdown

# edwh

[![PyPI - Version](https://img.shields.io/pypi/v/edwh.svg)](https://pypi.org/project/edwh)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/edwh.svg)](https://pypi.org/project/edwh)

-----

**Table of Contents**

- [Installation](#installation)
- [Usage](#usage)
- [Sudo authentication](#sudo-authentication)
- [Linting](#linting)
- [Testing](#testing)
- [Plugins](#plugins)
- [License](#license)
- [Changelog](#changelog)

## Installation

```console
pipx install edwh
# or: uvenv install edwh

# or with all plugins:
pipx install[plugins]
# or with specific plugins: 
pipx install[multipass,restic]

# managing plugins later:
edwh plugins
edwh plugin.add multipass
edwh plugin.remove multipass
```

## Usage

```console
# to see all available commands:
ew # or `edwh`
# to see help about a specific namespace:
ew help <namespace> # e.g. `ew help plugin`
# to see help about a specific command:
ew help <command> # e.g. `ew help plugin.list` 
```

## Sudo authentication

`edwh sudo` verifies your sudo password and safely stores it temporarily so commands that require sudo can run without
prompting again.

By default, EDWH uses the operating system keyring (for example, GNOME Secret Service on Linux). This is the preferred
backend when the desktop session and its keyring are available.

When the system keyring is locked, `edwh sudo` offers an SSH-agent fallback. If you accept, EDWH lists the public keys
available through `ssh-add -L`; select the key whose agent should unlock the encrypted EDWH keyring. The selection is
recorded in `~/.config/ssh-agent-keyring/config.json`, the SSH-agent backend's documented configuration file, and later
EDWH commands automatically use that backend.

The SSH-agent backend requires a reachable agent, `SSH_AUTH_SOCK`, and the selected key loaded in that agent. For a
remote development machine, connect with agent forwarding (`ssh -A` or `ForwardAgent yes`). The sudo password remains
encrypted on disk; access depends on the selected SSH agent rather than a second keyring password.

## Linting

`edwh lint` runs Ruff and Ty by default. Disable either tool for a specific project in its `pyproject.toml`:

```toml
[tool.edwh.lint]
ruff = false
ty = false
```

`edwh fmt` sorts imports and reformats Python code with Ruff.

## Releasing plugins

`edwh plugin.release` warns when a project still releases with
python-semantic-release, and when the `--hatch` build fallback is selected.
Silence either for a project:

```toml
[tool.edwh.release]
warn-psr = false
warn-hatch-build = false
```

Warnings about the build configuration itself (Hatchling, the `uv_build` pin, a
`hatch build` release command) come from
[vommit](https://github.com/educationwarehouse/vommit), which runs the build. Its
readme documents them under "Build checks", including how to silence one. When
using python-semantic-release they are not reported at all; that prints its
deprecation notice instead.

## Testing

`edwh test.run` runs pytest with coverage by default. It prefers `./venv/bin/pytest`, falls back to
`pytest` from `PATH`, and requires `pytest-cov` in the selected environment.

```console
edwh test.run
edwh test.run -k test_specific_behavior
edwh test.run --html
edwh test.run --no-coverage
```

To generate both the terminal summary and an HTML report on every run, add this
to `pyproject.toml`:

```toml
[tool.edwh.test]
html = true
```

If a project provides its own `test.tasks.py`, edwh warns about the override and that local `test.run`
replaces the built-in task.

## Worktrees

`edwh worktree <branch>` builds a second, isolated environment for a branch: a git worktree plus the
gitignored config, its own ports and hostnames, and a seeded database. `edwh worktree.rm <branch>`
removes it again, containers and volumes included. If no local branch exists, it checks fetched
upstream branches first and creates a tracking branch when it finds one.

```bash
edwh worktree.setup                  # configure this project once (writes [worktree] to .toml)
edwh worktree feature/login          # create + start
edwh worktree feature/login --no-up  # create, do not start
edwh worktree.list                   # branches, slugs, paths, ports, running containers
cd $(edwh worktree.path feature/login)
edwh worktree.rm feature/login       # containers, volumes, directory and branch
```

Worktrees live under `~/.cache/edwh/worktrees/<repo>-<slug>`; override with `$EDWH_WORKTREE_ROOT`
or `root` in the config. The slugified directory name keeps `$PROJECT`, the compose project and the
volume/container prefixes identical.

### How it works

`worktree` copies the `.env`, deletes the keys that must be unique, and reruns
`edwh setup --non-interactive` so your `local.setup` regenerates them via `next_value` /
`next_available_port`. Port allocation also scans `git worktree list`, so environments find each
other even though they are not adjacent directories.

### Configuration

```toml
[worktree]
copy = [".env", ".toml", "shared_keys/"]   # gitignored paths to carry over
reset = ["*_PORT", "SCHEMA_VERSION"]       # fnmatch globs; deleted so local.setup recomputes them
seed = "clone"                             # fresh | clone | devdb

[worktree.env]
# rewritten instead of regenerated; {value} {repo} {branch} {slug} available. All fields have defaults.
PROJECT = "{repo}-{slug}"
```

`worktree.setup` proposes both lists: `copy` comes from `.gitignore`, and `reset` is detected from
keys on the host side of a `ports:` mapping, a Traefik `Host()` rule, a Caddy `caddy` site-address
label, or `HOSTINGDOMAIN(S)`, plus keys with a unique value in every existing checkout on this
machine. Values shared by any checkout are otherwise left unticked, since they are likely shared or
machine-specific config.
`COMPOSE_PROJECT_NAME` is always reset, since a copied value would fuse the environments. Ticked
keys collapse back to a glob when the glob covers exactly your selection.

### reset or template?

Resetting only produces a new value when `local.setup`'s default is environment-aware
(`next_value`, ports, `os.getcwd()`). With a constant default like `"localhost"` it silently
rewrites the same value, and both environments share it. Those keys need a `[worktree.env]`
template instead; after `setup`, `worktree` warns about any reset that came back identical.

`HOSTINGDOMAIN` and `HOSTINGDOMAINS` are the exception: when either is in `reset`, `worktree`
asks for its replacement before it creates anything. It never copies the source value or accepts
an empty answer. Scripts can provide the answer with `--env HOSTINGDOMAIN=branch.localhost`
(repeat `--env` for each requested hostname key). Do not also put a prompted hostname in
`[worktree.env]`: that section is only for automatic templates.

Seeding:

* `fresh` - leave it empty and let `migrate` fill it.
* `clone` - copy the source's named docker volumes via a throwaway container each. Project-agnostic
  and current data, but services mounted on a copied volume pause in the source, so `worktree` asks
  first unless `--yes`. Note it copies everything: N worktrees means N copies of the full volume.
* `devdb` - run `devdb.recover` after `up` using the trimmed `edwh-devdb-plugin` snapshot (put
  `migrate/data/snapshot/` in `copy`). Postgres only, much smaller than a clone.

After seeding, a `worktree` task in your project's `tasks.py` runs last, inside the new worktree
(e.g. `c.run("./bin/load-fixtures")`).

### Hostnames

Traefik routes on `Host()`, so overlapping hostnames make it pick a router at random, breaking both
environments. Rewriting `HOSTINGDOMAIN` covers all rules, but needs wildcard DNS one label deeper
and therefore a wildcard certificate (or `CERTRESOLVER=default` locally).

`worktree` compares the traefik `Host()` labels of the new environment against every other
checkout and refuses to `up` on an overlap. `--force` starts it anyway.

## Task Load Order

Commands are loaded in the following order:

1. **EDWH Package**:
    - Loaded into the global namespace and its own namespaces (like `edwh plugins.`).

2. **Plugins**:
    - Loaded into their own namespaces (like `edwh mp.`).

3. **Current Directory**:
    - Loaded into the `local.` namespace. If it doesn't exist, it traverses up the directory tree
    - (e.g., `../tasks.py`, `../../tasks.py`).

4. **Other Local Tasks**:
    - Other local tasks with their own namespace are loaded (e.g., `namespace.tasks.py`) and can be invoked
      with `edwh namespace.command`.

5. **Personal Global Tasks**:
    - Personal global tasks (e.g., `~/.config/edwh/tasks.py`) are also loaded into the **global** namespace, useful for
      shortcuts, custom aliases, etc. (+ `add_alias`).

6. **Personal Namespaced Tasks**:
    - Personal tasks with their own namespace (e.g., `~/.config/edwh/namespace.tasks.py`). Similar to a plugin, but for
      personal use.

## Plugins

### Multipass

- pip name: [`edwh-multipass-plugin`](https://pypi.org/project/edwh-multipass-plugin/)
- github: [`educationwarehouse/edwh-multipass-plugin`](https://github.com/educationwarehouse/edwh-multipass-plugin)
- plugin name: `edwh[multipass]`
- subcommand namespace: `mp`

### Restic

- pip name: [`edwh-restic-plugin`](https://pypi.org/project/edwh-restic-plugin/)
- github: [`educationwarehouse/edwh-restic-plugin`](https://github.com/educationwarehouse/edwh-restic-plugin)
- plugin name: `edwh[restic]`
- subcommand namespace: `restic`

### Pip Compile

- pip name: [`edwh-pipcompile-plugin`](https://pypi.org/project/edwh-pipcompile-plugin/)
- github: [`educationwarehouse/edwh-pipcompile-plugin`](https://github.com/educationwarehouse/edwh-pipcompile-plugin)
- plugin name: `edwh[pip]`
- subcommand namespace: `pip`

### Bundler

- pip name: [`edwh-bundler-plugin`](https://pypi.org/project/edwh-bundler-plugin/)
- github: [`educationwarehouse/edwh-bundler-plugin`](https://github.com/educationwarehouse/edwh-bundler-plugin)
- plugin name: `edwh[bundler]`
- subcommand namespace: `bundle`

### Server Provisioning

- pip name: [`edwh-server-provisioning-plugin`](https://pypi.org/project/edwh-server-provisioning-plugin/)
- github: [`educationwarehouse/server_provisioning`](https://github.com/educationwarehouse/server_provisioning)
- plugin name: `edwh[server-provisioning]`
- subcommand namespace: `remote`

### b2

- pip name: [`edwh-b2-plugin`](https://pypi.org/project/edwh-b2-plugin/)
- github: [`educationwarehouse/edwh-b2-plugin`](https://github.com/educationwarehouse/edwh-b2-plugin)
- plugin name: `edwh[b2]`
- subcommand namespace: `b2`

### Locust

- pip name: [`edwh-locust-plugin`](https://pypi.org/project/edwh-locust-plugin/)
- github: [`educationwarehouse/edwh-locust-plugin`](https://github.com/educationwarehouse/edwh-locust-plugin)
- plugin name: `edwh[locust]`
- subcommand namespace: `locust`

### sshkey

- pip name: [`edwh-sshkey-plugin`](https://pypi.org/project/edwh-sshkey-plugin)
- github: [`educationwarehouse/edwh-sshkey-plugin`](https://github.com/educationwarehouse/edwh-sshkey-plugin)
- plugin name `edwh[sshkey]`
- subcommand namespace `sshkey`

### sshfs

- pip name: [`edwh-sshfs-plugin`](https://pypi.org/project/edwh-sshfs-plugin)
- github: [`educationwarehouse/edwh-sshfs-plugin`](https://github.com/educationwarehouse/edwh-sshfs-plugin)
- plugin name `edwh[sshfs]`
- subcommand namespace `sshfs`

### files

- pip name: [`edwh-files-plugin`](https://pypi.org/project/edwh-files-plugin)
- github: [`educationwarehouse/edwh-files-plugin`](https://github.com/educationwarehouse/edwh-files-plugin)
- plugin name `edwh[files]`
- subcommand namespace `file`

### whitelabel

- pip name: [`edwh-whitelabel-plugin`](https://pypi.org/project/edwh-whitelabel-plugin)
- github: [`educationwarehouse/edwh-whitelabel-plugin`](https://github.com/educationwarehouse/edwh-whitelabel-plugin)
- plugin name `edwh[whitelabel]`
- subcommand namespace `wl`

### devdb

- pip name: [`edwh-devdb-plugin`](https://pypi.org/project/edwh-whitelabel-plugin)
- github: [`educationwarehouse/edwh-devdb-plugin`](https://github.com/educationwarehouse/edwh-whitelabel-plugin)
- plugin name `edwh[devdb]`
- subcommand namespace `devdb`

## Improvements to `@task`

The `edwh.improved_task` decorator enhances the functionality of the standard `@task` decorator from Invoke by
introducing additional features:

- **Flags**: You can now specify custom flags for command line arguments. This allows you to define aliases, rename
  arguments (e.g., using `--json` for an argument named `as_json`), and create custom short flags (e.g., `--exclude` can
  also be represented as `-x`).

- **Hookable**: The improved task supports hooks that allow you to run additional tasks after the main task
  execution. If the `hookable` option is set to `True`, any tasks found across namespaces with the same name will be
  executed in sequence, passing along the context and any provided arguments.

The return value of a hookable task will be available in the context under the key `result`.
Using a dictionary as the return value is recommended, as it allows you to merge the results of multiple cascading
tasks.

### Example Usage

```python
from edwh import improved_task as task


@task(flags={"exclude": ["--exclude", "-x"], "as_json": ["--json"]}, hookable=True)
def process_data(ctx, exclude: str, as_json: bool = False):
    # Task implementation here
    return {
        "data": [],
    }


# other plugin (or local tasks.py) can now also specify 'process_data':
@task()
def process_data(ctx, exclude: str):
    # the cascading function can choose whether to include the arguments `exclude` and `as_json` or not.
    # this can be cherry-picked as long as the names match the arguments of the main function.
    print(
        ctx["result"]  # will contain {"data": []}
    )
```

## License

`edwh` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.

## Changelog

[See CHANGELOG.md](CHANGELOG.md)
