Metadata-Version: 2.1
Name: wexample-wex-core
Version: 30.0.0
Summary: Provides the wex framework kernel, addon manager, YAML/Python command runners, and built-in CLI commands.
Author-Email: weeger <contact@wexample.com>
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Project-URL: homepage, https://github.com/wexample/wexample-wex-core
Requires-Python: >=3.10
Requires-Dist: attrs>=23.1.0
Requires-Dist: cattrs>=23.1.0
Requires-Dist: click
Requires-Dist: psutil>=5.9
Requires-Dist: wexample-app>=19.1.0
Requires-Dist: wexample-cli>=2.1.0
Requires-Dist: wexample-filestate-git>=8.1.0
Requires-Dist: wexample-filestate>=17.0.0
Requires-Dist: wexample-helpers-git>=7.1.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Description-Content-Type: text/markdown

# wex_core

Version: 30.0.0

`wexample-wex-core` is the kernel and command-dispatch layer of the `wex` CLI: it resolves the four command namespaces (`addon::group/name`, `.group/name`, `@addon::group/name`, `~group/name`), runs commands as Python functions or multi-step YAML scripts (bash, python, docker runners), and provides the `AbstractAddonManager` base that every higher-level package uses to register its own commands, resolvers, middlewares, and step guards. It is the shared foundation consumed by all other `wex` packages, not an end-user tool on its own.

## Table of Contents

