diff --git a/.gitignore b/.gitignore index da055c2..b74d171 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,8 @@ wheels/ .devenv.flake.nix .pre-commit-config.yaml -# bulkgen state -.*.bulkgen-state.yaml +# hokusai state +.*.hokusai-state.yaml # Nix result diff --git a/.python-version b/.python-version index 6324d40..24ee5b1 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 +3.13 diff --git a/CLAUDE.md b/CLAUDE.md index c59b9d3..3a65f43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,17 +1,17 @@ -# CLAUDE.md - bulkgen development guide +# CLAUDE.md - hokusai development guide ## Project overview -bulkgen is a `make`-like build tool for AI-generated artifacts (images and text). A YAML config file defines targets with dependencies; bulkgen builds a DAG with networkx and executes generation in parallel topological order using Mistral (text) and BlackForestLabs (images) as providers. +hokusai is a `make`-like build tool for AI-generated artifacts (images and text). A YAML config file defines targets with dependencies; hokusai builds a DAG with networkx and executes generation in parallel topological order using Mistral (text) and BlackForestLabs (images) as providers. ## Commands ```bash uv sync # install dependencies -uv run bulkgen build # build all targets -uv run bulkgen build X # build target X and its transitive deps -uv run bulkgen clean # remove generated artifacts + state file -uv run bulkgen graph # print dependency graph with stages +uv run hokusai build # build all targets +uv run hokusai build X # build target X and its transitive deps +uv run hokusai clean # remove generated artifacts + state file +uv run hokusai graph # print dependency graph with stages uv run pytest # run tests ``` @@ -45,14 +45,14 @@ ruff format --check ### Module structure ``` -main.py # Entry point: imports and runs bulkgen.cli.app -bulkgen/ +main.py # Entry point: imports and runs hokusai.cli.app +hokusai/ __init__.py cli.py # Typer CLI: build, clean, graph commands config.py # Pydantic models for YAML config graph.py # networkx DAG construction and traversal builder.py # Build orchestrator: incremental + parallel - state.py # .bulkgen.state.yaml hash tracking + state.py # .hokusai.state.yaml hash tracking providers/ __init__.py # Abstract Provider base class (ABC) image.py # BlackForestLabs image generation @@ -61,7 +61,7 @@ bulkgen/ ### Data flow -1. **cli.py** finds the `*.bulkgen.yaml` in cwd, calls `load_config()` from `config.py` +1. **cli.py** finds the `*.hokusai.yaml` in cwd, calls `load_config()` from `config.py` 2. **config.py** parses YAML into `ProjectConfig` (pydantic), which contains `Defaults` and `dict[str, TargetConfig]` 3. **graph.py** builds an `nx.DiGraph` from target dependencies. `get_build_order()` uses `nx.topological_generations()` to return parallel batches 4. **builder.py** `run_build()` iterates generations. Per generation: @@ -77,13 +77,13 @@ bulkgen/ - **Prompt resolution**: if the `prompt` string is a path to an existing file, its contents are read; otherwise it's used as-is. Done in `builder.py:_resolve_prompt()`. - **BFL client is synchronous**: wrapped in `asyncio.to_thread()` in `providers/image.py`. Uses `ClientConfig(sync=True, timeout=300)` for internal polling. - **Mistral client is natively async**: uses `complete_async()` directly in `providers/text.py`. -- **Incremental builds**: `.bulkgen.state.yaml` tracks per-target: input file hashes, prompt hash, model name, and extra params hash. Any change marks the target dirty. +- **Incremental builds**: `.hokusai.state.yaml` tracks per-target: input file hashes, prompt hash, model name, and extra params hash. Any change marks the target dirty. - **Error isolation**: if a target fails, its dependents are marked "Dependency failed" but independent targets continue building. - **State saved per-generation**: partial progress survives crashes. At most one generation of work is lost. ### Provider interface -All providers implement `bulkgen.providers.Provider`: +All providers implement `hokusai.providers.Provider`: ```python async def generate(self, target_name, target_config, resolved_prompt, resolved_model, project_dir) -> None ``` diff --git a/README.md b/README.md index 3cac101..7a98612 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# bulkgen +# hokusai -A build tool for AI-generated artifacts. Define image and text targets in a YAML config, and bulkgen handles dependency resolution, incremental builds, and parallel execution. +A build tool for AI-generated artifacts. Define image and text targets in a YAML config, and hokusai handles dependency resolution, incremental builds, and parallel execution. Uses [Mistral](https://mistral.ai) and [OpenAI](https://openai.com) for text generation, and [BlackForestLabs](https://blackforestlabs.ai) (FLUX) and [OpenAI](https://openai.com) for image generation. +The name Hokusai was chosen in honor of [Katsushika Hokusai](https://en.wikipedia.org/wiki/Hokusai), who produced over 30,000 paintings, sketches, woodblock prints, and images for picture books, many in larger series. + ## Installation Requires Python 3.13+. @@ -28,7 +30,7 @@ export BFL_API_KEY="your-key" export OPENAI_API_KEY="your-key" ``` -2. Create a config file (e.g. `my-project.bulkgen.yaml`): +2. Create a config file (e.g. `my-project.hokusai.yaml`): ```yaml defaults: @@ -51,12 +53,12 @@ targets: 3. Build: ```bash -bulkgen build +hokusai build ``` ## Config format -The config file must be named `.bulkgen.yaml` and placed in your project directory. One config file per directory. +The config file must be named `.hokusai.yaml` and placed in your project directory. One config file per directory. ### Top-level fields @@ -127,7 +129,7 @@ targets: - research-notes.md # depends on an existing file ``` -bulkgen resolves dependencies automatically. If you build a single target, its transitive dependencies are included. +hokusai resolves dependencies automatically. If you build a single target, its transitive dependencies are included. ### Archiving previous outputs @@ -145,7 +147,7 @@ On each rebuild of `hero.png`, the previous file is archived as `archive/hero.01 ## CLI -### `bulkgen build [target]` +### `hokusai build [target]` Build all targets, or a specific target and its dependencies. @@ -153,11 +155,11 @@ Build all targets, or a specific target and its dependencies. - Runs independent targets in parallel - Continues building if a target fails (dependents of the failed target are skipped) -### `bulkgen clean` +### `hokusai clean` -Remove all generated target files and the build state file (`.bulkgen.state.yaml`). Input files are preserved. +Remove all generated target files and the build state file (`.hokusai.state.yaml`). Input files are preserved. -### `bulkgen graph` +### `hokusai graph` Print the dependency graph showing build stages: @@ -171,7 +173,7 @@ Stage 1 (targets): variant.png, summary.md ## Incremental builds -bulkgen tracks the state of each build in `.bulkgen.state.yaml` (auto-generated, add to `.gitignore`). A target is rebuilt when any of these change: +hokusai tracks the state of each build in `.hokusai.state.yaml` (auto-generated, add to `.gitignore`). A target is rebuilt when any of these change: - Input file contents (SHA-256 hash) - Prompt text @@ -180,7 +182,7 @@ bulkgen tracks the state of each build in `.bulkgen.state.yaml` (auto-generated, ## Installation with Nix / home-manager -bulkgen provides a Nix flake with a home-manager module. Add the flake as an input and enable the module: +hokusai provides a Nix flake with a home-manager module. Add the flake as an input and enable the module: ```nix # flake.nix @@ -188,17 +190,17 @@ bulkgen provides a Nix flake with a home-manager module. Add the flake as an inp inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; home-manager.url = "github:nix-community/home-manager"; - bulkgen.url = "github:kfickel/bulkgen"; # adjust to your actual repo URL + hokusai.url = "github:kfickel/hokusai"; # adjust to your actual repo URL }; - outputs = { nixpkgs, home-manager, bulkgen, ... }: { + outputs = { nixpkgs, home-manager, hokusai, ... }: { # ... your existing config, then in homeConfigurations: homeConfigurations."user" = home-manager.lib.homeManagerConfiguration { # ... modules = [ - bulkgen.homeManagerModules.bulkgen + hokusai.homeManagerModules.hokusai { - programs.bulkgen.enable = true; + programs.hokusai.enable = true; } ]; }; @@ -206,11 +208,11 @@ bulkgen provides a Nix flake with a home-manager module. Add the flake as an inp } ``` -This places the `bulkgen` binary on your `$PATH`. To use a different package build (e.g. from a different system or overlay), set `programs.bulkgen.package`. +This places the `hokusai` binary on your `$PATH`. To use a different package build (e.g. from a different system or overlay), set `programs.hokusai.package`. The flake also exposes: -- `packages..bulkgen` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/bulkgen`) +- `packages..hokusai` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/hokusai`) - `devShells..default` — development shell with all dependencies ## Environment variables diff --git a/flake.nix b/flake.nix index d4b9400..f3a4251 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "bulkgen - Bulk-Generate Images with Generative AI"; + description = "hokusai - Bulk-Generate Images with Generative AI"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -63,15 +63,15 @@ }; pyprojectOverrides = _final: prev: { - bulkgen = prev.bulkgen.overrideAttrs (old: { + hokusai = prev.hokusai.overrideAttrs (old: { passthru = old.passthru // { tests = (old.passthru.tests or { }) // { pytest = stdenv.mkDerivation { - name = "${_final.bulkgen.name}-pytest"; - inherit (_final.bulkgen) src; + name = "${_final.hokusai.name}-pytest"; + inherit (_final.hokusai) src; nativeBuildInputs = [ - (_final.mkVirtualEnv "bulkgen-pytest-env" { - bulkgen = [ "dev" ]; + (_final.mkVirtualEnv "hokusai-pytest-env" { + hokusai = [ "dev" ]; }) ]; dontConfigure = true; @@ -108,7 +108,7 @@ let pkgs = nixpkgs.legacyPackages.${system}; pythonSet = pythonSets.${system}; - venv = pythonSet.mkVirtualEnv "bulkgen-check-env" workspace.deps.default; + venv = pythonSet.mkVirtualEnv "hokusai-check-env" workspace.deps.default; in git-hooks.lib.${system}.run { src = ./.; @@ -142,17 +142,17 @@ inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication; in rec { - bulkgen = mkApplication { - venv = pythonSet.mkVirtualEnv "bulkgen-env" workspace.deps.default; - package = pythonSet.bulkgen; + hokusai = mkApplication { + venv = pythonSet.mkVirtualEnv "hokusai-env" workspace.deps.default; + package = pythonSet.hokusai; }; - default = bulkgen; + default = hokusai; } ); homeManagerModules = rec { - bulkgen = import ./nix/hm-module.nix self; - default = bulkgen; + hokusai = import ./nix/hm-module.nix self; + default = hokusai; }; checks = forAllSystems ( @@ -161,7 +161,7 @@ pythonSet = pythonSets.${system}; in { - inherit (pythonSet.bulkgen.passthru.tests) pytest; + inherit (pythonSet.hokusai.passthru.tests) pytest; pre-commit = mkGitHooksCheck system; } ); @@ -171,7 +171,7 @@ let pkgs = nixpkgs.legacyPackages.${system}; pythonSet = pythonSets.${system}.overrideScope editableOverlay; - virtualenv = pythonSet.mkVirtualEnv "bulkgen-dev-env" workspace.deps.all; + virtualenv = pythonSet.mkVirtualEnv "hokusai-dev-env" workspace.deps.all; in { default = pkgs.mkShell { diff --git a/bulkgen/__init__.py b/hokusai/__init__.py similarity index 100% rename from bulkgen/__init__.py rename to hokusai/__init__.py diff --git a/bulkgen/builder.py b/hokusai/builder.py similarity index 95% rename from bulkgen/builder.py rename to hokusai/builder.py index 9005f11..7e3a22d 100644 --- a/bulkgen/builder.py +++ b/hokusai/builder.py @@ -9,15 +9,15 @@ from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from bulkgen.config import ProjectConfig -from bulkgen.graph import build_graph, get_build_order, get_subgraph_for_target -from bulkgen.providers import Provider -from bulkgen.providers.blackforest import BlackForestProvider -from bulkgen.providers.mistral import MistralProvider -from bulkgen.providers.openai_image import OpenAIImageProvider -from bulkgen.providers.openai_text import OpenAITextProvider -from bulkgen.resolve import resolve_model -from bulkgen.state import ( +from hokusai.config import ProjectConfig +from hokusai.graph import build_graph, get_build_order, get_subgraph_for_target +from hokusai.providers import Provider +from hokusai.providers.blackforest import BlackForestProvider +from hokusai.providers.mistral import MistralProvider +from hokusai.providers.openai_image import OpenAIImageProvider +from hokusai.providers.openai_text import OpenAITextProvider +from hokusai.resolve import resolve_model +from hokusai.state import ( BuildState, is_target_dirty, load_state, diff --git a/bulkgen/cli.py b/hokusai/cli.py similarity index 90% rename from bulkgen/cli.py rename to hokusai/cli.py index af991be..26e4ef5 100644 --- a/bulkgen/cli.py +++ b/hokusai/cli.py @@ -1,4 +1,4 @@ -"""Typer CLI for bulkgen: build, clean, graph commands.""" +"""Typer CLI for hokusai: build, clean, graph commands.""" from __future__ import annotations @@ -10,33 +10,33 @@ from typing import Annotated import click import typer -from bulkgen.builder import BuildEvent, BuildResult, run_build -from bulkgen.config import ProjectConfig, load_config -from bulkgen.graph import build_graph, get_build_order -from bulkgen.providers.registry import get_all_models -from bulkgen.state import state_filename +from hokusai.builder import BuildEvent, BuildResult, run_build +from hokusai.config import ProjectConfig, load_config +from hokusai.graph import build_graph, get_build_order +from hokusai.providers.registry import get_all_models +from hokusai.state import state_filename -app = typer.Typer(name="bulkgen", help="AI artifact build tool.") +app = typer.Typer(name="hokusai", help="AI artifact build tool.") -_CONFIG_SUFFIX = ".bulkgen.yaml" +_CONFIG_SUFFIX = ".hokusai.yaml" def _project_name(config_path: Path) -> str: """Derive the project name from a config path. - ``cards.bulkgen.yaml`` → ``cards`` + ``cards.hokusai.yaml`` → ``cards`` """ name = config_path.name return name.removesuffix(_CONFIG_SUFFIX) def _find_config(directory: Path) -> Path: - """Find the single ``*.bulkgen.yaml`` file in *directory*.""" - candidates = list(directory.glob("*.bulkgen.yaml")) + """Find the single ``*.hokusai.yaml`` file in *directory*.""" + candidates = list(directory.glob("*.hokusai.yaml")) if len(candidates) == 0: click.echo( click.style("Error: ", fg="red", bold=True) - + "No .bulkgen.yaml file found in current directory", + + "No .hokusai.yaml file found in current directory", err=True, ) raise typer.Exit(code=1) @@ -44,7 +44,7 @@ def _find_config(directory: Path) -> Path: names = ", ".join(str(c.name) for c in candidates) click.echo( click.style("Error: ", fg="red", bold=True) - + f"Multiple .bulkgen.yaml files found: {names}", + + f"Multiple .hokusai.yaml files found: {names}", err=True, ) raise typer.Exit(code=1) @@ -116,7 +116,7 @@ def build( config = load_config(config_path) name = _project_name(config_path) - click.echo(click.style("bulkgen", fg="cyan", bold=True) + " building targets...\n") + click.echo(click.style("hokusai", fg="cyan", bold=True) + " building targets...\n") result, elapsed = _run_build(config, project_dir, name, target) diff --git a/bulkgen/config.py b/hokusai/config.py similarity index 86% rename from bulkgen/config.py rename to hokusai/config.py index 20b929d..76c293f 100644 --- a/bulkgen/config.py +++ b/hokusai/config.py @@ -1,4 +1,4 @@ -"""Pydantic models for bulkgen YAML configuration.""" +"""Pydantic models for hokusai YAML configuration.""" from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Self import yaml from pydantic import BaseModel, model_validator -from bulkgen.providers.models import Capability +from hokusai.providers.models import Capability IMAGE_EXTENSIONS: frozenset[str] = frozenset({".png", ".jpg", ".jpeg", ".webp"}) TEXT_EXTENSIONS: frozenset[str] = frozenset({".md", ".txt"}) @@ -42,7 +42,7 @@ class TargetConfig(BaseModel): class ProjectConfig(BaseModel): - """Top-level configuration parsed from ``.bulkgen.yaml``.""" + """Top-level configuration parsed from ``.hokusai.yaml``.""" defaults: Defaults = Defaults() targets: dict[str, TargetConfig] @@ -57,7 +57,7 @@ class ProjectConfig(BaseModel): def target_type_from_capabilities(capabilities: frozenset[Capability]) -> TargetType: """Derive the target type from a set of required capabilities.""" - from bulkgen.providers.models import Capability + from hokusai.providers.models import Capability if Capability.TEXT_TO_IMAGE in capabilities: return TargetType.IMAGE @@ -65,7 +65,7 @@ def target_type_from_capabilities(capabilities: frozenset[Capability]) -> Target def load_config(config_path: Path) -> ProjectConfig: - """Load and validate a ``.bulkgen.yaml`` file.""" + """Load and validate a ``.hokusai.yaml`` file.""" with config_path.open() as f: raw = yaml.safe_load(f) # pyright: ignore[reportAny] return ProjectConfig.model_validate(raw) diff --git a/bulkgen/graph.py b/hokusai/graph.py similarity index 98% rename from bulkgen/graph.py rename to hokusai/graph.py index 7ed15e4..3d83a29 100644 --- a/bulkgen/graph.py +++ b/hokusai/graph.py @@ -6,7 +6,7 @@ from pathlib import Path import networkx as nx -from bulkgen.config import ProjectConfig +from hokusai.config import ProjectConfig def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph[str]: diff --git a/bulkgen/providers/__init__.py b/hokusai/providers/__init__.py similarity index 92% rename from bulkgen/providers/__init__.py rename to hokusai/providers/__init__.py index 71f9b89..b91dc3b 100644 --- a/bulkgen/providers/__init__.py +++ b/hokusai/providers/__init__.py @@ -6,10 +6,10 @@ import abc from pathlib import Path from typing import TYPE_CHECKING -from bulkgen.providers.models import ModelInfo +from hokusai.providers.models import ModelInfo if TYPE_CHECKING: - from bulkgen.config import TargetConfig + from hokusai.config import TargetConfig class Provider(abc.ABC): diff --git a/bulkgen/providers/bfl.py b/hokusai/providers/bfl.py similarity index 100% rename from bulkgen/providers/bfl.py rename to hokusai/providers/bfl.py diff --git a/bulkgen/providers/blackforest.py b/hokusai/providers/blackforest.py similarity index 96% rename from bulkgen/providers/blackforest.py rename to hokusai/providers/blackforest.py index e94e250..d139964 100644 --- a/bulkgen/providers/blackforest.py +++ b/hokusai/providers/blackforest.py @@ -8,10 +8,10 @@ from typing import override import httpx -from bulkgen.config import TargetConfig -from bulkgen.providers import Provider -from bulkgen.providers.bfl import BFLClient -from bulkgen.providers.models import Capability, ModelInfo +from hokusai.config import TargetConfig +from hokusai.providers import Provider +from hokusai.providers.bfl import BFLClient +from hokusai.providers.models import Capability, ModelInfo def _encode_image_b64(path: Path) -> str: diff --git a/bulkgen/providers/mistral.py b/hokusai/providers/mistral.py similarity index 95% rename from bulkgen/providers/mistral.py rename to hokusai/providers/mistral.py index c8be302..03a0de8 100644 --- a/bulkgen/providers/mistral.py +++ b/hokusai/providers/mistral.py @@ -9,9 +9,9 @@ from typing import override from mistralai import Mistral, models -from bulkgen.config import IMAGE_EXTENSIONS, TargetConfig -from bulkgen.providers import Provider -from bulkgen.providers.models import Capability, ModelInfo +from hokusai.config import IMAGE_EXTENSIONS, TargetConfig +from hokusai.providers import Provider +from hokusai.providers.models import Capability, ModelInfo def _image_to_data_url(path: Path) -> str: @@ -128,7 +128,7 @@ def _build_multimodal_message( models.TextChunk(text=prompt), ] - from bulkgen.config import IMAGE_EXTENSIONS + from hokusai.config import IMAGE_EXTENSIONS for name in input_names: input_path = project_dir / name diff --git a/bulkgen/providers/models.py b/hokusai/providers/models.py similarity index 100% rename from bulkgen/providers/models.py rename to hokusai/providers/models.py diff --git a/bulkgen/providers/openai_image.py b/hokusai/providers/openai_image.py similarity index 98% rename from bulkgen/providers/openai_image.py rename to hokusai/providers/openai_image.py index fe5bbfb..398b64e 100644 --- a/bulkgen/providers/openai_image.py +++ b/hokusai/providers/openai_image.py @@ -10,9 +10,9 @@ import httpx from openai import AsyncOpenAI from openai.types.images_response import ImagesResponse -from bulkgen.config import TargetConfig -from bulkgen.providers import Provider -from bulkgen.providers.models import Capability, ModelInfo +from hokusai.config import TargetConfig +from hokusai.providers import Provider +from hokusai.providers.models import Capability, ModelInfo _SIZE = Literal[ "auto", diff --git a/bulkgen/providers/openai_text.py b/hokusai/providers/openai_text.py similarity index 97% rename from bulkgen/providers/openai_text.py rename to hokusai/providers/openai_text.py index b1006c4..d205aa2 100644 --- a/bulkgen/providers/openai_text.py +++ b/hokusai/providers/openai_text.py @@ -15,9 +15,9 @@ from openai.types.chat import ( ChatCompletionUserMessageParam, ) -from bulkgen.config import IMAGE_EXTENSIONS, TargetConfig -from bulkgen.providers import Provider -from bulkgen.providers.models import Capability, ModelInfo +from hokusai.config import IMAGE_EXTENSIONS, TargetConfig +from hokusai.providers import Provider +from hokusai.providers.models import Capability, ModelInfo def _image_to_data_url(path: Path) -> str: diff --git a/bulkgen/providers/registry.py b/hokusai/providers/registry.py similarity index 58% rename from bulkgen/providers/registry.py rename to hokusai/providers/registry.py index 9ac12b8..0161019 100644 --- a/bulkgen/providers/registry.py +++ b/hokusai/providers/registry.py @@ -2,15 +2,15 @@ from __future__ import annotations -from bulkgen.providers.models import ModelInfo +from hokusai.providers.models import ModelInfo def get_all_models() -> list[ModelInfo]: """Return the merged list of models from all providers.""" - from bulkgen.providers.blackforest import BlackForestProvider - from bulkgen.providers.mistral import MistralProvider - from bulkgen.providers.openai_image import OpenAIImageProvider - from bulkgen.providers.openai_text import OpenAITextProvider + from hokusai.providers.blackforest import BlackForestProvider + from hokusai.providers.mistral import MistralProvider + from hokusai.providers.openai_image import OpenAIImageProvider + from hokusai.providers.openai_text import OpenAITextProvider return ( MistralProvider.get_provided_models() diff --git a/bulkgen/resolve.py b/hokusai/resolve.py similarity index 95% rename from bulkgen/resolve.py rename to hokusai/resolve.py index 1e2b48b..63ee2cc 100644 --- a/bulkgen/resolve.py +++ b/hokusai/resolve.py @@ -2,7 +2,7 @@ from __future__ import annotations from pathlib import Path -from bulkgen.config import ( +from hokusai.config import ( IMAGE_EXTENSIONS, TEXT_EXTENSIONS, Defaults, @@ -10,7 +10,7 @@ from bulkgen.config import ( TargetType, target_type_from_capabilities, ) -from bulkgen.providers.models import Capability, ModelInfo +from hokusai.providers.models import Capability, ModelInfo def infer_required_capabilities( @@ -53,7 +53,7 @@ def resolve_model( Raises :class:`ValueError` if no suitable model can be found. """ - from bulkgen.providers.registry import get_all_models + from hokusai.providers.registry import get_all_models all_models = get_all_models() required = infer_required_capabilities(target_name, target) diff --git a/bulkgen/state.py b/hokusai/state.py similarity index 93% rename from bulkgen/state.py rename to hokusai/state.py index 5e87b1e..d10b955 100644 --- a/bulkgen/state.py +++ b/hokusai/state.py @@ -1,4 +1,4 @@ -"""Incremental build state tracking via ``..bulkgen-state.yaml``.""" +"""Incremental build state tracking via ``..hokusai-state.yaml``.""" from __future__ import annotations @@ -12,10 +12,10 @@ from pydantic import BaseModel def state_filename(project_name: str) -> str: """Return the state filename for a given project name. - For a config file named ``cards.bulkgen.yaml`` the project name is - ``cards`` and the state file is ``.cards.bulkgen-state.yaml``. + For a config file named ``cards.hokusai.yaml`` the project name is + ``cards`` and the state file is ``.cards.hokusai-state.yaml``. """ - return f".{project_name}.bulkgen-state.yaml" + return f".{project_name}.hokusai-state.yaml" class TargetState(BaseModel): diff --git a/main.py b/main.py index 816f76c..8cdbc1c 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ -"""Entry point for the bulkgen CLI.""" +"""Entry point for the hokusai CLI.""" -from bulkgen.cli import app +from hokusai.cli import app def main() -> None: diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 33fc0b2..90f5d38 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -6,12 +6,12 @@ self: ... }: let - cfg = config.programs.bulkgen; + cfg = config.programs.hokusai; in { - options.programs.bulkgen = { - enable = lib.mkEnableOption "bulkgen"; - package = lib.mkPackageOption self.packages.${pkgs.system} "bulkgen" { }; + options.programs.hokusai = { + enable = lib.mkEnableOption "hokusai"; + package = lib.mkPackageOption self.packages.${pkgs.system} "hokusai" { }; }; config = lib.mkIf cfg.enable { diff --git a/pyproject.toml b/pyproject.toml index 3c7abd6..4430c1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "bulkgen" +name = "hokusai" version = "0.1.0" description = "Bulk-Generate Images with Generative AI" readme = "README.md" @@ -16,7 +16,7 @@ dependencies = [ ] [project.scripts] -bulkgen = "bulkgen.cli:app" +hokusai = "hokusai.cli:app" [build-system] requires = ["hatchling"] diff --git a/tests/conftest.py b/tests/conftest.py index d29c5be..37ee407 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -"""Shared fixtures for bulkgen integration tests.""" +"""Shared fixtures for hokusai integration tests.""" from __future__ import annotations @@ -8,7 +8,7 @@ from pathlib import Path import pytest import yaml -from bulkgen.config import ProjectConfig, load_config +from hokusai.config import ProjectConfig, load_config WriteConfig = Callable[[dict[str, object]], ProjectConfig] @@ -21,7 +21,7 @@ def project_dir(tmp_path: Path) -> Path: @pytest.fixture def write_config(project_dir: Path) -> WriteConfig: - """Write a bulkgen YAML config and return the loaded ProjectConfig. + """Write a hokusai YAML config and return the loaded ProjectConfig. Usage:: @@ -29,7 +29,7 @@ def write_config(project_dir: Path) -> WriteConfig: """ def _write(raw: dict[str, object]) -> ProjectConfig: - config_path = project_dir / "project.bulkgen.yaml" + config_path = project_dir / "project.hokusai.yaml" _ = config_path.write_text(yaml.dump(raw, default_flow_style=False)) return load_config(config_path) diff --git a/tests/test_builder.py b/tests/test_builder.py index 867e7fe..70031a9 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.builder.""" +"""Integration tests for hokusai.builder.""" from __future__ import annotations @@ -9,17 +9,17 @@ from unittest.mock import patch import pytest -from bulkgen.builder import ( +from hokusai.builder import ( _collect_all_deps, # pyright: ignore[reportPrivateUsage] _collect_dep_files, # pyright: ignore[reportPrivateUsage] _collect_extra_params, # pyright: ignore[reportPrivateUsage] _resolve_prompt, # pyright: ignore[reportPrivateUsage] run_build, ) -from bulkgen.config import ProjectConfig, TargetConfig -from bulkgen.providers import Provider -from bulkgen.providers.models import Capability, ModelInfo -from bulkgen.state import load_state +from hokusai.config import ProjectConfig, TargetConfig +from hokusai.providers import Provider +from hokusai.providers.models import Capability, ModelInfo +from hokusai.state import load_state WriteConfig = Callable[[dict[str, object]], ProjectConfig] @@ -197,7 +197,7 @@ class TestRunBuild: async def test_build_single_text_target( self, project_dir: Path, simple_text_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result = await run_build(simple_text_config, project_dir, _PROJECT) assert result.built == ["output.txt"] @@ -208,7 +208,7 @@ class TestRunBuild: async def test_build_chain_dependency( self, project_dir: Path, multi_target_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result = await run_build(multi_target_config, project_dir, _PROJECT) assert "summary.md" in result.built @@ -223,7 +223,7 @@ class TestRunBuild: async def test_incremental_build_skips_clean_targets( self, project_dir: Path, simple_text_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result1 = await run_build(simple_text_config, project_dir, _PROJECT) assert result1.built == ["output.txt"] @@ -235,7 +235,7 @@ class TestRunBuild: self, project_dir: Path, write_config: WriteConfig ) -> None: config1 = write_config({"targets": {"out.txt": {"prompt": "version 1"}}}) - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): r1 = await run_build(config1, project_dir, _PROJECT) assert r1.built == ["out.txt"] @@ -250,7 +250,7 @@ class TestRunBuild: config = write_config( {"targets": {"out.md": {"prompt": "x", "inputs": ["data.txt"]}}} ) - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): r1 = await run_build(config, project_dir, _PROJECT) assert r1.built == ["out.md"] @@ -261,7 +261,7 @@ class TestRunBuild: async def test_selective_build_single_target( self, project_dir: Path, multi_target_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result = await run_build( multi_target_config, project_dir, _PROJECT, target="summary.md" ) @@ -273,7 +273,7 @@ class TestRunBuild: async def test_selective_build_unknown_target_raises( self, project_dir: Path, simple_text_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): with pytest.raises(ValueError, match="Unknown target"): _ = await run_build( simple_text_config, project_dir, _PROJECT, target="nonexistent.txt" @@ -321,7 +321,7 @@ class TestRunBuild: routing_provider.generate = selective_generate # type: ignore[assignment] with patch( - "bulkgen.builder._create_providers", + "hokusai.builder._create_providers", return_value=[routing_provider, FakeImageProvider()], ): result = await run_build(config, project_dir, _PROJECT) @@ -342,7 +342,7 @@ class TestRunBuild: ) with patch( - "bulkgen.builder._create_providers", + "hokusai.builder._create_providers", return_value=[FailingTextProvider(), FakeImageProvider()], ): result = await run_build(config, project_dir, _PROJECT) @@ -355,7 +355,7 @@ class TestRunBuild: self, project_dir: Path, simple_text_config: ProjectConfig ) -> None: with patch( - "bulkgen.builder._create_providers", + "hokusai.builder._create_providers", return_value=[], ): result = await run_build(simple_text_config, project_dir, _PROJECT) @@ -374,7 +374,7 @@ class TestRunBuild: } } ) - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): _ = await run_build(config, project_dir, _PROJECT) state = load_state(project_dir, _PROJECT) @@ -386,7 +386,7 @@ class TestRunBuild: ) -> None: config = write_config({"targets": {"out.txt": {"prompt": prompt_file.name}}}) - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result = await run_build(config, project_dir, _PROJECT) assert result.built == ["out.txt"] @@ -396,7 +396,7 @@ class TestRunBuild: async def test_rebuild_after_output_deleted( self, project_dir: Path, simple_text_config: ProjectConfig ) -> None: - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): r1 = await run_build(simple_text_config, project_dir, _PROJECT) assert r1.built == ["output.txt"] @@ -421,7 +421,7 @@ class TestRunBuild: } } ) - with patch("bulkgen.builder._create_providers", return_value=_fake_providers()): + with patch("hokusai.builder._create_providers", return_value=_fake_providers()): result = await run_build(config, project_dir, _PROJECT) assert set(result.built) == {"left.md", "right.md", "merge.txt"} diff --git a/tests/test_cli.py b/tests/test_cli.py index 9c00cd9..1d2dca2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.cli. +"""Integration tests for hokusai.cli. Patching ``Path.cwd()`` produces Any-typed return values from mock objects. """ @@ -13,8 +13,8 @@ import pytest import yaml from typer.testing import CliRunner -from bulkgen.builder import BuildResult -from bulkgen.cli import app +from hokusai.builder import BuildResult +from hokusai.cli import app runner = CliRunner() @@ -28,7 +28,7 @@ def cli_project(tmp_path: Path) -> Path: "image.png": {"prompt": "Generate image"}, } } - _ = (tmp_path / "project.bulkgen.yaml").write_text( + _ = (tmp_path / "project.hokusai.yaml").write_text( yaml.dump(config, default_flow_style=False) ) return tmp_path @@ -38,24 +38,24 @@ class TestFindConfig: """Test config file discovery.""" def test_no_config_file(self, tmp_path: Path) -> None: - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = tmp_path result = runner.invoke(app, ["build"]) assert result.exit_code != 0 - assert "No .bulkgen.yaml file found" in result.output + assert "No .hokusai.yaml file found" in result.output def test_multiple_config_files(self, tmp_path: Path) -> None: - _ = (tmp_path / "a.bulkgen.yaml").write_text( + _ = (tmp_path / "a.hokusai.yaml").write_text( yaml.dump({"targets": {"x.txt": {"prompt": "a"}}}) ) - _ = (tmp_path / "b.bulkgen.yaml").write_text( + _ = (tmp_path / "b.hokusai.yaml").write_text( yaml.dump({"targets": {"y.txt": {"prompt": "b"}}}) ) - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = tmp_path result = runner.invoke(app, ["build"]) assert result.exit_code != 0 - assert "Multiple .bulkgen.yaml files found" in result.output + assert "Multiple .hokusai.yaml files found" in result.output class TestBuildCommand: @@ -66,9 +66,9 @@ class TestBuildCommand: built=["output.txt", "image.png"], skipped=[], failed={} ) with ( - patch("bulkgen.cli.Path") as mock_path_cls, + patch("hokusai.cli.Path") as mock_path_cls, patch( - "bulkgen.cli.run_build", + "hokusai.cli.run_build", new_callable=AsyncMock, return_value=build_result, ), @@ -84,9 +84,9 @@ class TestBuildCommand: built=[], skipped=["output.txt", "image.png"], failed={} ) with ( - patch("bulkgen.cli.Path") as mock_path_cls, + patch("hokusai.cli.Path") as mock_path_cls, patch( - "bulkgen.cli.run_build", + "hokusai.cli.run_build", new_callable=AsyncMock, return_value=build_result, ), @@ -104,9 +104,9 @@ class TestBuildCommand: failed={"image.png": "Missing BFL_API_KEY"}, ) with ( - patch("bulkgen.cli.Path") as mock_path_cls, + patch("hokusai.cli.Path") as mock_path_cls, patch( - "bulkgen.cli.run_build", + "hokusai.cli.run_build", new_callable=AsyncMock, return_value=build_result, ), @@ -120,9 +120,9 @@ class TestBuildCommand: def test_build_specific_target(self, cli_project: Path) -> None: build_result = BuildResult(built=["output.txt"], skipped=[], failed={}) with ( - patch("bulkgen.cli.Path") as mock_path_cls, + patch("hokusai.cli.Path") as mock_path_cls, patch( - "bulkgen.cli.run_build", + "hokusai.cli.run_build", new_callable=AsyncMock, return_value=build_result, ) as mock_run, @@ -142,10 +142,10 @@ class TestCleanCommand: def test_clean_removes_targets(self, cli_project: Path) -> None: _ = (cli_project / "output.txt").write_text("generated") _ = (cli_project / "image.png").write_bytes(b"\x89PNG") - state_file = ".project.bulkgen-state.yaml" + state_file = ".project.hokusai-state.yaml" _ = (cli_project / state_file).write_text("targets: {}") - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = cli_project result = runner.invoke(app, ["clean"]) @@ -156,7 +156,7 @@ class TestCleanCommand: assert "Cleaned 2 artifact(s)" in result.output def test_clean_no_artifacts(self, cli_project: Path) -> None: - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = cli_project result = runner.invoke(app, ["clean"]) @@ -166,7 +166,7 @@ class TestCleanCommand: def test_clean_partial_artifacts(self, cli_project: Path) -> None: _ = (cli_project / "output.txt").write_text("generated") - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = cli_project result = runner.invoke(app, ["clean"]) @@ -180,10 +180,10 @@ class TestGraphCommand: def test_graph_single_target(self, tmp_path: Path) -> None: config = {"targets": {"out.txt": {"prompt": "hello"}}} - _ = (tmp_path / "test.bulkgen.yaml").write_text( + _ = (tmp_path / "test.hokusai.yaml").write_text( yaml.dump(config, default_flow_style=False) ) - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = tmp_path result = runner.invoke(app, ["graph"]) @@ -198,10 +198,10 @@ class TestGraphCommand: "step2.txt": {"prompt": "y", "inputs": ["step1.md"]}, } } - _ = (tmp_path / "test.bulkgen.yaml").write_text( + _ = (tmp_path / "test.hokusai.yaml").write_text( yaml.dump(config, default_flow_style=False) ) - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = tmp_path result = runner.invoke(app, ["graph"]) @@ -219,10 +219,10 @@ class TestGraphCommand: "b.txt": {"prompt": "y", "inputs": ["a.txt"]}, } } - _ = (tmp_path / "test.bulkgen.yaml").write_text( + _ = (tmp_path / "test.hokusai.yaml").write_text( yaml.dump(config, default_flow_style=False) ) - with patch("bulkgen.cli.Path") as mock_path_cls: + with patch("hokusai.cli.Path") as mock_path_cls: mock_path_cls.cwd.return_value = tmp_path result = runner.invoke(app, ["graph"]) diff --git a/tests/test_config.py b/tests/test_config.py index a858b0a..612d1b8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.config.""" +"""Integration tests for hokusai.config.""" from __future__ import annotations @@ -7,14 +7,14 @@ from pathlib import Path import pytest import yaml -from bulkgen.config import load_config +from hokusai.config import load_config class TestLoadConfig: """Test loading and validating YAML config files end-to-end.""" def test_minimal_config(self, project_dir: Path) -> None: - config_path = project_dir / "test.bulkgen.yaml" + config_path = project_dir / "test.hokusai.yaml" _ = config_path.write_text( yaml.dump({"targets": {"out.txt": {"prompt": "hello"}}}) ) @@ -47,7 +47,7 @@ class TestLoadConfig: }, }, } - config_path = project_dir / "full.bulkgen.yaml" + config_path = project_dir / "full.hokusai.yaml" _ = config_path.write_text(yaml.dump(raw, default_flow_style=False)) config = load_config(config_path) @@ -67,14 +67,14 @@ class TestLoadConfig: assert story.inputs == ["banner.png"] def test_empty_targets_rejected(self, project_dir: Path) -> None: - config_path = project_dir / "empty.bulkgen.yaml" + config_path = project_dir / "empty.hokusai.yaml" _ = config_path.write_text(yaml.dump({"targets": {}})) with pytest.raises(Exception, match="At least one target"): _ = load_config(config_path) def test_missing_prompt_rejected(self, project_dir: Path) -> None: - config_path = project_dir / "bad.bulkgen.yaml" + config_path = project_dir / "bad.hokusai.yaml" _ = config_path.write_text(yaml.dump({"targets": {"out.txt": {}}})) with pytest.raises(Exception): diff --git a/tests/test_graph.py b/tests/test_graph.py index 914714b..029a203 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.graph.""" +"""Integration tests for hokusai.graph.""" from __future__ import annotations @@ -7,8 +7,8 @@ from pathlib import Path import pytest -from bulkgen.config import ProjectConfig -from bulkgen.graph import build_graph, get_build_order, get_subgraph_for_target +from hokusai.config import ProjectConfig +from hokusai.graph import build_graph, get_build_order, get_subgraph_for_target WriteConfig = Callable[[dict[str, object]], ProjectConfig] diff --git a/tests/test_providers.py b/tests/test_providers.py index bc5e7e4..d690c9a 100644 --- a/tests/test_providers.py +++ b/tests/test_providers.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.providers (image and text). +"""Integration tests for hokusai.providers (image and text). Mock-heavy tests produce many Any-typed expressions from MagicMock. """ @@ -12,16 +12,16 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from bulkgen.config import TargetConfig -from bulkgen.providers.bfl import BFLResult -from bulkgen.providers.blackforest import BlackForestProvider -from bulkgen.providers.blackforest import ( +from hokusai.config import TargetConfig +from hokusai.providers.bfl import BFLResult +from hokusai.providers.blackforest import BlackForestProvider +from hokusai.providers.blackforest import ( _encode_image_b64 as encode_image_b64, # pyright: ignore[reportPrivateUsage] ) -from bulkgen.providers.mistral import MistralProvider -from bulkgen.providers.models import ModelInfo -from bulkgen.providers.openai_image import OpenAIImageProvider -from bulkgen.providers.registry import get_all_models +from hokusai.providers.mistral import MistralProvider +from hokusai.providers.models import ModelInfo +from hokusai.providers.openai_image import OpenAIImageProvider +from hokusai.providers.registry import get_all_models def _model(name: str) -> ModelInfo: @@ -85,8 +85,8 @@ class TestBlackForestProvider: bfl_result, mock_http = _make_bfl_mocks(image_bytes) with ( - patch("bulkgen.providers.blackforest.BFLClient") as mock_cls, - patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls, + patch("hokusai.providers.blackforest.BFLClient") as mock_cls, + patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls, ): mock_cls.return_value.generate = AsyncMock(return_value=bfl_result) mock_http_cls.return_value = mock_http @@ -111,8 +111,8 @@ class TestBlackForestProvider: bfl_result, mock_http = _make_bfl_mocks(image_bytes) with ( - patch("bulkgen.providers.blackforest.BFLClient") as mock_cls, - patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls, + patch("hokusai.providers.blackforest.BFLClient") as mock_cls, + patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls, ): mock_generate = AsyncMock(return_value=bfl_result) mock_cls.return_value.generate = mock_generate @@ -142,8 +142,8 @@ class TestBlackForestProvider: bfl_result, mock_http = _make_bfl_mocks(image_bytes) with ( - patch("bulkgen.providers.blackforest.BFLClient") as mock_cls, - patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls, + patch("hokusai.providers.blackforest.BFLClient") as mock_cls, + patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls, ): mock_generate = AsyncMock(return_value=bfl_result) mock_cls.return_value.generate = mock_generate @@ -177,8 +177,8 @@ class TestBlackForestProvider: bfl_result, mock_http = _make_bfl_mocks(image_bytes) with ( - patch("bulkgen.providers.blackforest.BFLClient") as mock_cls, - patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls, + patch("hokusai.providers.blackforest.BFLClient") as mock_cls, + patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls, ): mock_generate = AsyncMock(return_value=bfl_result) mock_cls.return_value.generate = mock_generate @@ -208,8 +208,8 @@ class TestBlackForestProvider: bfl_result, mock_http = _make_bfl_mocks(image_bytes) with ( - patch("bulkgen.providers.blackforest.BFLClient") as mock_cls, - patch("bulkgen.providers.blackforest.httpx.AsyncClient") as mock_http_cls, + patch("hokusai.providers.blackforest.BFLClient") as mock_cls, + patch("hokusai.providers.blackforest.httpx.AsyncClient") as mock_http_cls, ): mock_generate = AsyncMock(return_value=bfl_result) mock_cls.return_value.generate = mock_generate @@ -231,8 +231,8 @@ class TestBlackForestProvider: async def test_image_no_sample_url_raises(self, project_dir: Path) -> None: target_config = TargetConfig(prompt="x") - with patch("bulkgen.providers.blackforest.BFLClient") as mock_cls: - from bulkgen.providers.bfl import BFLError + with patch("hokusai.providers.blackforest.BFLClient") as mock_cls: + from hokusai.providers.bfl import BFLError mock_cls.return_value.generate = AsyncMock( side_effect=BFLError("BFL task test ready but no sample URL: {}") @@ -264,7 +264,7 @@ class TestMistralProvider: target_config = TargetConfig(prompt="Write a poem") response = _make_text_response("Roses are red...") - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_cls.return_value = _make_mistral_mock(response) provider = MistralProvider(api_key="test-key") @@ -285,7 +285,7 @@ class TestMistralProvider: target_config = TargetConfig(prompt="Summarize", inputs=["source.txt"]) response = _make_text_response("Summary: ...") - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_client = _make_mistral_mock(response) mock_cls.return_value = mock_client @@ -309,7 +309,7 @@ class TestMistralProvider: target_config = TargetConfig(prompt="Describe this image", inputs=["photo.png"]) response = _make_text_response("A beautiful photo") - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_client = _make_mistral_mock(response) mock_cls.return_value = mock_client @@ -334,7 +334,7 @@ class TestMistralProvider: response = MagicMock() response.choices = [] - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_cls.return_value = _make_mistral_mock(response) provider = MistralProvider(api_key="test-key") @@ -351,7 +351,7 @@ class TestMistralProvider: target_config = TargetConfig(prompt="x") response = _make_text_response(None) - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_cls.return_value = _make_mistral_mock(response) provider = MistralProvider(api_key="test-key") @@ -374,7 +374,7 @@ class TestMistralProvider: ) response = _make_text_response("Combined") - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_client = _make_mistral_mock(response) mock_cls.return_value = mock_client @@ -405,7 +405,7 @@ class TestMistralProvider: ) response = _make_text_response("A stylized image") - with patch("bulkgen.providers.mistral.Mistral") as mock_cls: + with patch("hokusai.providers.mistral.Mistral") as mock_cls: mock_client = _make_mistral_mock(response) mock_cls.return_value = mock_client @@ -459,7 +459,7 @@ class TestOpenAIImageProvider: b64 = base64.b64encode(image_bytes).decode() mock_client = _make_openai_mock(b64) - with patch("bulkgen.providers.openai_image.AsyncOpenAI") as mock_cls: + with patch("hokusai.providers.openai_image.AsyncOpenAI") as mock_cls: mock_cls.return_value = mock_client provider = OpenAIImageProvider(api_key="test-key") @@ -493,7 +493,7 @@ class TestOpenAIImageProvider: b64 = base64.b64encode(image_bytes).decode() mock_client = _make_openai_mock(b64) - with patch("bulkgen.providers.openai_image.AsyncOpenAI") as mock_cls: + with patch("hokusai.providers.openai_image.AsyncOpenAI") as mock_cls: mock_cls.return_value = mock_client provider = OpenAIImageProvider(api_key="test-key") diff --git a/tests/test_resolve.py b/tests/test_resolve.py index 3c33e6e..ad27574 100644 --- a/tests/test_resolve.py +++ b/tests/test_resolve.py @@ -1,15 +1,15 @@ -"""Integration tests for bulkgen.config.""" +"""Integration tests for hokusai.config.""" from __future__ import annotations import pytest -from bulkgen.config import ( +from hokusai.config import ( Defaults, TargetConfig, ) -from bulkgen.providers.models import Capability -from bulkgen.resolve import infer_required_capabilities, resolve_model +from hokusai.providers.models import Capability +from hokusai.resolve import infer_required_capabilities, resolve_model class TestInferRequiredCapabilities: diff --git a/tests/test_state.py b/tests/test_state.py index ab872cf..947501b 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1,4 +1,4 @@ -"""Integration tests for bulkgen.state.""" +"""Integration tests for hokusai.state.""" from __future__ import annotations @@ -6,7 +6,7 @@ from pathlib import Path import yaml -from bulkgen.state import ( +from hokusai.state import ( BuildState, TargetState, hash_file, @@ -41,10 +41,10 @@ class TestStateFilename: """Test state filename derivation.""" def test_state_filename(self) -> None: - assert state_filename("cards") == ".cards.bulkgen-state.yaml" + assert state_filename("cards") == ".cards.hokusai-state.yaml" def test_state_filename_simple(self) -> None: - assert state_filename("project") == ".project.bulkgen-state.yaml" + assert state_filename("project") == ".project.hokusai-state.yaml" class TestStatePersistence: diff --git a/uv.lock b/uv.lock index 9b76caa..4b1cd63 100644 --- a/uv.lock +++ b/uv.lock @@ -45,7 +45,7 @@ wheels = [ ] [[package]] -name = "bulkgen" +name = "hokusai" version = "0.1.0" source = { editable = "." } dependencies = [