refactor: switch to basedpyright, remove pydantic-settings

- Replace pyright with basedpyright in devenv.nix (custom hook)
- Add basedpyright to devenv packages
- Fix all basedpyright warnings: add DiGraph[str] type args, annotate
  class attributes, narrow SyncResponse, handle unused call results,
  suppress unavoidable Any from yaml.safe_load and untyped blackforest
- Replace pydantic-settings[yaml] with direct pyyaml dependency
- Update CLAUDE.md to reflect basedpyright and dependency changes
This commit is contained in:
Konstantin Fickel 2026-02-13 20:25:28 +01:00
parent f71af1cfaf
commit 7ab25d49cb
Signed by: kfickel
GPG key ID: A793722F9933C1A5
11 changed files with 58 additions and 59 deletions

View file

@ -96,6 +96,6 @@ def graph() -> None:
typer.echo(f"Stage {i} (targets): {', '.join(targets_in_gen)}")
for node in gen:
predecessors = list(dep_graph.predecessors(node))
if predecessors:
typer.echo(f" {node} <- {', '.join(predecessors)}")
preds: list[str] = list(dep_graph.predecessors(node))
if preds:
typer.echo(f" {node} <- {', '.join(preds)}")

View file

@ -78,5 +78,5 @@ def resolve_model(target_name: str, target: TargetConfig, defaults: Defaults) ->
def load_config(config_path: Path) -> ProjectConfig:
"""Load and validate a ``.bulkgen.yaml`` file."""
with config_path.open() as f:
raw = yaml.safe_load(f)
raw = yaml.safe_load(f) # pyright: ignore[reportAny]
return ProjectConfig.model_validate(raw)

View file

@ -9,7 +9,7 @@ import networkx as nx
from bulkgen.config import ProjectConfig
def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph[str]:
"""Build a dependency DAG from the project configuration.
Nodes are filenames: target names (keys in ``config.targets``) and
@ -19,7 +19,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
Raises :class:`ValueError` if a dependency is neither a defined target
nor an existing file, or if the graph contains a cycle.
"""
graph = nx.DiGraph()
graph: nx.DiGraph[str] = nx.DiGraph()
target_names = set(config.targets)
for target_name, target_cfg in config.targets.items():
@ -37,7 +37,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
f"which is neither a defined target nor an existing file"
)
raise ValueError(msg)
graph.add_edge(dep, target_name)
_ = graph.add_edge(dep, target_name)
if not nx.is_directed_acyclic_graph(graph):
cycles = list(nx.simple_cycles(graph))
@ -47,7 +47,7 @@ def build_graph(config: ProjectConfig, project_dir: Path) -> nx.DiGraph:
return graph
def get_build_order(graph: nx.DiGraph) -> list[list[str]]:
def get_build_order(graph: nx.DiGraph[str]) -> list[list[str]]:
"""Return targets grouped into generations for parallel execution.
Each inner list contains nodes with no inter-dependencies that can
@ -56,9 +56,9 @@ def get_build_order(graph: nx.DiGraph) -> list[list[str]]:
return [list(gen) for gen in nx.topological_generations(graph)]
def get_subgraph_for_target(graph: nx.DiGraph, target: str) -> nx.DiGraph:
def get_subgraph_for_target(graph: nx.DiGraph[str], target: str) -> nx.DiGraph[str]:
"""Return the subgraph containing *target* and all its transitive dependencies."""
ancestors = nx.ancestors(graph, target)
ancestors: set[str] = nx.ancestors(graph, target) # pyright: ignore[reportUnknownMemberType]
ancestors.add(target)
subgraph = nx.DiGraph(graph.subgraph(ancestors))
subgraph: nx.DiGraph[str] = nx.DiGraph(graph.subgraph(ancestors))
return subgraph

View file

@ -8,8 +8,13 @@ from pathlib import Path
from typing import override
import httpx
from blackforest import BFLClient
from blackforest.types.general.client_config import ClientConfig
from blackforest import BFLClient # pyright: ignore[reportMissingTypeStubs]
from blackforest.types.general.client_config import ( # pyright: ignore[reportMissingTypeStubs]
ClientConfig,
)
from blackforest.types.responses.responses import ( # pyright: ignore[reportMissingTypeStubs]
SyncResponse,
)
from bulkgen.config import TargetConfig
from bulkgen.providers import Provider
@ -25,6 +30,8 @@ def _encode_image_b64(path: Path) -> str:
class ImageProvider(Provider):
"""Generates images via the BlackForestLabs API."""
_client: BFLClient
def __init__(self, api_key: str) -> None:
self._client = BFLClient(api_key=api_key)
@ -58,13 +65,20 @@ class ImageProvider(Provider):
self._client.generate, resolved_model, inputs, _BFL_SYNC_CONFIG
)
image_url: str | None = result.result.get("sample") # type: ignore[union-attr]
if not isinstance(result, SyncResponse):
msg = (
f"BFL API returned unexpected response type for target '{target_name}'"
)
raise RuntimeError(msg)
result_dict: dict[str, str] = result.result # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
image_url = result_dict.get("sample")
if not image_url:
msg = f"BFL API did not return an image URL for target '{target_name}'"
raise RuntimeError(msg)
async with httpx.AsyncClient() as http:
response = await http.get(image_url)
response.raise_for_status()
_ = response.raise_for_status()
output_path.write_bytes(response.content)
_ = output_path.write_bytes(response.content)

View file

@ -14,6 +14,8 @@ from bulkgen.providers import Provider
class TextProvider(Provider):
"""Generates text via the Mistral API."""
_api_key: str
def __init__(self, api_key: str) -> None:
self._api_key = api_key
@ -50,8 +52,8 @@ class TextProvider(Provider):
messages=[models.UserMessage(content=full_prompt)],
)
if response is None or not response.choices:
msg = f"Mistral API returned no response for target '{target_name}'"
if not response.choices:
msg = f"Mistral API returned no choices for target '{target_name}'"
raise RuntimeError(msg)
content = response.choices[0].message.content
@ -60,4 +62,4 @@ class TextProvider(Provider):
raise RuntimeError(msg)
text = content if isinstance(content, str) else str(content)
output_path.write_text(text)
_ = output_path.write_text(text)

View file

@ -46,7 +46,7 @@ def load_state(project_dir: Path) -> BuildState:
if not state_path.exists():
return BuildState()
with state_path.open() as f:
raw = yaml.safe_load(f)
raw = yaml.safe_load(f) # pyright: ignore[reportAny]
if raw is None:
return BuildState()
return BuildState.model_validate(raw)