diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..afe0a42 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,84 @@ +name: Release + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + github_release: + name: GitHub Release + runs-on: ubuntu-latest + outputs: + new_release_published: ${{ steps.semantic.outputs.new_release_published }} + new_release_version: ${{ steps.semantic.outputs.new_release_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "lts/*" + + - name: Install semantic-release plugins + run: | + npm install -g semantic-release \ + @semantic-release/commit-analyzer \ + @semantic-release/release-notes-generator \ + @semantic-release/exec \ + @semantic-release/github \ + @semantic-release/git \ + conventional-changelog-conventionalcommits + + - name: Semantic Release (dry-run) + env: + GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} + run: npx semantic-release --dry-run + + - name: Semantic Release + id: semantic + env: + GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} + run: npx semantic-release + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: github_release + if: needs.github_release.outputs.new_release_published == 'true' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: Pull latest changes + run: git pull origin main + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install build tools + run: pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..983ddeb --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + push: + branches: ["*"] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: | + python -m pip install --upgrade pip + python -m pip install uv + uv tool install tox --with tox-uv + + - name: Run tox + run: tox diff --git a/.releaserc.yaml b/.releaserc.yaml new file mode 100644 index 0000000..cc3c4a2 --- /dev/null +++ b/.releaserc.yaml @@ -0,0 +1,77 @@ +branches: + - main + +tagFormat: ${version} + +plugins: + - - "@semantic-release/commit-analyzer" + - preset: conventionalcommits + releaseRules: + - type: feat + release: minor + - type: fix + release: patch + - type: perf + release: patch + - type: docs + release: patch + - type: refactor + release: patch + - type: style + release: patch + - type: ci + release: patch + - type: build + release: patch + - type: chore + release: patch + - type: test + release: patch + + - - "@semantic-release/release-notes-generator" + - preset: conventionalcommits + presetConfig: + types: + - type: feat + section: "Features" + hidden: false + - type: fix + section: "Bug Fixes" + hidden: false + - type: perf + section: "Performance" + hidden: false + - type: docs + section: "Documentation" + hidden: false + - type: refactor + section: "Refactoring" + hidden: false + - type: style + section: "Style" + hidden: false + - type: ci + section: "CI/CD" + hidden: false + - type: build + section: "Build" + hidden: false + - type: chore + section: "Chores" + hidden: false + - type: test + section: "Tests" + hidden: false + + - - "@semantic-release/exec" + - prepareCmd: "sed -i 's/^version = .*$/version = \"${nextRelease.version}\"/' pyproject.toml" + + - - "@semantic-release/github" + - releaseNameTemplate: "Version ${nextRelease.version}" + + - - "@semantic-release/git" + - assets: + - pyproject.toml + message: "chore(release): ${nextRelease.version}" + +repositoryUrl: "https://github.com/mloda-ai/mloda-plugin-template" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..730b898 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# AGENTS.md + +This file provides guidance to Claude Code when working with this repository. + +**Required reading:** Must start by reading [README.md](README.md) for project overview, structure, key files, and common tasks. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7134c2d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# CLAUDE.md + +Must follow [AGENTS.md](AGENTS.md) for AI guidance and documentation links. diff --git a/README.md b/README.md index 21d59fe..6920f5e 100644 --- a/README.md +++ b/README.md @@ -1 +1,135 @@ -# mloda-plugin-template \ No newline at end of file +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/mloda-ai/mloda-plugin-template/blob/main/LICENSE) +[![mloda](https://img.shields.io/badge/built%20with-mloda-blue.svg)](https://github.com/mloda-ai/mloda) +[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/) + +# mloda-plugin-template + +> **A GitHub template for creating standalone mloda plugins.** Part of the [mloda](https://github.com/mloda-ai/mloda) ecosystem for open data access. Visit [mloda.ai](https://mloda.ai) for an overview and business context, the [GitHub repository](https://github.com/mloda-ai/mloda) for technical context, or the [documentation](https://mloda-ai.github.io/mloda/) for detailed guides. + +Create your own FeatureGroups, ComputeFrameworks, and Extenders as standalone packages. See the [Getting Started guide](docs/getting-started.md) to create your repository, then follow the setup steps below. + +## Related Repositories + +- **[mloda](https://github.com/mloda-ai/mloda)**: The core library for open data access. Declaratively define what data you need, not how to get it. mloda handles feature resolution, dependency management, and compute framework abstraction automatically. + +- **[mloda-registry](https://github.com/mloda-ai/mloda-registry)**: The central hub for discovering and sharing mloda plugins. Browse community-contributed FeatureGroups, find integration guides, and publish your own plugins for others to use. + +## Structure + +``` +placeholder/ +├── feature_groups/ +│ └── my_plugin/ +│ ├── __init__.py # Package exports +│ ├── my_feature_group.py # Example FeatureGroup implementation +│ └── tests/ +│ └── test_my_feature_group.py +├── compute_frameworks/ +│ └── my_framework/ +│ ├── __init__.py +│ └── my_compute_framework.py +└── extenders/ + └── my_extender/ + ├── __init__.py + └── my_extender.py +``` + +## Key Files + +- `placeholder/` - Root namespace (users rename to company name) +- `pyproject.toml` - Package config (users edit directly, not auto-generated) +- `.github/workflows/test.yml` - CI workflow running pytest + +## Common Tasks + +### Setup Your Plugin + +Follow these steps to customize the template for your organization: + +#### 1. Rename the directory + +```bash +mv placeholder acme +``` + +#### 2. Update pyproject.toml + +Edit the following fields in `pyproject.toml`: + +- `name`: Change `"placeholder-my-plugin"` to `"acme-my-plugin"` +- `authors`: Update name and email +- `description`: Update to describe your plugin +- `tool.setuptools.packages.find.include`: Change `["placeholder*"]` to `["acme*"]` +- `tool.pytest.ini_options.testpaths`: Change `["placeholder", "tests"]` to `["acme", "tests"]` + +#### 3. Update Python imports + +Update imports in these files (change `from placeholder.` to `from acme.`): + +- `acme/feature_groups/my_plugin/__init__.py` +- `acme/feature_groups/my_plugin/tests/test_my_feature_group.py` +- `acme/compute_frameworks/my_plugin/__init__.py` +- `acme/compute_frameworks/my_plugin/tests/test_my_compute_framework.py` +- `acme/extenders/my_plugin/__init__.py` +- `acme/extenders/my_plugin/tests/test_my_extender.py` + +#### 4. Verify setup + +```bash +uv venv && source .venv/bin/activate && uv pip install -e ".[dev]" && tox +``` + +### Development Setup with uv + +**Install uv** (if not already installed): +```bash +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + +**Create virtual environment and install dependencies:** +```bash +uv venv +source .venv/bin/activate +uv pip install -e ".[dev]" +``` + +**Run all checks with tox:** +```bash +# Install tox with uv backend +uv tool install tox --with tox-uv + +# Run all checks (pytest, ruff, mypy, bandit) +tox +``` + +### Run individual checks + +```bash +# Tests only +pytest + +# Format check +ruff format --check --line-length 120 . + +# Lint check +ruff check . + +# Type check +mypy --strict --ignore-missing-imports . + +# Security check +bandit -c pyproject.toml -r -q . +``` + +### Add new FeatureGroup +Create new directory under `placeholder/feature_groups/` following the `my_plugin/` pattern. + +## Related Documentation + +Guides for plugin development can be found in mloda-registry: + +- https://github.com/mloda-ai/mloda-registry/tree/main/docs/guides/ + +## Architecture Overview + +- [Repository structure and relationships](https://github.com/mloda-ai/mloda-registry/blob/main/docs/architecture/00_repositories.md) diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..cc82e47 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,50 @@ +# Getting Started + +There are two ways to create your own mloda plugin from this template. + +## Option 1: Use as Template (Recommended) + +GitHub's template feature creates a new repository with all files but a clean commit history. + +1. Click the green **"Use this template"** button on the [repository page](https://github.com/mloda-ai/mloda-plugin-template) +2. Choose **"Create a new repository"** +3. Name your repository (e.g., `acme-features`, `acme-data-plugins`, or `mycompany-feature-groups`) + + > **Note:** Please don't use "mloda" in your package or repository name. However, you can use formulations like "for mloda" or "mloda-compatible" in descriptions. + +4. Choose public or private visibility +5. Click **"Create repository"** + +**Advantages:** +- Clean commit history (starts fresh) +- No fork relationship to maintain +- Your repo is completely independent +- One-click setup in GitHub UI + +## Option 2: Fork + +Forking creates a copy that maintains a connection to the original repository. + +1. Click the **"Fork"** button on the [repository page](https://github.com/mloda-ai/mloda-plugin-template) +2. Choose your account or organization +3. Name your forked repository +4. Click **"Create fork"** + +**Advantages:** +- Can pull updates from the original template +- Familiar workflow for open source contributors + +**Disadvantages:** +- Maintains fork relationship (shows "forked from" on your repo) +- Copies entire commit history +- GitHub may suggest contributing back to the original + +## After Creating Your Repository + +Regardless of which option you chose, follow the setup steps in the [README](../README.md#setup-your-plugin) to customize the template: + +1. Clone your new repository locally +2. Rename the `placeholder/` directory +3. Update `pyproject.toml` +4. Update Python imports +5. Verify with `tox` diff --git a/placeholder/__init__.py b/placeholder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/placeholder/compute_frameworks/__init__.py b/placeholder/compute_frameworks/__init__.py new file mode 100644 index 0000000..7173f84 --- /dev/null +++ b/placeholder/compute_frameworks/__init__.py @@ -0,0 +1 @@ +"""Compute frameworks namespace package.""" diff --git a/placeholder/compute_frameworks/my_plugin/__init__.py b/placeholder/compute_frameworks/my_plugin/__init__.py new file mode 100644 index 0000000..103945a --- /dev/null +++ b/placeholder/compute_frameworks/my_plugin/__init__.py @@ -0,0 +1,5 @@ +"""My Plugin ComputeFramework package.""" + +from placeholder.compute_frameworks.my_plugin.my_compute_framework import MyComputeFramework + +__all__ = ["MyComputeFramework"] diff --git a/placeholder/compute_frameworks/my_plugin/my_compute_framework.py b/placeholder/compute_frameworks/my_plugin/my_compute_framework.py new file mode 100644 index 0000000..1cb5df4 --- /dev/null +++ b/placeholder/compute_frameworks/my_plugin/my_compute_framework.py @@ -0,0 +1,22 @@ +"""Example ComputeFramework implementation.""" + +from typing import Optional, Set +from uuid import UUID, uuid4 + +from mloda.core.abstract_plugins.components.parallelization_modes import ParallelizationMode +from mloda.core.abstract_plugins.function_extender import Extender +from mloda.provider import ComputeFramework + + +class MyComputeFramework(ComputeFramework): + """Example ComputeFramework - rename and customize for your use case.""" + + def __init__( + self, + mode: ParallelizationMode = ParallelizationMode.SYNC, + children_if_root: frozenset[UUID] = frozenset(), + uuid: UUID = uuid4(), + function_extender: Optional[Set[Extender]] = None, + ) -> None: + """Initialize with default values for minimal instantiation.""" + super().__init__(mode, children_if_root, uuid, function_extender) diff --git a/placeholder/compute_frameworks/my_plugin/tests/__init__.py b/placeholder/compute_frameworks/my_plugin/tests/__init__.py new file mode 100644 index 0000000..d239d81 --- /dev/null +++ b/placeholder/compute_frameworks/my_plugin/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for My Plugin ComputeFramework.""" diff --git a/placeholder/compute_frameworks/my_plugin/tests/test_my_compute_framework.py b/placeholder/compute_frameworks/my_plugin/tests/test_my_compute_framework.py new file mode 100644 index 0000000..3c10722 --- /dev/null +++ b/placeholder/compute_frameworks/my_plugin/tests/test_my_compute_framework.py @@ -0,0 +1,15 @@ +"""Tests for MyComputeFramework.""" + +from placeholder.compute_frameworks.my_plugin import MyComputeFramework +from mloda.provider import ComputeFramework + + +def test_extends_base() -> None: + """MyComputeFramework should extend ComputeFramework.""" + assert issubclass(MyComputeFramework, ComputeFramework) + + +def test_instantiation() -> None: + """MyComputeFramework should instantiate with no arguments.""" + instance = MyComputeFramework() + assert instance is not None diff --git a/placeholder/extenders/__init__.py b/placeholder/extenders/__init__.py new file mode 100644 index 0000000..1e3e767 --- /dev/null +++ b/placeholder/extenders/__init__.py @@ -0,0 +1 @@ +"""Extenders namespace package.""" diff --git a/placeholder/extenders/my_plugin/__init__.py b/placeholder/extenders/my_plugin/__init__.py new file mode 100644 index 0000000..151e8c0 --- /dev/null +++ b/placeholder/extenders/my_plugin/__init__.py @@ -0,0 +1,5 @@ +"""My Plugin Extender package.""" + +from placeholder.extenders.my_plugin.my_extender import MyExtender + +__all__ = ["MyExtender"] diff --git a/placeholder/extenders/my_plugin/my_extender.py b/placeholder/extenders/my_plugin/my_extender.py new file mode 100644 index 0000000..f0d761d --- /dev/null +++ b/placeholder/extenders/my_plugin/my_extender.py @@ -0,0 +1,17 @@ +"""Example Extender implementation.""" + +from typing import Any, Set + +from mloda.core.abstract_plugins.function_extender import Extender, ExtenderHook + + +class MyExtender(Extender): + """Example Extender - rename and customize for your use case.""" + + def wraps(self) -> Set[ExtenderHook]: + """Return the set of hooks this extender wraps.""" + return set() + + def __call__(self, func: Any, *args: Any, **kwargs: Any) -> Any: + """Execute the wrapped function without modification.""" + return func(*args, **kwargs) diff --git a/placeholder/extenders/my_plugin/tests/__init__.py b/placeholder/extenders/my_plugin/tests/__init__.py new file mode 100644 index 0000000..2e9e8e6 --- /dev/null +++ b/placeholder/extenders/my_plugin/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for My Plugin Extender.""" diff --git a/placeholder/extenders/my_plugin/tests/test_my_extender.py b/placeholder/extenders/my_plugin/tests/test_my_extender.py new file mode 100644 index 0000000..6f2a3d2 --- /dev/null +++ b/placeholder/extenders/my_plugin/tests/test_my_extender.py @@ -0,0 +1,15 @@ +"""Tests for MyExtender.""" + +from placeholder.extenders.my_plugin import MyExtender +from mloda.core.abstract_plugins.function_extender import Extender + + +def test_extends_base() -> None: + """MyExtender should extend Extender.""" + assert issubclass(MyExtender, Extender) + + +def test_instantiation() -> None: + """MyExtender should instantiate with no arguments.""" + instance = MyExtender() + assert instance is not None diff --git a/placeholder/feature_groups/__init__.py b/placeholder/feature_groups/__init__.py new file mode 100644 index 0000000..cc6e452 --- /dev/null +++ b/placeholder/feature_groups/__init__.py @@ -0,0 +1 @@ +"""Feature groups namespace package.""" diff --git a/placeholder/feature_groups/my_plugin/__init__.py b/placeholder/feature_groups/my_plugin/__init__.py new file mode 100644 index 0000000..31826cf --- /dev/null +++ b/placeholder/feature_groups/my_plugin/__init__.py @@ -0,0 +1,5 @@ +"""My Plugin FeatureGroup package.""" + +from placeholder.feature_groups.my_plugin.my_feature_group import MyFeatureGroup + +__all__ = ["MyFeatureGroup"] diff --git a/placeholder/feature_groups/my_plugin/my_feature_group.py b/placeholder/feature_groups/my_plugin/my_feature_group.py new file mode 100644 index 0000000..4c36e54 --- /dev/null +++ b/placeholder/feature_groups/my_plugin/my_feature_group.py @@ -0,0 +1,14 @@ +"""Example FeatureGroup implementation.""" + +from typing import Any + +from mloda.provider import FeatureGroup + + +class MyFeatureGroup(FeatureGroup): + """Example FeatureGroup - rename and customize for your use case.""" + + @classmethod + def calculate_feature(cls, data: Any, features: Any) -> Any: + """Calculate and return feature data.""" + return {"example": "data"} diff --git a/placeholder/feature_groups/my_plugin/tests/__init__.py b/placeholder/feature_groups/my_plugin/tests/__init__.py new file mode 100644 index 0000000..3e05d09 --- /dev/null +++ b/placeholder/feature_groups/my_plugin/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for My Plugin FeatureGroup.""" diff --git a/placeholder/feature_groups/my_plugin/tests/test_my_feature_group.py b/placeholder/feature_groups/my_plugin/tests/test_my_feature_group.py new file mode 100644 index 0000000..029e29c --- /dev/null +++ b/placeholder/feature_groups/my_plugin/tests/test_my_feature_group.py @@ -0,0 +1,15 @@ +"""Tests for MyFeatureGroup.""" + +from placeholder.feature_groups.my_plugin import MyFeatureGroup +from mloda.provider import FeatureGroup + + +def test_extends_base() -> None: + """MyFeatureGroup should extend FeatureGroup.""" + assert issubclass(MyFeatureGroup, FeatureGroup) + + +def test_calculate_feature() -> None: + """calculate_feature should return example data.""" + result = MyFeatureGroup.calculate_feature(None, None) + assert result == {"example": "data"} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0836e5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "placeholder-my-plugin" +version = "0.2.0" +description = "TEMPLATE PLACEHOLDER - Do not install directly. This package reserves the name for mloda-plugin-template. See https://github.com/mloda-ai/mloda-plugin-template" +license = "Apache-2.0" +authors = [{ name = "Your Name placeholder", email = "placeholder@placeholder.com" }] +dependencies = ["mloda>=0.4.3", "mloda-testing>=0.2.6"] +requires-python = ">=3.10" + +[project.optional-dependencies] +dev = [ + "tox", + "tox-uv", + "pytest", + "ruff", + "mypy", + "bandit", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["placeholder*"] + +[tool.pytest.ini_options] +testpaths = ["placeholder", "tests"] +python_files = ["test_*.py"] + +[tool.ruff] +line-length = 120 + +[tool.mypy] +strict = true +ignore_missing_imports = true +namespace_packages = true +explicit_package_bases = true + +[tool.bandit] +exclude_dirs = [".tox", ".venv", "tests"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..792e5a7 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Root-level tests for mloda plugin template.""" diff --git a/tests/test_mloda_imports.py b/tests/test_mloda_imports.py new file mode 100644 index 0000000..00e9425 --- /dev/null +++ b/tests/test_mloda_imports.py @@ -0,0 +1,23 @@ +"""Tests to verify mloda dependencies can be imported.""" + + +def test_mloda_provider_imports() -> None: + """Verify mloda.provider module imports work.""" + from mloda.provider import FeatureGroup, ComputeFramework + + assert FeatureGroup is not None + assert ComputeFramework is not None + + +def test_mloda_core_imports() -> None: + """Verify mloda.core module imports work.""" + from mloda.core.abstract_plugins.function_extender import Extender + + assert Extender is not None + + +def test_mloda_testing_imports() -> None: + """Verify mloda.testing module imports work.""" + from mloda.testing import FeatureGroupTestBase + + assert FeatureGroupTestBase is not None diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1dae751 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = python310 + +[testenv] +usedevelop = true +extras = dev +allowlist_externals = sh +commands = + pytest + ruff format --check --line-length 120 . + ruff check . + mypy --strict --ignore-missing-imports . + bandit -c pyproject.toml -r -q .