- [Installation](#installation)
- [Quickstart](#quickstart)
- [Tests](#tests)
- [Architecture](#architecture)
- [Integration in the Suite](#integration-in-the-suite)
- [Dependencies](#dependencies)
- [Versioning & Compatibility Policy](#versioning--compatibility-policy)
- [License](#license)
- [About us](#about-us)
- [Known Limitations & Roadmap](#known-limitations--roadmap)
- [Status & Compatibility](#status--compatibility)
- [Useful Links](#useful-links)
- [Migration Notes](#migration-notes)

## Installation

```bash
pip install wexample-wex-core
```

Requires Python >=3.10.

## Quickstart

Install the package:

```bash
pip install wexample-wex-core
```

The snippet below builds a kernel, registers two addons, and executes the built-in `demo::ping/pong` command. It needs no pre-existing project on disk.

```python
import tempfile
from pathlib import Path

from wexample_app.const.output import OUTPUT_TARGET_NONE
from wexample_wex_core.addons.core.core_addon_manager import CoreAddonManager
from wexample_wex_core.addons.demo.demo_addon_manager import DemoAddonManager
from wexample_wex_core.common.command_request import CommandRequest
from wexample_wex_core.common.kernel import Kernel

with tempfile.TemporaryDirectory() as tmp:
    root = Path(tmp)
    wex_dir = root / "wex"
    wex_dir.mkdir()
    (root / ".env.yml").write_text("APP_ENV: test\n")

    kernel = Kernel(entrypoint_path=wex_dir)
    kernel.setup(addons=[CoreAddonManager, DemoAddonManager])

    request = CommandRequest(
        kernel=kernel,
        name="demo::ping/pong",
        output_target=[OUTPUT_TARGET_NONE],
        arguments={"type": "dict"},
    )

    response = kernel.execute_kernel_command(request)
    print(response.content)  # {'status': 'pong'}
```

`Kernel(entrypoint_path=...)` takes the `.wex/` directory itself — a `wex/` subdirectory of the project root. The `.env.yml` file lives one level up (at the project root) and must declare at least `APP_ENV`.

`CoreAddonManager` wires the built-in middlewares. `DemoAddonManager` registers the `demo` namespace, including `ping/pong`. A real application passes its own addon managers to `setup()` in place of or alongside these.

The `CommandRequest` fields that matter at call time:

| field | purpose |
|---|---|
| `name` | command address — `addon::group/name`, `.group/name`, `@addon::group/name`, or `~group/name` |
| `output_target` | where output goes: `OUTPUT_TARGET_NONE`, `OUTPUT_TARGET_STDOUT`, or `OUTPUT_TARGET_FILE` |
| `arguments` | option values keyed by option name |

## Tests

This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.

### Installation

First, install the required testing dependencies:
```bash
.venv/bin/python -m pip install pytest pytest-cov
```

### Basic Usage

Run all tests with coverage:
```bash
.venv/bin/python -m pytest --cov --cov-report=html
```

### Common Commands
```bash
# Run tests with coverage for a specific module
.venv/bin/python -m pytest --cov=your_module

# Show which lines are not covered
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing

# Generate an HTML coverage report
.venv/bin/python -m pytest --cov=your_module --cov-report=html

# Combine terminal and HTML reports
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html

# Run specific test file with coverage
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
```

### Viewing HTML Reports

After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.

### Coverage Threshold

To enforce a minimum coverage percentage:
```bash
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
```

This will cause the test suite to fail if coverage drops below 80%.

## Architecture

`wexample-wex-core` is the kernel and command-dispatch layer consumed by every other `wex` package. It owns four things: the `Kernel` that orchestrates startup and execution, the resolver chain that maps a command string to a file, the runner pair that executes that file, and the addon system that lets higher-level packages extend all of the above.

### Source layout

```
src/wexample_wex_core/
  common/           # Kernel, AbstractAddonManager, CommandRequest, CommandAddress, RegistryBuilder
  registry/         # KernelRegistry — the in-memory command map
  path/             # KernelRegistryFile — JSON persistence of the registry
  resolver/         # AbstractCommandResolver, AddonCommandResolver, UserCommandResolver
  runner/           # CorePythonCommandRunner, CoreYamlCommandRunner
  yaml/             # YamlCommandDefinition, AbstractScriptRunner and its four runners
  middleware/       # AbstractEachPathMiddleware, EachPathMiddleware, EachFileMiddleware, EachDirectoryMiddleware
  decorator/        # @command, @option, @alias, @attach, @as_sudo — applied to command functions
  context/          # ExecutionContext — passed to every command function
  workdir/          # KernelWorkdir, AddonWorkdir — file-state wrappers for .wex directories
  webhook/          # HTTP server, routing, token store, type resolvers
  addons/           # CoreAddonManager, DemoAddonManager, DockerAddonManager, GitAddonManager, SystemAddonManager
  const/            # globals.py — command patterns, separators, type names
  exception/        # Typed exceptions for option and command errors
```

### Kernel

src/wexample_wex_core/common/kernel.py is the root object. `Kernel.setup()` runs a fixed sequence of `_init_*` methods:

1. `_init_local_env` — loads `.wex/local/env.yml` into `os.environ`.
2. `_init_command_line_kernel` — parses core CLI flags (`--output-target`, `--output-format`, `--indentation-level`, …).
3. `_init_logging` — attaches a `WARNING`-level stderr logger named `wex`.
4. `_init_addons` — instantiates every `AbstractAddonManager` class passed to `setup()`.
5. `_auto_detect_env` — asks each addon for auto-detectable env vars (e.g. PATH entries) and persists found values back to `.wex/local/env.yml`.
6. `_init_resolvers` — builds `AddonCommandResolver`, `UserCommandResolver`, and any resolver classes contributed by addons.
7. `_init_runners` — builds `CorePythonCommandRunner` and `CoreYamlCommandRunner`.
8. `_init_middlewares` — collects middleware classes from every addon.
9. `_init_registry` — reads or builds `src/wexample_wex_core/path/kernel_registry_file.py` at `.wex/tmp/registry.json`. Live resolvers (user) are always re-scanned; addon resolver data is read from the cached file when present.
10. `_init_script_runner_registry` — registers `bash`, `docker`, `exec`, and `python` YAML step runners.
11. `_init_step_guard_registry` — collects `AbstractStepGuard` subclasses from addons.

`execute_kernel_command(request)` is the single dispatch entry point. It increments an execution-depth counter (used to gate sudo re-exec to depth 0), fires `before` attachment commands, delegates to the parent runner chain, and fires `after` / `always_after` attachments.

### Command namespaces

Four patterns, defined in src/wexample_wex_core/const/globals.py:

| Pattern | Example | Resolver |
|---|---|---|
| `addon::group/name` | `core::ping/hi` | `AddonCommandResolver` |
| `.group/name` | `.install/local` | app resolver (contributed by host) |
| `@addon::group/name` | `@nginx::status` | service resolver (contributed by host) |
| `~group/name` | `~my-group/my-cmd` | `UserCommandResolver` |

Unqualified `group/name` (no prefix) is resolved by scanning all loaded addon workdirs for a unique match; if ambiguous, it raises. Aliases declared with `@alias(…)` are resolved first by searching the registry before the pattern match.

A `CommandAddress` (src/wexample_wex_core/common/command_address.py) is the canonical three-tuple `(addon, group, name)`. It converts between every representation used internally: command string (`core::ping/hi`), Python function name (`core__ping__hi`), and file path (`commands/ping/hi.py`).

### Resolvers

src/wexample_wex_core/resolver/abstract_command_resolver.py defines `build_registry_data()`, `build_command_path()`, `build_command_function_name()`, and `is_live()`. `is_live()` returns `False` for addon commands (cached in the registry file) and `True` for user commands (always scanned fresh).

src/wexample_wex_core/resolver/addon_command_resolver.py walks each addon's `commands/` directory: for `.py` files it imports the module with `importlib` and reads the `CommandMethodWrapper`; for `.yml` files it reads the `decorators:` block. Both paths populate the same `RegistryCommandData` structure.

src/wexample_wex_core/resolver/user_command_resolver.py does the same scan against `~/.wex/commands/` and adds that directory to `sys.path` on first use.

### Registry

src/wexample_wex_core/registry/kernel_registry.py holds `_resolvers`, a nested dict `{resolver_key: {addon_name: {command_key: RegistryCommandData}}}`. `get_all_commands()` flattens it into a single keyed dict and caches the result. `serialize()` serialises non-live resolvers to JSON; `hydrate()` restores from the same JSON. The file is managed by src/wexample_wex_core/path/kernel_registry_file.py, which extends `JsonFile` from `wexample-filestate`.

Running `wex core::registry/build` (src/wexample_wex_core/addons/core/commands/registry/build.py) forces a full re-scan, overwrites the file, and writes `.wex/tmp/autocomplete.json` for shell completion.

### Runners

Two runners handle the two command file types:

**`CorePythonCommandRunner`** (src/wexample_wex_core/runner/core_python_command_runner.py) — calls the parent `PythonCommandRunner._build_command_function()` to import the module and fetch the decorated function, wraps it in an `ExtendedCommand`, and returns it. The function must be named `{addon}__{group}__{name}` and decorated with `@command(type=COMMAND_TYPE_ADDON)`.

**`CoreYamlCommandRunner`** (src/wexample_wex_core/runner/core_yaml_command_runner.py) — reads and caches `YamlCommandDefinition.from_path()`, synthesises a `CommandMethodWrapper` around a closure that iterates over `scripts:`, and returns an `ExtendedCommand`. Before each step runs, `${VAR_NAME}` placeholders are substituted from a merged dict of `.wex/local/env.yml`, `os.environ`, built-ins (`PATH_CURRENT`), and option values (uppercased). After substitution, `StepGuardRegistry.should_skip_step()` is checked; if the step has a `command:` key it dispatches a sub-request; otherwise it looks up the named script runner.

### YAML step runners

All four implement src/wexample_wex_core/yaml/abstract_script_runner.py:

| `runner:` key | Class | What it executes |
|---|---|---|
| `bash` | `BashScriptRunner` | `bash -c <script>` or `bash <file>` on the host |
| `python` | `PythonScriptRunner` | inline Python via `PythonScriptResponse` (executed in-process) |
| `docker` | `DockerScriptRunner` | `docker exec --env CI=true <container> bash -c <script>` |
| `exec` | `ExecScriptRunner` | arbitrary interpreter list, e.g. `[node, -e]` + script |

`DockerScriptRunner._resolve_service_name()` iterates addons calling `get_service_docker_container_name(service)`; the first non-`None` answer wins.

### Addons

Every addon is a subclass of src/wexample_wex_core/common/abstract_addon_manager.py. At construction its workdir is set to the directory containing the addon manager file, so `commands/` is always found relative to the package. The methods an addon can override:

- `get_command_resolver_classes()` — add resolver types (e.g. an app or service resolver).
- `get_middlewares_classes()` — add middleware types.
- `get_step_guard_classes()` — add `AbstractStepGuard` subclasses that can skip YAML steps.
- `get_local_configurable_keys()` — declare env vars the kernel should auto-detect.
- `get_service_docker_container_name(service)` — resolve a short Docker service name.
- `get_webhook_resolvers()` — register webhook type resolvers.

The five addons shipped in this package are `CoreAddonManager`, `DemoAddonManager`, `DockerAddonManager`, `GitAddonManager`, and `SystemAddonManager`. `CoreAddonManager` is the only one that contributes middlewares: `EachPathMiddleware`, `EachFileMiddleware`, and `EachDirectoryMiddleware`.

### Middlewares

src/wexample_wex_core/middleware/abstract_each_path_middleware.py is a middleware base that expands a `path` option into multiple `ExecutionContext` objects — one per matching file or directory — before the command function is called. Subclasses override `_should_process_item()` and `_should_explore_directory()` to filter by type. Options `recursive`, `expand_glob`, `recursion_limit`, and `should_exist` are declared as public attrs.

### Call path (Python command)

```
CLI argv
  → Kernel._build_command_requests_from_arguments()      # parse name + options
  → Kernel.execute_kernel_command(CommandRequest)
      → _enforce_sudo_if_needed()                        # re-exec under sudo if @as_sudo
      → _execute_attached(request, "before")             # before-hooks
      → AbstractKernel.execute_kernel_command()
          → AddonCommandResolver.supports()              # regex match; alias/unqualified fallback
          → CorePythonCommandRunner.will_run()           # check .py file exists
          → CorePythonCommandRunner.build_runnable_command()
              → import module, fetch CommandMethodWrapper
              → ExtendedCommand(kernel, wrapper)
          → ExtendedCommand.execute(context)
              → middleware chain → command function(context, **options)
      → _execute_attached(request, "after")
```

### Call path (YAML command)

```
  → CoreYamlCommandRunner.will_run()                     # check .yml file exists
  → CoreYamlCommandRunner.build_runnable_command()
      → YamlCommandDefinition.from_path()                # parse yaml once, cache
      → synthesise CommandMethodWrapper(_make_executor)
  → ExtendedCommand.execute()
      → _make_executor(**kwargs)
          for step in scripts:
              yaml_substitute_step(step, variables)      # ${VAR} replacement
              StepGuardRegistry.should_skip_step()
              if "command": sub-request → execute_kernel_command()
              else: ScriptRunner.run(step, variables)    # bash / python / docker / exec
```

### Webhook server

`core::webhook/listen` starts a `ThreadingHTTPServer` (src/wexample_wex_core/webhook/handler.py) on a configurable port. Incoming GET requests are validated by `routing_is_allowed_route()`, then matched to a route (`/webhook/{type}/{path}`, `/health`, `/metrics`). Token validation uses HMAC comparison. Matched requests spawn a `wex core::webhook/exec --command-str …` subprocess; synchronous calls wait for its output, async calls return immediately with the PID. Prometheus-format metrics are accumulated in-process across requests.

### Key types

- `CommandRequest` — `(kernel, name, arguments, output_target, request_id)`. The `name` field is rewritten in-place by resolvers when an alias or unqualified form is found.
- `CommandAddress` — frozen dataclass `(addon, group, name)` with conversion helpers.
- `KernelRegistry` — in-memory command map; serialisable to JSON.
- `RegistryCommandData` — typed dict: `command`, `path`, `test`, `description`, `alias`, `attachments`, `options`, `tags`, `sudo`, `webhook`.
- `ExecutionContext` — carries `kernel`, `request`, `command_wrapper`, `function_kwargs`, and `middleware` into every command function.

## Integration in the Suite

This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.

### Related Packages

The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.

Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.

## Dependencies

- attrs: >=23.1.0
- cattrs: >=23.1.0
- click: 
- psutil: >=5.9
- wexample-app: >=19.1.0
- wexample-cli: >=2.1.0
- wexample-filestate-git: >=8.1.0
- wexample-filestate: >=17.0.0
- wexample-helpers-git: >=7.1.0

## Versioning & Compatibility Policy

Wexample packages follow **Semantic Versioning** (SemVer):

- **MAJOR**: Breaking changes
- **MINOR**: New features, backward compatible
- **PATCH**: Bug fixes, backward compatible

We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

Free to use in both personal and commercial projects.

## About us

[Wexample](https://wexample.com) stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.

This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.

Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.

## Known Limitations & Roadmap

Current limitations and planned features are tracked in the GitHub issues.

See the [project roadmap](https://github.com/wexample/python-wex_core/issues) for upcoming features and improvements.

## Status & Compatibility

**Maturity**: Production-ready

**Python Support**: >=3.10

**OS Support**: Linux, macOS, Windows

**Status**: Actively maintained

## Useful Links

- **Homepage**: https://github.com/wexample/python-wex-core
- **Documentation**: [docs.wexample.com](https://docs.wexample.com)
- **Issue Tracker**: https://github.com/wexample/python-wex-core/issues
- **Discussions**: https://github.com/wexample/python-wex-core/discussions
- **PyPI**: [pypi.org/project/wexample-wex-core](https://pypi.org/project/wexample-wex-core/)

## Migration Notes

When upgrading between major versions, refer to the migration guides in the documentation.

Breaking changes are clearly documented with upgrade paths and examples.
