Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/publish-prereleases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Publish Prerelease

on:
release:
types: [prereleased]

jobs:
publish:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
if: github.event.release.prerelease == true

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v3

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"

- name: Build package
run: uvx --with uv-dynamic-versioning hatchling build -d ./dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
attestations: true
1 change: 1 addition & 0 deletions .github/workflows/publish-releases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
environment: release
permissions:
id-token: write
if: github.event.release.prerelease == false

steps:
- uses: actions/checkout@v4
Expand Down
33 changes: 33 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false,
},
{
"name": "Python Debugger: invtk CLI",
"type": "debugpy",
"request": "launch",
"module": "invoke_toolkit.main",
"console": "integratedTerminal",
"justMyCode": false
},
{
"name": "Python Debugger: Test program (tests/program/main)",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/tests/program/main.py",
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/tests/program/",
"justMyCode": false
}
]
}
16 changes: 16 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"peacock.color": "#9d4640",
"ruff.format.preview": true,
"ruff.lint.preview": true,
"ruff.showNotifications": "off",
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"files.exclude": {
".venv": true,
".ruff_cache": true,
".pytest_cache": true
}
}
12 changes: 12 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "pre-commit",
"type": "shell",
"command": "pre-commit run --all-files"
}
]
}
18 changes: 18 additions & 0 deletions config_improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Config Improvements

## Goal

Make the `Config` base class compatible with `attrs` to allow for more structured and less human-readable configuration files, especially for nested configurations.

## Findings

* The current configuration class is `ToolkitConfig`, which inherits from `invoke.config.Config`.
* The `ToolkitContext` class uses `ToolkitConfig` to manage configuration.
* To add `attrs` compatibility, a new class `AttrsConfig` will be created.

## Plan

1. **Create `AttrsConfig`:** This new class will inherit from `ToolkitConfig` and use the `attrs` library to define a structured configuration. It will override `__getattr__` to recursively convert nested dictionaries into `AttrsConfig` instances, enabling dot-notation access.
2. **Integrate with `ToolkitContext`:** The `ToolkitContext` will be modified to use `AttrsConfig` instead of `ToolkitConfig`.
3. **Dependencies:** `attrs` and `cattrs` have been added to `pyproject.toml` and installed.
4. **Testing:** Add tests to verify the new `attrs`-based configuration works as expected.
53 changes: 53 additions & 0 deletions extensions/invoke-toolkit-mcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "invoke-toolkit-mcp"
version = "0.1.0"
description = "MCP (Model Context Protocol) server for invoke-toolkit collections"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
keywords = ["mcp", "model-context-protocol", "invoke", "fastmcp", "llm"]
authors = [
{ name = "Nahuel Defossé", email = "D3f0@users.noreply.github.com" },
]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Software Development :: Build Tools",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]

dependencies = [
"invoke-toolkit>=2.0.0", # Core dependency - will be satisfied by workspace
"fastmcp>=2.14.5,<3.0.0", # Pin to v2 for stability
]

[project.optional-dependencies]
dev = [
"pytest>=8.3.5",
"pytest-cov>=5.0.0",
"pytest-html>=4.1.1",
]

[project.scripts]
invoke-mcp = "invoke_toolkit_mcp.cli:main"

[project.urls]
Documentation = "https://github.com/D3f0/invoke-toolkit"
Issues = "https://github.com/D3f0/invoke-toolkit/issues"
Source = "https://github.com/D3f0/invoke-toolkit"

[tool.hatch.build.targets.wheel]
packages = ["src/invoke_toolkit_mcp"]
5 changes: 5 additions & 0 deletions local_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from invoke import task, Context


@task()
def im_local(ctx: Context) -> None: ...
74 changes: 74 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
site_name: Invoke Toolkit Documentation
site_description: A set of extended APIs for the venerable PyInvoke aimed at scripts composition, plugins and richer output
site_url: https://github.com/D3f0/invoke-toolkit
repo_url: https://github.com/D3f0/invoke-toolkit
repo_name: D3f0/invoke-toolkit

