chore: rename bulkgen to hokusai
All checks were successful
Continuous Integration / Build Package (push) Successful in 35s
Continuous Integration / Lint, Check & Test (push) Successful in 57s

This commit is contained in:
Konstantin Fickel 2026-02-20 17:08:12 +01:00
parent a28cc97aed
commit 4def49350e
Signed by: kfickel
GPG key ID: A793722F9933C1A5
32 changed files with 215 additions and 213 deletions

4
.gitignore vendored
View file

@ -14,8 +14,8 @@ wheels/
.devenv.flake.nix
.pre-commit-config.yaml
# bulkgen state
.*.bulkgen-state.yaml
# hokusai state
.*.hokusai-state.yaml
# Nix
result

View file

@ -1 +1 @@
3.14
3.13

View file

@ -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
```

View file

@ -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 `<anything>.bulkgen.yaml` and placed in your project directory. One config file per directory.
The config file must be named `<anything>.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.<system>.bulkgen` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/bulkgen`)
- `packages.<system>.hokusai` — the standalone package, usable without home-manager (e.g. `nix run github:kfickel/hokusai`)
- `devShells.<system>.default` — development shell with all dependencies
## Environment variables

View file

@ -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 {

View file

@ -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,

View file

@ -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)

View file

@ -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 ``<name>.bulkgen.yaml``."""
"""Top-level configuration parsed from ``<name>.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)

View file

@ -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]:

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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:

View file

@ -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()

View file

@ -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)

View file

@ -1,4 +1,4 @@
"""Incremental build state tracking via ``.<project>.bulkgen-state.yaml``."""
"""Incremental build state tracking via ``.<project>.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):

View file

@ -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:

View file

@ -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 {

View file

@ -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"]

View file

@ -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)

View file

@ -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"}

View file

@ -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"])

View file

@ -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):

View file

@ -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]

View file

@ -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")

View file

@ -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:

View file

@ -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:

2
uv.lock generated
View file

@ -45,7 +45,7 @@ wheels = [
]
[[package]]
name = "bulkgen"
name = "hokusai"
version = "0.1.0"
source = { editable = "." }
dependencies = [