From 3d4d02abd2c7e23b7ca3de0f27cf422fc8323506 Mon Sep 17 00:00:00 2001 From: rhdzmota Date: Mon, 20 Jul 2026 18:27:58 -0600 Subject: [PATCH 1/4] Scaffold alissa-python-sdk: namespace-anchor package with tool extras Stand up the `alissa` distribution as the single package that anchors the `alissa` import namespace and curates optional tool extras, using alissa-github-review-daemon as the structural reference. - Aggregator model: ship only the owned leaf `alissa.sdk`; keep every namespace level PEP 420 so tool distributions merge in via extras (e.g. `pip install 'alissa[tools.github.reviewloop]'`). - Extras and runtime discovery share one source of truth (_tools.py:: CURATED_TOOLS), read by both setup.py and installed_tools(). - installed_tools() probes via importlib find_spec so it is correct for both editable and wheel installs. - Console script `alissa-sdk` (not `alissa`, to avoid shadowing the Alissa by Fahera CLI). - Reference conventions: src/main + src/test, plain-text version file, dev scripts, and CI (style/tests/types/wheel/publish) matrixed over the distribution directory. TASK-450587736 Co-Authored-By: alissa-app Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/check-style.yaml | 27 ++++++ .github/workflows/check-tests.yaml | 30 ++++++ .github/workflows/check-types.yaml | 29 ++++++ .github/workflows/check-wheel.yaml | 34 +++++++ .github/workflows/package-publish.yaml | 50 ++++++++++ .gitignore | 13 +++ .python-version | 1 + README.md | 82 ++++++++++++++++- alissa/MANIFEST.in | 2 + alissa/README.md | 92 +++++++++++++++++++ alissa/requirements.txt | 2 + alissa/setup.py | 82 +++++++++++++++++ alissa/src/main/alissa/sdk/__init__.py | 48 ++++++++++ alissa/src/main/alissa/sdk/__main__.py | 46 ++++++++++ alissa/src/main/alissa/sdk/_tools.py | 20 ++++ alissa/src/main/alissa/sdk/version | 1 + alissa/src/main/alissa/sdk/version.py | 46 ++++++++++ .../src/test/test_alissa/test_sdk/test_sdk.py | 62 +++++++++++++ check-style.sh | 1 + check-types.sh | 1 + requirements-develop.txt | 6 ++ tests-coverage.sh | 1 + tests-unit.sh | 1 + 23 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-style.yaml create mode 100644 .github/workflows/check-tests.yaml create mode 100644 .github/workflows/check-types.yaml create mode 100644 .github/workflows/check-wheel.yaml create mode 100644 .github/workflows/package-publish.yaml create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 alissa/MANIFEST.in create mode 100644 alissa/README.md create mode 100644 alissa/requirements.txt create mode 100644 alissa/setup.py create mode 100644 alissa/src/main/alissa/sdk/__init__.py create mode 100644 alissa/src/main/alissa/sdk/__main__.py create mode 100644 alissa/src/main/alissa/sdk/_tools.py create mode 100644 alissa/src/main/alissa/sdk/version create mode 100644 alissa/src/main/alissa/sdk/version.py create mode 100644 alissa/src/test/test_alissa/test_sdk/test_sdk.py create mode 100644 check-style.sh create mode 100644 check-types.sh create mode 100644 requirements-develop.txt create mode 100644 tests-coverage.sh create mode 100644 tests-unit.sh diff --git a/.github/workflows/check-style.yaml b/.github/workflows/check-style.yaml new file mode 100644 index 0000000..045b752 --- /dev/null +++ b/.github/workflows/check-style.yaml @@ -0,0 +1,27 @@ +name: Check Python Style +on: [pull_request] + +jobs: + python-style-check: + strategy: + matrix: + path: + - alissa + name: Python Style Check for ${{ matrix.path }} + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Requirements + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-develop.txt + - name: Execute Style Check + run: | + bash check-style.sh ${{ matrix.path }} diff --git a/.github/workflows/check-tests.yaml b/.github/workflows/check-tests.yaml new file mode 100644 index 0000000..0f74287 --- /dev/null +++ b/.github/workflows/check-tests.yaml @@ -0,0 +1,30 @@ +name: Check Unit-Tests and Simple Coverage Report +on: [pull_request] + +jobs: + python-unit-testing: + strategy: + matrix: + path: + - alissa + name: Unit-Test Check for ${{ matrix.path }} + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Requirements + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-develop.txt + - name: Install Package + run: python -m pip install -e ./${{ matrix.path }} + - name: Execute Unit-Tests + run: | + bash tests-unit.sh ${{ matrix.path }} + bash tests-coverage.sh ${{ matrix.path }} diff --git a/.github/workflows/check-types.yaml b/.github/workflows/check-types.yaml new file mode 100644 index 0000000..ab890d1 --- /dev/null +++ b/.github/workflows/check-types.yaml @@ -0,0 +1,29 @@ +name: Check Python Types +on: [pull_request] + +jobs: + python-unit-testing: + strategy: + matrix: + path: + - alissa + name: Python Types Check for ${{ matrix.path }} + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Requirements + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements-develop.txt + - name: Install Package + run: python -m pip install -e ./${{ matrix.path }} + - name: Execute Type Checks + run: | + bash check-types.sh ${{ matrix.path }} diff --git a/.github/workflows/check-wheel.yaml b/.github/workflows/check-wheel.yaml new file mode 100644 index 0000000..87a3b90 --- /dev/null +++ b/.github/workflows/check-wheel.yaml @@ -0,0 +1,34 @@ +name: Check Creation of Wheel Package +on: [pull_request] + +jobs: + python-wheel-check: + strategy: + matrix: + path: + - alissa + name: Wheel Package Check for ${{ matrix.path }} + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Requirements + run: | + python -m pip install --upgrade pip + python -m pip install setuptools wheel + - name: Create Wheel Package + run: (cd ${{ matrix.path }}; python setup.py bdist_wheel) + - name: Install Wheel Package + run: python -m pip install ${{ matrix.path }}/dist/*.whl + - name: Execute Simple Command to Verify Installation + run: | + python -c "from alissa.sdk.version import version; print(version)" + python -c "from alissa.sdk import installed_tools; print(installed_tools())" + alissa-sdk --help + alissa-sdk --tools diff --git a/.github/workflows/package-publish.yaml b/.github/workflows/package-publish.yaml new file mode 100644 index 0000000..a278989 --- /dev/null +++ b/.github/workflows/package-publish.yaml @@ -0,0 +1,50 @@ +name: Python Package Publish + +# Publishing is irreversible: a version number, once on PyPI, can never be +# reused even if the release is deleted. The version file is therefore the +# release trigger -- merging a PR that does not bump it publishes nothing, +# and twine's --skip-existing keeps a re-run from failing the workflow. + +on: + pull_request: + paths: + - .github/workflows/package-publish.yaml + - alissa/** + types: + - closed + branches: + - main + +jobs: + python-publish: + environment: PYPI Package Publishing + strategy: + matrix: + path: + - alissa + if: github.event.pull_request.merged == true + name: Python Package Build and Publish for ${{ matrix.path }} + runs-on: ubuntu-latest + steps: + - name: Clone Repo + uses: actions/checkout@v4 + with: + submodules: true + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install Build Tools + run: | + python -m pip install --upgrade pip + python -m pip install build twine + - name: Build + run: (cd ${{ matrix.path }}; python -m build) + - name: Check Metadata + run: twine check ${{ matrix.path }}/dist/* + - name: Publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + GH_BRANCH: ${{ github.ref }} + run: twine upload --verbose --skip-existing ${{ matrix.path }}/dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8e5b80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.coverage +.pytest_cache/ +.mypy_cache/ + +# Local environments +venv/ +.venv/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..871f80a --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.3 diff --git a/README.md b/README.md index 9cc409b..249381e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,82 @@ # alissa-python-sdk -Alissa Python SDK + +The single Python package that owns and anchors the `alissa` import namespace — +`import alissa.sdk` — and curates its **tool extras**: + +```sh +pip install alissa # SDK core, zero third-party deps +pip install 'alissa[tools.github.reviewloop]' # + a tool, merged into alissa.tools.* +pip install 'alissa[all]' # + every tool +``` + +## How ownership works + +`alissa` is the anchor distribution: the thing you `pip install`, the top-level +SDK surface (`alissa.sdk`), and the list of available tools (its extras). But it +ships **no** `__init__.py` at the namespace levels, so it doesn't monopolize the +namespace — tool distributions pulled in by extras merge their own +`alissa.tools.*` subtrees in via [PEP 420](https://peps.python.org/pep-0420/): + +``` +alissa/ ← namespace (no __init__.py) +├── sdk/ __init__.py ← the SDK distribution owns this leaf +└── tools/ ← namespace + └── github/ + └── reviewloop/ __init__.py ← ships from alissa-tools-github-reviewloop +``` + +Each `alissa[tools..]` extra maps to a separately published +distribution (`alissa-tools--`). Tools stay independently versioned +and released; the SDK just curates which ones exist and pulls them in on demand. +See [`alissa/README.md`](./alissa/README.md) for the full model and for how to +add a new tool extra. + +## Repository layout + +This repo is a monorepo of distributions. Today it holds one, `alissa/`; the +shared dev tooling and CI live at the root and matrix over each distribution's +directory name. + +``` +alissa-python-sdk/ +├── alissa/ ← the `alissa` distribution +│ ├── setup.py ← packaging + extras_require (the tool curation) +│ ├── requirements.txt ← core deps (empty — the SDK core has none) +│ ├── MANIFEST.in +│ └── src/ +│ ├── main/alissa/sdk/ ← owned leaf package (+ plain-text `version` file) +│ └── test/test_alissa/ ← mirrors main as test_* +├── .github/workflows/ ← style / types / tests / wheel / publish (matrix: alissa) +├── check-style.sh check-types.sh tests-unit.sh tests-coverage.sh +├── requirements-develop.txt +└── .python-version ← 3.12.3 +``` + +## Develop + +```sh +python -m venv venv && source venv/bin/activate +pip install -r requirements-develop.txt +pip install -e ./alissa + +alissa-sdk # SDK version + how to add tools +alissa-sdk --tools # list installed alissa.tools.* packages +``` + +## Checks + +Each script takes a distribution directory (matching the CI matrix): + +```sh +bash tests-unit.sh alissa +bash tests-coverage.sh alissa +bash check-style.sh alissa +bash check-types.sh alissa +``` + +## Publishing + +Publishing is driven by the per-distribution `version` file +(`alissa/src/main/alissa/sdk/version`). Merging a PR to `main` that bumps it +publishes to PyPI; a PR that doesn't bump it publishes nothing +(`twine upload --skip-existing`). Versions are irreversible once on PyPI. diff --git a/alissa/MANIFEST.in b/alissa/MANIFEST.in new file mode 100644 index 0000000..14f5d05 --- /dev/null +++ b/alissa/MANIFEST.in @@ -0,0 +1,2 @@ +include requirements.txt +include src/main/alissa/sdk/version diff --git a/alissa/README.md b/alissa/README.md new file mode 100644 index 0000000..7ec27be --- /dev/null +++ b/alissa/README.md @@ -0,0 +1,92 @@ +# alissa + +The `alissa` distribution: the single package that owns and anchors the +`alissa` import namespace, and the curation point for its tool extras. + +## The model + +This distribution ships **only** the leaf package `alissa.sdk`. Everything +above it is a [PEP 420](https://peps.python.org/pep-0420/) namespace package +with no `__init__.py`, so other distributions — pulled in through this package's +extras — contribute their own subtrees under the same namespace: + +``` +alissa/ ← namespace (no __init__.py) +├── sdk/ __init__.py ← THIS distribution owns this leaf +└── tools/ ← namespace (no __init__.py) + └── github/ ← namespace + └── reviewloop/ __init__.py ← ships from alissa-tools-github-reviewloop +``` + +The rule (same as every distribution in this ecosystem): own a leaf, declare +only that subtree (`find_namespace_packages(include=[PACKAGE, f"{PACKAGE}.*"])`), +and never add an `__init__.py` at a namespace level — doing so would claim it for +one distribution and shadow the others. + +"Owning the namespace" here means being the **anchor**: the distribution you +`pip install`, the one that provides the top-level SDK surface (`alissa.sdk`), +and the one whose extras enumerate the available tools. + +## Install + +```sh +pip install alissa # SDK core only, zero third-party deps +pip install 'alissa[tools.github.reviewloop]' # + the GitHub review-loop tool +pip install 'alissa[all]' # + every tool extra +``` + +Each extra pulls in a separately published tool distribution (e.g. +`alissa-tools-github-reviewloop`), which merges its `alissa.tools.*` packages +into the namespace. pip normalizes the dotted extra name, so +`alissa[tools.github.reviewloop]` resolves. + +### Editable / development install + +```sh +pip install -e ./alissa +``` + +## Console scripts + +| Command | Entry point | +| --- | --- | +| `alissa-sdk` | `alissa.sdk.__main__:main` | + +`alissa-sdk` is deliberately **not** `alissa` — the `alissa` command is the +Alissa by Fahera CLI, which this SDK does not shadow. + +```sh +alissa-sdk # SDK version + how to add tools +alissa-sdk --tools # list installed alissa.tools.* packages +alissa-sdk --version +``` + +## Using it + +```python +from alissa.sdk import __version__, installed_tools + +print(__version__) +print(installed_tools()) # ['alissa.tools.github.reviewloop', ...] — whatever extras are installed +``` + +`installed_tools()` reports which of the SDK's curated tools are installed, +probing each through the import machinery — so it is correct for both wheel and +editable installs. The curated set lives in `src/main/alissa/sdk/_tools.py`, the +same registry `setup.py` builds the extras from. + +## Layout + +`src/main` holds the package tree, `src/test` mirrors it as `test_*`. The +distribution version lives in the plain-text `version` file next to the package +it versions (`src/main/alissa/sdk/version`), read by both `setup.py` and +`version.py`. + +## Adding a tool extra + +1. Ship the tool as its own distribution owning a leaf under `alissa.tools.*` + (see [`alissa-tools-github-reviewloop`](https://pypi.org/project/alissa-tools-github-reviewloop/) + for the template). +2. Add one line to `extras_require` in [`setup.py`](./setup.py): + `"tools..": ["alissa-tools--"]`. The `all` extra + updates itself. diff --git a/alissa/requirements.txt b/alissa/requirements.txt new file mode 100644 index 0000000..26a2626 --- /dev/null +++ b/alissa/requirements.txt @@ -0,0 +1,2 @@ +# The SDK core has no third-party dependencies. Tool dependencies are pulled in +# per-extra (see extras_require in setup.py), never from here. diff --git a/alissa/setup.py b/alissa/setup.py new file mode 100644 index 0000000..ff5c0ec --- /dev/null +++ b/alissa/setup.py @@ -0,0 +1,82 @@ +import os +from setuptools import setup, find_namespace_packages + + +CODEBASE_PATH = os.environ.get( + "CODEBASE_PATH", + default=os.path.join("src", "main"), +) + +# This distribution owns exactly ONE leaf package, `alissa.sdk`. Everything +# above it (`alissa`, `alissa.tools`, ...) is left as a PEP 420 namespace with +# no __init__.py, so tool distributions installed through the extras below merge +# their own alissa.tools.* subtrees into the same namespace. Adding an +# __init__.py at any namespace level would claim it for this distribution and +# shadow the tools. +PACKAGE = "alissa.sdk" + +with open("requirements.txt", "r") as file: + requirements = [line for line in file.read().splitlines() if line and not line.startswith("#")] + +version_filepath = os.path.join(CODEBASE_PATH, *PACKAGE.split("."), "version") +with open(version_filepath, "r") as file: + version = file.read().strip() + + +with open("README.md") as file: + readme = file.read() + + +# Extras are the SDK's curation surface. Each extra pulls in a separately +# published tool distribution that contributes its own alissa.tools.* subtree; +# the SDK core itself carries no third-party dependencies. Install one with +# pip install 'alissa[tools.github.reviewloop]' +# pip normalizes the dotted extra name, so the dotted spelling above resolves. +# +# The curated tools live in the package's `_tools.py` so the extras and the +# runtime's installed_tools() share one source of truth. Read it here without +# importing the package (its deps may not be installed at build time). +_tools_namespace: dict = {} +_tools_filepath = os.path.join(CODEBASE_PATH, *PACKAGE.split("."), "_tools.py") +with open(_tools_filepath, "r") as file: + exec(compile(file.read(), _tools_filepath, "exec"), _tools_namespace) + +extras_require = { + extra: [distribution] + for extra, (distribution, _module) in _tools_namespace["CURATED_TOOLS"].items() +} +# `all` is the union of every tool extra, kept in sync automatically. +extras_require["all"] = sorted({dep for deps in extras_require.values() for dep in deps}) + + +setup( + name="alissa", + version=version, + description="Alissa Python SDK — anchors the 'alissa' namespace and its tool extras.", + long_description=readme, + long_description_content_type='text/markdown', + url="https://alissa.app", + author="Fahera", + author_email="support@alissa.app", + packages=find_namespace_packages( + where=CODEBASE_PATH, + include=[PACKAGE, f"{PACKAGE}.*"], + ), + package_dir={ + "": CODEBASE_PATH + }, + package_data={ + "": [ + version_filepath, + ] + }, + entry_points={ + "console_scripts": [ + "alissa-sdk=alissa.sdk.__main__:main", + ] + }, + install_requires=requirements, + extras_require=extras_require, + include_package_data=True, + python_requires=">=3.11", +) diff --git a/alissa/src/main/alissa/sdk/__init__.py b/alissa/src/main/alissa/sdk/__init__.py new file mode 100644 index 0000000..f0c2523 --- /dev/null +++ b/alissa/src/main/alissa/sdk/__init__.py @@ -0,0 +1,48 @@ +"""alissa — the Alissa Python SDK. + +This distribution is the canonical anchor of the ``alissa`` import namespace. +It ships **no** ``__init__.py`` at the namespace levels (``alissa``, +``alissa.tools``, ...) — only this owned leaf, ``alissa.sdk``, is a regular +package. That keeps every namespace level a +`PEP 420 `_ implicit namespace package, so +tool distributions pulled in through extras — for example +``pip install 'alissa[tools.github.reviewloop]'`` — merge their own +``alissa.tools.*`` subtrees into the same namespace at import time. + +The SDK's own surface lives here, under ``alissa.sdk``. Tools live under +``alissa.tools.*`` and are discovered dynamically via :func:`installed_tools`. +""" + +from .version import version + +__all__ = ["version", "__version__", "installed_tools"] +__version__ = version.value + + +def installed_tools() -> list: + """Return the curated ``alissa.tools.*`` packages installed in this env. + + The SDK curates a set of tools, each pulled in by an extra + (``pip install 'alissa[]'``); see :data:`alissa.sdk._tools.CURATED_TOOLS`, + the same registry ``setup.py`` builds ``extras_require`` from. This probes + each curated tool with :func:`importlib.util.find_spec`, which resolves + through the import machinery — so it works for both wheel and editable + installs (a plain filesystem walk of ``__path__`` misses editable ones, + whose path entries are synthetic finder hooks). Returns an empty list when + no tool extras are installed. + """ + import importlib.util + + from ._tools import CURATED_TOOLS + + found: list = [] + for _extra, (_distribution, module) in CURATED_TOOLS.items(): + try: + spec = importlib.util.find_spec(module) + except (ImportError, ValueError): + # A broken parent package must not sink discovery of the others. + spec = None + if spec is not None: + found.append(module) + + return sorted(found) diff --git a/alissa/src/main/alissa/sdk/__main__.py b/alissa/src/main/alissa/sdk/__main__.py new file mode 100644 index 0000000..f2f23a6 --- /dev/null +++ b/alissa/src/main/alissa/sdk/__main__.py @@ -0,0 +1,46 @@ +"""Console entry point for the Alissa Python SDK (``alissa-sdk``). + +Named ``alissa-sdk`` — not ``alissa`` — on purpose: the ``alissa`` command is +the Alissa by Fahera CLI (tasks, sessions, tmux queues), a separate tool this +SDK does not shadow. +""" +import argparse + +from . import __version__, installed_tools + + +def main() -> None: + parser = argparse.ArgumentParser( + prog="alissa-sdk", + description="Alissa Python SDK — anchors the 'alissa' namespace and its tool extras.", + ) + parser.add_argument( + "--version", + action="version", + version=f"alissa {__version__}", + ) + parser.add_argument( + "--tools", + action="store_true", + help="list the alissa.tools.* packages installed in this environment", + ) + args = parser.parse_args() + + if args.tools: + tools = installed_tools() + if tools: + for name in tools: + print(name) + else: + print("no alissa.tools.* packages installed") + print("try: pip install 'alissa[tools.github.reviewloop]'") + return + + print(f"alissa {__version__}") + print("the Alissa Python SDK — anchors the 'alissa' namespace") + print("install tools via extras, e.g.: pip install 'alissa[tools.github.reviewloop]'") + print("list installed tools: alissa-sdk --tools") + + +if __name__ == "__main__": + main() diff --git a/alissa/src/main/alissa/sdk/_tools.py b/alissa/src/main/alissa/sdk/_tools.py new file mode 100644 index 0000000..71eda8a --- /dev/null +++ b/alissa/src/main/alissa/sdk/_tools.py @@ -0,0 +1,20 @@ +"""Single source of truth for the tools this SDK curates. + +Each entry maps a pip *extra* name to the distribution it pulls in and the +module that distribution makes importable: + + "": ("", "") + +`setup.py` reads this to build ``extras_require`` (so the extras and the +runtime never drift), and :func:`alissa.sdk.installed_tools` reads it to report +which curated tools are actually installed. + +To curate a new tool, add one line here — nothing else needs editing. +""" + +CURATED_TOOLS = { + "tools.github.reviewloop": ( + "alissa-tools-github-reviewloop", + "alissa.tools.github.reviewloop", + ), +} diff --git a/alissa/src/main/alissa/sdk/version b/alissa/src/main/alissa/sdk/version new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/alissa/src/main/alissa/sdk/version @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/alissa/src/main/alissa/sdk/version.py b/alissa/src/main/alissa/sdk/version.py new file mode 100644 index 0000000..770d54a --- /dev/null +++ b/alissa/src/main/alissa/sdk/version.py @@ -0,0 +1,46 @@ +import os +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Version: + name: str + value: str + + def components(self, as_int: bool = False) -> list: + return [int(val) if as_int else val for val in self.value.split(".")] + + @property + def major(self) -> int: + component, *_ = self.components(as_int=True) + return component + + @property + def minor(self) -> int: + _, component, *_ = self.components(as_int=True) + return component + + @property + def patch(self) -> int: + *_, component = self.components(as_int=True) + return component + + @classmethod + def from_path(cls, dirpath: str, name: str): + for file in os.listdir(dirpath): + if file.lower().endswith("version"): + filepath = os.path.join(dirpath, file) + break + else: + raise ValueError("Version file not found for package name: " + name) + + with open(filepath, "r") as version_file: + version_value = version_file.readline().strip() + return cls(name=name, value=version_value) + + +try: + version = Version.from_path(name="alissa", dirpath=os.path.dirname(__file__)) +except Exception: + print("Version file not found for package name: alissa, using 0.0.0") + version = Version(name="alissa", value="0.0.0") diff --git a/alissa/src/test/test_alissa/test_sdk/test_sdk.py b/alissa/src/test/test_alissa/test_sdk/test_sdk.py new file mode 100644 index 0000000..267535d --- /dev/null +++ b/alissa/src/test/test_alissa/test_sdk/test_sdk.py @@ -0,0 +1,62 @@ +"""The SDK's package contract: version wiring and curated-tool discovery. + +These lock the two things the aggregator model depends on — that the leaf +package reports a coherent version, and that tool discovery is driven by the +curated registry through the import machinery (so it is correct for both wheel +and editable installs), without depending on which tools happen to be installed. +""" +from __future__ import annotations + +from alissa.sdk import __version__, installed_tools +from alissa.sdk.version import version + + +def test_version_matches_the_version_file(): + assert __version__ == version.value + assert version.name == "alissa" + + +def test_version_is_semver(): + major, minor, patch = version.components(as_int=True) + assert isinstance(major, int) and isinstance(minor, int) and isinstance(patch, int) + + +def test_curated_registry_is_wellformed(): + from alissa.sdk._tools import CURATED_TOOLS + + for extra, value in CURATED_TOOLS.items(): + distribution, module = value + assert extra.startswith("tools."), extra + assert distribution.startswith("alissa-"), distribution + assert module.startswith("alissa.tools."), module + + +def test_installed_tools_returns_a_sorted_subset_of_the_registry(): + from alissa.sdk._tools import CURATED_TOOLS + + curated_modules = {module for _distribution, module in CURATED_TOOLS.values()} + tools = installed_tools() + + assert isinstance(tools, list) + assert tools == sorted(tools) + # Whatever is reported is a curated module — never something invented. + assert set(tools) <= curated_modules + + +def test_installed_tools_probes_via_the_import_machinery(monkeypatch): + # Drive discovery off a fake registry: one importable module, one not. + # This is deterministic regardless of which real tools are installed. + import alissa.sdk._tools as tools_module + + monkeypatch.setattr( + tools_module, + "CURATED_TOOLS", + { + "tools.present": ("alissa-tools-present", "json"), + "tools.absent": ("alissa-tools-absent", "alissa.tools.__does_not_exist__"), + }, + ) + + result = installed_tools() + assert "json" in result + assert "alissa.tools.__does_not_exist__" not in result diff --git a/check-style.sh b/check-style.sh new file mode 100644 index 0000000..c1fc342 --- /dev/null +++ b/check-style.sh @@ -0,0 +1 @@ +pycodestyle ${1} --max-line-length 120 diff --git a/check-types.sh b/check-types.sh new file mode 100644 index 0000000..4fb04b0 --- /dev/null +++ b/check-types.sh @@ -0,0 +1 @@ +mypy ${1} --ignore-missing-imports --install-types --non-interactive diff --git a/requirements-develop.txt b/requirements-develop.txt new file mode 100644 index 0000000..2f7e060 --- /dev/null +++ b/requirements-develop.txt @@ -0,0 +1,6 @@ +pycodestyle==2.14.0 +pytest==8.4.1 +coverage==7.10.6 +mypy==1.17.1 +mypy-extensions==1.1.0 +typing_extensions==4.15.0 diff --git a/tests-coverage.sh b/tests-coverage.sh new file mode 100644 index 0000000..a719d5f --- /dev/null +++ b/tests-coverage.sh @@ -0,0 +1 @@ +(cd ${1}; coverage report --omit="test_*","*_remote_module_non_scriptable.py") diff --git a/tests-unit.sh b/tests-unit.sh new file mode 100644 index 0000000..81ee660 --- /dev/null +++ b/tests-unit.sh @@ -0,0 +1 @@ +(cd ${1}; coverage run -m pytest src/test) From 83b003e211a90627c9f4ac09c3e5437672ff5e37 Mon Sep 17 00:00:00 2001 From: rhdzmota Date: Mon, 20 Jul 2026 18:39:12 -0600 Subject: [PATCH 2/4] Name the console script alissa-py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A literal `alissa.py` command cannot work: the generated console-script file `bin/alissa.py` is a valid target for `import alissa` and shadows the installed `alissa` package, so `alissa.sdk` fails to import. Use `alissa-py` — same explicit "Python SDK" signal, no import collision. TASK-450587736 Co-Authored-By: alissa-app Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/check-wheel.yaml | 4 ++-- README.md | 4 ++-- alissa/README.md | 15 +++++++++------ alissa/setup.py | 2 +- alissa/src/main/alissa/sdk/__main__.py | 14 ++++++++------ 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check-wheel.yaml b/.github/workflows/check-wheel.yaml index 87a3b90..9fd8a53 100644 --- a/.github/workflows/check-wheel.yaml +++ b/.github/workflows/check-wheel.yaml @@ -30,5 +30,5 @@ jobs: run: | python -c "from alissa.sdk.version import version; print(version)" python -c "from alissa.sdk import installed_tools; print(installed_tools())" - alissa-sdk --help - alissa-sdk --tools + alissa-py --help + alissa-py --tools diff --git a/README.md b/README.md index 249381e..9eae573 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ python -m venv venv && source venv/bin/activate pip install -r requirements-develop.txt pip install -e ./alissa -alissa-sdk # SDK version + how to add tools -alissa-sdk --tools # list installed alissa.tools.* packages +alissa-py # SDK version + how to add tools +alissa-py --tools # list installed alissa.tools.* packages ``` ## Checks diff --git a/alissa/README.md b/alissa/README.md index 7ec27be..cad5cd4 100644 --- a/alissa/README.md +++ b/alissa/README.md @@ -50,15 +50,18 @@ pip install -e ./alissa | Command | Entry point | | --- | --- | -| `alissa-sdk` | `alissa.sdk.__main__:main` | +| `alissa-py` | `alissa.sdk.__main__:main` | -`alissa-sdk` is deliberately **not** `alissa` — the `alissa` command is the -Alissa by Fahera CLI, which this SDK does not shadow. +The command is `alissa-py` — deliberately **not** `alissa`, which is the Alissa +by Fahera CLI this SDK does not shadow. The `-py` suffix marks it, explicitly, +as the Python SDK's counterpart. (A literal `alissa.py` can't be used: the +console-script file `bin/alissa.py` would shadow the importable `alissa` +package.) ```sh -alissa-sdk # SDK version + how to add tools -alissa-sdk --tools # list installed alissa.tools.* packages -alissa-sdk --version +alissa-py # SDK version + how to add tools +alissa-py --tools # list installed alissa.tools.* packages +alissa-py --version ``` ## Using it diff --git a/alissa/setup.py b/alissa/setup.py index ff5c0ec..251263e 100644 --- a/alissa/setup.py +++ b/alissa/setup.py @@ -72,7 +72,7 @@ }, entry_points={ "console_scripts": [ - "alissa-sdk=alissa.sdk.__main__:main", + "alissa-py=alissa.sdk.__main__:main", ] }, install_requires=requirements, diff --git a/alissa/src/main/alissa/sdk/__main__.py b/alissa/src/main/alissa/sdk/__main__.py index f2f23a6..a97db91 100644 --- a/alissa/src/main/alissa/sdk/__main__.py +++ b/alissa/src/main/alissa/sdk/__main__.py @@ -1,8 +1,10 @@ -"""Console entry point for the Alissa Python SDK (``alissa-sdk``). +"""Console entry point for the Alissa Python SDK (``alissa-py``). -Named ``alissa-sdk`` — not ``alissa`` — on purpose: the ``alissa`` command is -the Alissa by Fahera CLI (tasks, sessions, tmux queues), a separate tool this -SDK does not shadow. +The command is ``alissa-py`` — not ``alissa`` — on purpose: ``alissa`` is the +Alissa by Fahera CLI (tasks, sessions, tmux queues), and the ``-py`` suffix +marks this one, explicitly, as the Python SDK's counterpart. (A literal +``alissa.py`` cannot be used: the console-script file ``bin/alissa.py`` would +shadow the importable ``alissa`` package.) """ import argparse @@ -11,7 +13,7 @@ def main() -> None: parser = argparse.ArgumentParser( - prog="alissa-sdk", + prog="alissa-py", description="Alissa Python SDK — anchors the 'alissa' namespace and its tool extras.", ) parser.add_argument( @@ -39,7 +41,7 @@ def main() -> None: print(f"alissa {__version__}") print("the Alissa Python SDK — anchors the 'alissa' namespace") print("install tools via extras, e.g.: pip install 'alissa[tools.github.reviewloop]'") - print("list installed tools: alissa-sdk --tools") + print("list installed tools: alissa-py --tools") if __name__ == "__main__": From d93919520dc53b58792c0b539a6303d711678e00 Mon Sep 17 00:00:00 2001 From: rhdzmota Date: Mon, 20 Jul 2026 18:43:49 -0600 Subject: [PATCH 3/4] Add alissa.utils.version shared version loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Factor the version-file loader into alissa.utils.version.Version so every alissa.* distribution reuses one implementation instead of re-implementing it. Adds `load()` (warn + fall back) alongside `from_path()` (strict), re-exported from `alissa.utils`. Dogfood it: alissa.sdk.version now loads the distribution's version through the shared utility — the canonical example downstream tools follow. The SDK distribution now owns two leaves (alissa.sdk, alissa.utils); both namespace levels stay PEP 420 so tools still merge in. Downstream tools should declare install_requires=["alissa"] and load their version via alissa.utils.version — no dependency cycle (base alissa requires nothing; the alissa[tools.*] extras are a separate opt-in edge) and no circular import (the SDK core never imports tool code). TASK-450587736 Co-Authored-By: alissa-app Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 6 +- alissa/README.md | 49 +++++++++--- alissa/setup.py | 25 ++++--- alissa/src/main/alissa/sdk/version.py | 50 ++----------- alissa/src/main/alissa/utils/__init__.py | 11 +++ alissa/src/main/alissa/utils/version.py | 75 +++++++++++++++++++ .../test_alissa/test_utils/test_version.py | 55 ++++++++++++++ 7 files changed, 207 insertions(+), 64 deletions(-) create mode 100644 alissa/src/main/alissa/utils/__init__.py create mode 100644 alissa/src/main/alissa/utils/version.py create mode 100644 alissa/src/test/test_alissa/test_utils/test_version.py diff --git a/README.md b/README.md index 9eae573..dd70f01 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ namespace — tool distributions pulled in by extras merge their own ``` alissa/ ← namespace (no __init__.py) -├── sdk/ __init__.py ← the SDK distribution owns this leaf +├── sdk/ __init__.py ← the SDK distribution: surface + version file +├── utils/ __init__.py ← the SDK distribution: shared helpers (utils.version) └── tools/ ← namespace └── github/ └── reviewloop/ __init__.py ← ships from alissa-tools-github-reviewloop @@ -44,7 +45,8 @@ alissa-python-sdk/ │ ├── requirements.txt ← core deps (empty — the SDK core has none) │ ├── MANIFEST.in │ └── src/ -│ ├── main/alissa/sdk/ ← owned leaf package (+ plain-text `version` file) +│ ├── main/alissa/sdk/ ← owned leaf: SDK surface (+ plain-text `version` file) +│ ├── main/alissa/utils/ ← owned leaf: shared helpers (alissa.utils.version) │ └── test/test_alissa/ ← mirrors main as test_* ├── .github/workflows/ ← style / types / tests / wheel / publish (matrix: alissa) ├── check-style.sh check-types.sh tests-unit.sh tests-coverage.sh diff --git a/alissa/README.md b/alissa/README.md index cad5cd4..f451b2a 100644 --- a/alissa/README.md +++ b/alissa/README.md @@ -5,23 +5,25 @@ The `alissa` distribution: the single package that owns and anchors the ## The model -This distribution ships **only** the leaf package `alissa.sdk`. Everything -above it is a [PEP 420](https://peps.python.org/pep-0420/) namespace package -with no `__init__.py`, so other distributions — pulled in through this package's +This distribution ships **only** the leaf packages `alissa.sdk` and +`alissa.utils`. Everything above them is a +[PEP 420](https://peps.python.org/pep-0420/) namespace package with no +`__init__.py`, so other distributions — pulled in through this package's extras — contribute their own subtrees under the same namespace: ``` alissa/ ← namespace (no __init__.py) -├── sdk/ __init__.py ← THIS distribution owns this leaf +├── sdk/ __init__.py ← THIS distribution: SDK surface + version file +├── utils/ __init__.py ← THIS distribution: shared helpers (e.g. utils.version) └── tools/ ← namespace (no __init__.py) └── github/ ← namespace └── reviewloop/ __init__.py ← ships from alissa-tools-github-reviewloop ``` -The rule (same as every distribution in this ecosystem): own a leaf, declare -only that subtree (`find_namespace_packages(include=[PACKAGE, f"{PACKAGE}.*"])`), -and never add an `__init__.py` at a namespace level — doing so would claim it for -one distribution and shadow the others. +The rule (same as every distribution in this ecosystem): own your leaves, +declare only those subtrees (`find_namespace_packages(include=[...])`), and never +add an `__init__.py` at a namespace level — doing so would claim it for one +distribution and shadow the others. "Owning the namespace" here means being the **anchor**: the distribution you `pip install`, the one that provides the top-level SDK surface (`alissa.sdk`), @@ -78,6 +80,34 @@ probing each through the import machinery — so it is correct for both wheel an editable installs. The curated set lives in `src/main/alissa/sdk/_tools.py`, the same registry `setup.py` builds the extras from. +## Shared utilities (`alissa.utils`) + +Helpers the SDK factors out so every `alissa.*` distribution reuses one +implementation instead of copying it. Downstream packages may treat these as +stable, public API. + +### `alissa.utils.version` — load a distribution's version file + +Ship a plain-text `version` file next to your package and load it the same way +the SDK loads its own (`alissa/src/main/alissa/sdk/version.py` is the reference): + +```python +import os +from alissa.utils.version import Version + +version = Version.load(os.path.dirname(__file__), name="alissa-tools-github-reviewloop") +``` + +`Version.load` warns and falls back to `0.0.0` when the file is missing; +`Version.from_path` raises instead, when a missing file should be fatal. + +To use it, declare the SDK as a dependency: `install_requires=["alissa"]` (pin +`alissa>=`). **There is no +dependency cycle** — base `alissa` requires nothing, tools require base `alissa`, +and the `alissa[tools.*]` extras are a separate, opt-in edge. The only +constraint is release ordering: publish `alissa` before a tool that depends on +that version. And no circular *import*: the SDK core never imports tool code. + ## Layout `src/main` holds the package tree, `src/test` mirrors it as `test_*`. The @@ -89,7 +119,8 @@ it versions (`src/main/alissa/sdk/version`), read by both `setup.py` and 1. Ship the tool as its own distribution owning a leaf under `alissa.tools.*` (see [`alissa-tools-github-reviewloop`](https://pypi.org/project/alissa-tools-github-reviewloop/) - for the template). + for the template). Have it load its version via `alissa.utils.version` and + declare `install_requires=["alissa"]`. 2. Add one line to `extras_require` in [`setup.py`](./setup.py): `"tools..": ["alissa-tools--"]`. The `all` extra updates itself. diff --git a/alissa/setup.py b/alissa/setup.py index 251263e..b5d1bc3 100644 --- a/alissa/setup.py +++ b/alissa/setup.py @@ -7,18 +7,23 @@ default=os.path.join("src", "main"), ) -# This distribution owns exactly ONE leaf package, `alissa.sdk`. Everything -# above it (`alissa`, `alissa.tools`, ...) is left as a PEP 420 namespace with -# no __init__.py, so tool distributions installed through the extras below merge -# their own alissa.tools.* subtrees into the same namespace. Adding an -# __init__.py at any namespace level would claim it for this distribution and -# shadow the tools. -PACKAGE = "alissa.sdk" +# This distribution owns two leaf packages under the alissa namespace: +# alissa.sdk — the SDK surface + the distribution's version file +# alissa.utils — shared helpers downstream alissa.* packages reuse +# Everything above them (`alissa`, `alissa.tools`, ...) is left as a PEP 420 +# namespace with no __init__.py, so tool distributions installed through the +# extras below merge their own alissa.tools.* subtrees into the same namespace. +# Adding an __init__.py at any namespace level would claim it for this +# distribution and shadow the tools. +OWNED_PACKAGES = ["alissa.sdk", "alissa.utils"] + +# The distribution's version file lives beside the SDK leaf. +VERSION_PACKAGE = "alissa.sdk" with open("requirements.txt", "r") as file: requirements = [line for line in file.read().splitlines() if line and not line.startswith("#")] -version_filepath = os.path.join(CODEBASE_PATH, *PACKAGE.split("."), "version") +version_filepath = os.path.join(CODEBASE_PATH, *VERSION_PACKAGE.split("."), "version") with open(version_filepath, "r") as file: version = file.read().strip() @@ -37,7 +42,7 @@ # runtime's installed_tools() share one source of truth. Read it here without # importing the package (its deps may not be installed at build time). _tools_namespace: dict = {} -_tools_filepath = os.path.join(CODEBASE_PATH, *PACKAGE.split("."), "_tools.py") +_tools_filepath = os.path.join(CODEBASE_PATH, *VERSION_PACKAGE.split("."), "_tools.py") with open(_tools_filepath, "r") as file: exec(compile(file.read(), _tools_filepath, "exec"), _tools_namespace) @@ -60,7 +65,7 @@ author_email="support@alissa.app", packages=find_namespace_packages( where=CODEBASE_PATH, - include=[PACKAGE, f"{PACKAGE}.*"], + include=[pattern for pkg in OWNED_PACKAGES for pattern in (pkg, f"{pkg}.*")], ), package_dir={ "": CODEBASE_PATH diff --git a/alissa/src/main/alissa/sdk/version.py b/alissa/src/main/alissa/sdk/version.py index 770d54a..bfaa42a 100644 --- a/alissa/src/main/alissa/sdk/version.py +++ b/alissa/src/main/alissa/sdk/version.py @@ -1,46 +1,10 @@ -import os -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True) -class Version: - name: str - value: str - - def components(self, as_int: bool = False) -> list: - return [int(val) if as_int else val for val in self.value.split(".")] - - @property - def major(self) -> int: - component, *_ = self.components(as_int=True) - return component +"""The alissa distribution's version, loaded via the shared utility. - @property - def minor(self) -> int: - _, component, *_ = self.components(as_int=True) - return component - - @property - def patch(self) -> int: - *_, component = self.components(as_int=True) - return component - - @classmethod - def from_path(cls, dirpath: str, name: str): - for file in os.listdir(dirpath): - if file.lower().endswith("version"): - filepath = os.path.join(dirpath, file) - break - else: - raise ValueError("Version file not found for package name: " + name) - - with open(filepath, "r") as version_file: - version_value = version_file.readline().strip() - return cls(name=name, value=version_value) +This is the canonical example of the pattern every alissa.* distribution +follows — see :mod:`alissa.utils.version`. +""" +import os +from alissa.utils.version import Version -try: - version = Version.from_path(name="alissa", dirpath=os.path.dirname(__file__)) -except Exception: - print("Version file not found for package name: alissa, using 0.0.0") - version = Version(name="alissa", value="0.0.0") +version = Version.load(os.path.dirname(__file__), name="alissa") diff --git a/alissa/src/main/alissa/utils/__init__.py b/alissa/src/main/alissa/utils/__init__.py new file mode 100644 index 0000000..d2f7c2a --- /dev/null +++ b/alissa/src/main/alissa/utils/__init__.py @@ -0,0 +1,11 @@ +"""alissa.utils — shared helpers for the ``alissa.*`` ecosystem. + +The SDK factors these out so every ``alissa.*`` distribution (tools included) +reuses one implementation instead of copying it. Downstream packages may assume +``alissa`` is importable at runtime and should declare it as a dependency +(``install_requires=["alissa"]``); there is no dependency cycle, because the SDK +core never imports tool code. +""" +from .version import Version + +__all__ = ["Version"] diff --git a/alissa/src/main/alissa/utils/version.py b/alissa/src/main/alissa/utils/version.py new file mode 100644 index 0000000..e9286b8 --- /dev/null +++ b/alissa/src/main/alissa/utils/version.py @@ -0,0 +1,75 @@ +"""Load a distribution's version from its plain-text ``version`` file. + +Factored out so every ``alissa.*`` distribution reads its version identically +instead of re-implementing the loader. Ship a plain-text ``version`` file next +to your package and, in a ``version.py`` beside it: + + import os + from alissa.utils.version import Version + + version = Version.load(os.path.dirname(__file__), name="my-distribution") + +:meth:`Version.load` tolerates a missing file (warns, falls back to a default); +:meth:`Version.from_path` raises instead, when a missing file should be fatal. + +Downstream packages may treat this as stable, public API — declare ``alissa`` as +a dependency (``install_requires=["alissa"]``) to guarantee it is importable. +""" +import os +import warnings +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Version: + name: str + value: str + + def components(self, as_int: bool = False) -> list: + return [int(val) if as_int else val for val in self.value.split(".")] + + @property + def major(self) -> int: + component, *_ = self.components(as_int=True) + return component + + @property + def minor(self) -> int: + _, component, *_ = self.components(as_int=True) + return component + + @property + def patch(self) -> int: + *_, component = self.components(as_int=True) + return component + + def __str__(self) -> str: + return self.value + + @classmethod + def from_path(cls, dirpath: str, name: str) -> "Version": + """Read the version from the ``*version`` file in ``dirpath``. + + Raises ``ValueError`` when no such file exists. + """ + for file in os.listdir(dirpath): + if file.lower().endswith("version"): + filepath = os.path.join(dirpath, file) + break + else: + raise ValueError("Version file not found for package name: " + name) + + with open(filepath, "r") as version_file: + return cls(name=name, value=version_file.readline().strip()) + + @classmethod + def load(cls, dirpath: str, name: str, default: str = "0.0.0") -> "Version": + """Like :meth:`from_path`, but warn and fall back to ``default`` if absent.""" + try: + return cls.from_path(dirpath, name) + except Exception: + warnings.warn( + f"Version file not found for package name: {name}, using {default}", + stacklevel=2, + ) + return cls(name=name, value=default) diff --git a/alissa/src/test/test_alissa/test_utils/test_version.py b/alissa/src/test/test_alissa/test_utils/test_version.py new file mode 100644 index 0000000..cae9f76 --- /dev/null +++ b/alissa/src/test/test_alissa/test_utils/test_version.py @@ -0,0 +1,55 @@ +"""The shared version loader — the contract downstream alissa.* packages rely on. + +These lock the public behaviour of ``alissa.utils.version.Version``: parsing, +the strict vs. tolerant loaders, and the ``alissa.utils`` re-export. +""" +from __future__ import annotations + +import pytest + +from alissa.utils import Version as ReexportedVersion +from alissa.utils.version import Version + + +def test_reexported_from_the_utils_package(): + assert ReexportedVersion is Version + + +def test_from_path_reads_and_parses(tmp_path): + (tmp_path / "version").write_text("1.2.3\n") + + v = Version.from_path(str(tmp_path), name="demo") + + assert v.name == "demo" + assert v.value == "1.2.3" + assert str(v) == "1.2.3" + assert v.components(as_int=True) == [1, 2, 3] + assert (v.major, v.minor, v.patch) == (1, 2, 3) + + +def test_from_path_raises_when_absent(tmp_path): + with pytest.raises(ValueError): + Version.from_path(str(tmp_path), name="demo") + + +def test_load_reads_when_present(tmp_path): + (tmp_path / "version").write_text("4.5.6") + + v = Version.load(str(tmp_path), name="demo") + + assert (v.name, v.value) == ("demo", "4.5.6") + + +def test_load_falls_back_and_warns_when_absent(tmp_path): + with pytest.warns(UserWarning): + v = Version.load(str(tmp_path), name="demo") + + assert v.value == "0.0.0" + assert v.name == "demo" + + +def test_load_honors_a_custom_default(tmp_path): + with pytest.warns(UserWarning): + v = Version.load(str(tmp_path), name="demo", default="9.9.9") + + assert v.value == "9.9.9" From c70945e77f368debe75806f95d37e409e34f536a Mon Sep 17 00:00:00 2001 From: rhdzmota Date: Wed, 22 Jul 2026 01:49:10 -0600 Subject: [PATCH 4/4] Rename the extra to tools.github.revloop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public extra becomes `alissa[tools.github.revloop]`, ahead of renaming the source package to `revloop` (to pair with a planned `devloop`). Only the extra moves. The distribution and module stay `alissa-tools-github-reviewloop` / `alissa.tools.github.reviewloop` because that is what is published on PyPI — `alissa-tools-github-revloop` does not exist, so renaming them now would resolve to a missing distribution. The curated registry already decouples the extra name from both, so this is a one-line change there; when the source package is renamed, the other two values move with it. Verified: metadata records `Provides-Extra: tools-github-revloop` requiring alissa-tools-github-reviewloop, and discovery still resolves the module. TASK-450587736 Co-Authored-By: alissa-app Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 12 +++++++----- alissa/README.md | 13 +++++++++---- alissa/setup.py | 2 +- alissa/src/main/alissa/sdk/__init__.py | 2 +- alissa/src/main/alissa/sdk/__main__.py | 4 ++-- alissa/src/main/alissa/sdk/_tools.py | 11 ++++++++++- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index dd70f01..367627d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The single Python package that owns and anchors the `alissa` import namespace ```sh pip install alissa # SDK core, zero third-party deps -pip install 'alissa[tools.github.reviewloop]' # + a tool, merged into alissa.tools.* +pip install 'alissa[tools.github.revloop]' # + a tool, merged into alissa.tools.* pip install 'alissa[all]' # + every tool ``` @@ -27,10 +27,12 @@ alissa/ ← namespace (no __init__.py) ``` Each `alissa[tools..]` extra maps to a separately published -distribution (`alissa-tools--`). Tools stay independently versioned -and released; the SDK just curates which ones exist and pulls them in on demand. -See [`alissa/README.md`](./alissa/README.md) for the full model and for how to -add a new tool extra. +distribution. Tools stay independently versioned and released; the SDK just +curates which ones exist and pulls them in on demand. The mapping lives in a +registry, so an extra name need not match its distribution name — today +`tools.github.revloop` pulls `alissa-tools-github-reviewloop`, pending that +package's rename. See [`alissa/README.md`](./alissa/README.md) for the full +model and for how to add a new tool extra. ## Repository layout diff --git a/alissa/README.md b/alissa/README.md index f451b2a..b0136d3 100644 --- a/alissa/README.md +++ b/alissa/README.md @@ -32,15 +32,20 @@ and the one whose extras enumerate the available tools. ## Install ```sh -pip install alissa # SDK core only, zero third-party deps -pip install 'alissa[tools.github.reviewloop]' # + the GitHub review-loop tool -pip install 'alissa[all]' # + every tool extra +pip install alissa # SDK core only, zero third-party deps +pip install 'alissa[tools.github.revloop]' # + the GitHub review-loop tool +pip install 'alissa[all]' # + every tool extra ``` Each extra pulls in a separately published tool distribution (e.g. `alissa-tools-github-reviewloop`), which merges its `alissa.tools.*` packages into the namespace. pip normalizes the dotted extra name, so -`alissa[tools.github.reviewloop]` resolves. +`alissa[tools.github.revloop]` resolves. + +Extra names are decoupled from distribution names by the curated registry, so +they need not match: `tools.github.revloop` currently pulls +`alissa-tools-github-reviewloop`. That package will be renamed to `revloop` +later (to pair with a planned `devloop`); only the registry changes when it does. ### Editable / development install diff --git a/alissa/setup.py b/alissa/setup.py index b5d1bc3..f63957c 100644 --- a/alissa/setup.py +++ b/alissa/setup.py @@ -35,7 +35,7 @@ # Extras are the SDK's curation surface. Each extra pulls in a separately # published tool distribution that contributes its own alissa.tools.* subtree; # the SDK core itself carries no third-party dependencies. Install one with -# pip install 'alissa[tools.github.reviewloop]' +# pip install 'alissa[tools.github.revloop]' # pip normalizes the dotted extra name, so the dotted spelling above resolves. # # The curated tools live in the package's `_tools.py` so the extras and the diff --git a/alissa/src/main/alissa/sdk/__init__.py b/alissa/src/main/alissa/sdk/__init__.py index f0c2523..e63c771 100644 --- a/alissa/src/main/alissa/sdk/__init__.py +++ b/alissa/src/main/alissa/sdk/__init__.py @@ -6,7 +6,7 @@ package. That keeps every namespace level a `PEP 420 `_ implicit namespace package, so tool distributions pulled in through extras — for example -``pip install 'alissa[tools.github.reviewloop]'`` — merge their own +``pip install 'alissa[tools.github.revloop]'`` — merge their own ``alissa.tools.*`` subtrees into the same namespace at import time. The SDK's own surface lives here, under ``alissa.sdk``. Tools live under diff --git a/alissa/src/main/alissa/sdk/__main__.py b/alissa/src/main/alissa/sdk/__main__.py index a97db91..71f4a7e 100644 --- a/alissa/src/main/alissa/sdk/__main__.py +++ b/alissa/src/main/alissa/sdk/__main__.py @@ -35,12 +35,12 @@ def main() -> None: print(name) else: print("no alissa.tools.* packages installed") - print("try: pip install 'alissa[tools.github.reviewloop]'") + print("try: pip install 'alissa[tools.github.revloop]'") return print(f"alissa {__version__}") print("the Alissa Python SDK — anchors the 'alissa' namespace") - print("install tools via extras, e.g.: pip install 'alissa[tools.github.reviewloop]'") + print("install tools via extras, e.g.: pip install 'alissa[tools.github.revloop]'") print("list installed tools: alissa-py --tools") diff --git a/alissa/src/main/alissa/sdk/_tools.py b/alissa/src/main/alissa/sdk/_tools.py index 71eda8a..c5b3172 100644 --- a/alissa/src/main/alissa/sdk/_tools.py +++ b/alissa/src/main/alissa/sdk/_tools.py @@ -10,10 +10,19 @@ which curated tools are actually installed. To curate a new tool, add one line here — nothing else needs editing. + +Note the indirection is deliberate: the public extra name is decoupled from the +distribution and module names, so a tool can be renamed on the SDK side before +(or independently of) its source package. """ CURATED_TOOLS = { - "tools.github.reviewloop": ( + # The extra is `revloop`; the distribution and module are still `reviewloop` + # because that is what is published on PyPI. The source package will be + # renamed to `revloop` later — to pair with a planned `devloop` — and the + # two values below move with it then. Renaming them before the rename ships + # would resolve to a distribution that does not exist. + "tools.github.revloop": ( "alissa-tools-github-reviewloop", "alissa.tools.github.reviewloop", ),