theme:
name: readthedocs
features:
- navigation.tabs
- navigation.sections
- navigation.expand
- navigation.top
- search.highlight
- search.share
- content.code.copy
palette:
- scheme: default
primary: blue
accent: blue
toggle:
icon: material/brightness-7
name: Switch to dark mode
- scheme: slate
primary: blue
accent: blue
toggle:
icon: material/brightness-4
name: Switch to light mode

plugins:
- search
- gen-files:
scripts:
- docs/gen_ref_pages.py
- literate-nav:
nav_file: SUMMARY.md
- section-index
- mkdocstrings:
handlers:
python:
options:
docstring_style: google
show_source: true
show_root_heading: true
show_root_toc_entry: false
merge_init_into_class: true

nav:
- Home: index.md
- User Guide:
- Installation: user-guide/installation.md
- Quick Start: user-guide/quickstart.md
- Configuration: user-guide/configuration.md
- Tasks:
- Overview: tasks/index.md
- Available Tasks: tasks/available.md
- API Reference: reference/
- Development:
- Contributing: development/contributing.md
- Testing: development/testing.md

# markdown_extensions:
# - admonition
# - pymdownx.details
# - pymdownx.superfences
# - pymdownx.highlight:
# anchor_linenums: true
# - pymdownx.inlinehilite
# - pymdownx.snippets
# - pymdownx.tabbed:
# alternate_style: true
# - toc:
# permalink: true
7 changes: 6 additions & 1 deletion src/invoke_toolkit/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,12 @@ def get_choices_for_argument(
if arg_name in callbacks:
try:
# Try to call the callback with context and incomplete
ctx = ToolkitContext(config=ToolkitConfig())
# Automatically load project config for completion callbacks
config = ToolkitConfig()
if hasattr(collection, "root") and collection.root:
config.set_project_location(collection.root)
config.load_project()
ctx = ToolkitContext(config=config)

# Get timeout from config (default: 10 seconds)
timeout = ctx.get_config_value(
Expand Down
7 changes: 7 additions & 0 deletions src/invoke_toolkit/config/attrs_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from attrs import define
from .config import ToolkitConfig


@define
class AttrsConfig(ToolkitConfig):
pass
89 changes: 89 additions & 0 deletions tests/examples/completion_config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Completion Config Example

This example demonstrates how completion callbacks can read values from configuration files using `ctx.get_config_value()`.

## Overview

Instead of hardcoding completion options, this example shows how to:
- Read completion values from `invoke.yaml` configuration
- Use `ctx.get_config_value()` method (no import needed)
- Provide fallback defaults when config is missing
- Share config values between tasks and completion callbacks

## Files

- `invoke.yaml` - Configuration file with completion values
- `tasks.py` - Tasks with completion callbacks that read from config

## Usage

```bash
# Navigate to this directory
cd tests/examples/completion_config

# View current config values
intk show-config

# Try tab completion (reads from invoke.yaml)
intk deploy --environment <TAB>
intk deploy --region <TAB>
intk connect-db --instance <TAB>
intk toggle-feature --flag <TAB>
```

## How It Works

### 1. Define completion callback that reads config

```python
def complete_environments(ctx: Context, incomplete: str) -> list[str]:
# Read from config - no import needed!
environments = ctx.get_config_value(
"deployment.environments",
default=["development", "staging", "production"],
)

if incomplete:
environments = [e for e in environments if e.startswith(incomplete)]

return sorted(environments)
```

### 2. Use the callback with Annotated type hint

```python
@task
def deploy(
ctx: Context,
environment: Annotated[str, complete_environments],
) -> None:
...
```

### 3. Configure values in invoke.yaml

```yaml
deployment:
environments:
- development
- staging
- production
- canary
```

## Config Sources

The `ctx.get_config_value()` method reads from the full config hierarchy:

1. **Project config** - `./invoke.yaml` in current directory
2. **User config** - `~/.invoke.yaml` in home directory
3. **System config** - `/etc/invoke.yaml`
4. **Environment variables** - `INVOKE_*` prefix
5. **Defaults** - Built-in toolkit defaults

## Benefits

- **Dynamic completions** - Options come from config, not code
- **User customizable** - Users can add/remove options via config
- **Shared values** - Same config used by tasks and completions
- **Fallback defaults** - Works even without config file
Loading
Loading