katabatic 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. katabatic/__init__.py +15 -0
  2. katabatic/artifacts/__init__.py +27 -0
  3. katabatic/artifacts/base.py +21 -0
  4. katabatic/artifacts/dataset_split.py +138 -0
  5. katabatic/artifacts/ids.py +19 -0
  6. katabatic/artifacts/local.py +33 -0
  7. katabatic/artifacts/refs.py +88 -0
  8. katabatic/cli/__init__.py +3 -0
  9. katabatic/cli/commands/__init__.py +3 -0
  10. katabatic/cli/commands/model_init.py +201 -0
  11. katabatic/cli/commands/pin_notebook_kernel.py +101 -0
  12. katabatic/cli/commands/register_dataset.py +34 -0
  13. katabatic/cli/main.py +101 -0
  14. katabatic/datasets/__init__.py +9 -0
  15. katabatic/datasets/adult.csv +32562 -0
  16. katabatic/datasets/car.csv +1729 -0
  17. katabatic/datasets/car1.csv +10 -0
  18. katabatic/datasets/compatibility.py +45 -0
  19. katabatic/datasets/magic.csv +19021 -0
  20. katabatic/datasets/nursery.csv +12961 -0
  21. katabatic/datasets/profile.py +85 -0
  22. katabatic/datasets/registry.py +109 -0
  23. katabatic/datasets/shuttle.csv +58001 -0
  24. katabatic/evaluate/base_evaluation.py +24 -0
  25. katabatic/evaluate/consistency/__init__.py +3 -0
  26. katabatic/evaluate/consistency/evaluation.py +255 -0
  27. katabatic/evaluate/diversity/__init__.py +3 -0
  28. katabatic/evaluate/diversity/evaluation.py +256 -0
  29. katabatic/evaluate/fidelity/__init__.py +3 -0
  30. katabatic/evaluate/fidelity/evaluation.py +212 -0
  31. katabatic/evaluate/privacy/__init__.py +3 -0
  32. katabatic/evaluate/privacy/evaluation.py +259 -0
  33. katabatic/evaluate/report/__init__.py +3 -0
  34. katabatic/evaluate/report/composite.py +156 -0
  35. katabatic/evaluate/stability/__init__.py +3 -0
  36. katabatic/evaluate/stability/evaluation.py +191 -0
  37. katabatic/evaluate/tstr/evaluation.py +204 -0
  38. katabatic/evaluate/utility/__init__.py +3 -0
  39. katabatic/evaluate/utility/evaluation.py +268 -0
  40. katabatic/models/__init__.py +9 -0
  41. katabatic/models/base_model.py +67 -0
  42. katabatic/models/codi/README.md +67 -0
  43. katabatic/models/codi/__init__.py +12 -0
  44. katabatic/models/codi/image.png +0 -0
  45. katabatic/models/codi/models.py +685 -0
  46. katabatic/models/codi/utils.py +549 -0
  47. katabatic/models/ctgan/__init__.py +5 -0
  48. katabatic/models/ctgan/models.py +596 -0
  49. katabatic/models/ctgan/utils.py +194 -0
  50. katabatic/models/ganblr/README.md +79 -0
  51. katabatic/models/ganblr/__init__.py +14 -0
  52. katabatic/models/ganblr/models.py +833 -0
  53. katabatic/models/ganblr/utils.py +137 -0
  54. katabatic/models/great/great_dataset.py +97 -0
  55. katabatic/models/great/great_start.py +167 -0
  56. katabatic/models/great/great_trainer.py +46 -0
  57. katabatic/models/great/great_utils.py +185 -0
  58. katabatic/models/great/models.py +919 -0
  59. katabatic/models/medgan/README.md +73 -0
  60. katabatic/models/medgan/__init__.py +11 -0
  61. katabatic/models/medgan/models.py +384 -0
  62. katabatic/models/medgan/utils.py +201 -0
  63. katabatic/models/pategan/README.md +327 -0
  64. katabatic/models/pategan/__init__.py +4 -0
  65. katabatic/models/pategan/models.py +717 -0
  66. katabatic/models/pategan/utils.py +320 -0
  67. katabatic/models/registry.py +150 -0
  68. katabatic/models/tabddpm/__init__.py +5 -0
  69. katabatic/models/tabddpm/models.py +680 -0
  70. katabatic/models/tabddpm/utils.py +225 -0
  71. katabatic/models/tabsyn/__init__.py +5 -0
  72. katabatic/models/tabsyn/models.py +185 -0
  73. katabatic/models/tabsyn/utils.py +859 -0
  74. katabatic/pipeline/base_pipeline.py +19 -0
  75. katabatic/pipeline/cross_validation/pipeline.py +24 -0
  76. katabatic/pipeline/evaluation_pipeline.py +278 -0
  77. katabatic/pipeline/train_test_split/pipeline.py +370 -0
  78. katabatic/utils/__init__.py +1 -0
  79. katabatic/utils/column_types.py +43 -0
  80. katabatic/utils/preprocess.py +94 -0
  81. katabatic/utils/split_dataset.py +133 -0
  82. katabatic/utils/train_test_consistency.py +50 -0
  83. katabatic-0.2.0.dist-info/METADATA +730 -0
  84. katabatic-0.2.0.dist-info/RECORD +86 -0
  85. katabatic-0.2.0.dist-info/WHEEL +4 -0
  86. katabatic-0.2.0.dist-info/licenses/LICENSE +21 -0
katabatic/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ Katabatic package initializer.
3
+ Synthetic tabular data generation, pipelines, and evaluation.
4
+ """
5
+
6
+ from importlib.metadata import PackageNotFoundError, version
7
+
8
+ try:
9
+ __version__ = version("katabatic")
10
+ except PackageNotFoundError:
11
+ __version__ = "0.0.0.dev0"
12
+
13
+ from . import models, pipeline, utils
14
+
15
+ __all__ = ["__version__", "models", "pipeline", "utils"]
@@ -0,0 +1,27 @@
1
+ from katabatic.artifacts.base import ArtifactStore
2
+ from katabatic.artifacts.dataset_split import (
3
+ write_dataset_artifact,
4
+ write_dataset_artifact_presplit,
5
+ )
6
+ from katabatic.artifacts.ids import new_eval_id, new_split_id, new_train_id
7
+ from katabatic.artifacts.local import LocalArtifactStore
8
+ from katabatic.artifacts.refs import (
9
+ DatasetRef,
10
+ EvaluationRef,
11
+ ModelRef,
12
+ artifact_path_segment,
13
+ )
14
+
15
+ __all__ = [
16
+ "ArtifactStore",
17
+ "LocalArtifactStore",
18
+ "DatasetRef",
19
+ "ModelRef",
20
+ "EvaluationRef",
21
+ "artifact_path_segment",
22
+ "write_dataset_artifact",
23
+ "write_dataset_artifact_presplit",
24
+ "new_split_id",
25
+ "new_train_id",
26
+ "new_eval_id",
27
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+
7
+ class ArtifactStore(ABC):
8
+ @abstractmethod
9
+ def save_json(self, path: str, data: dict) -> None: ...
10
+
11
+ @abstractmethod
12
+ def load_json(self, path: str) -> dict: ...
13
+
14
+ @abstractmethod
15
+ def save_bytes(self, path: str, data: bytes) -> None: ...
16
+
17
+ @abstractmethod
18
+ def open_path(self, path: str) -> Path: ...
19
+
20
+ @abstractmethod
21
+ def exists(self, path: str) -> bool: ...
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+
8
+ from katabatic.artifacts.base import ArtifactStore
9
+ from katabatic.artifacts.ids import new_split_id
10
+ from katabatic.artifacts.refs import DatasetRef
11
+ from katabatic.utils.split_dataset import compute_train_test_split
12
+ from katabatic.utils.train_test_consistency import sanity_check_train_test
13
+
14
+
15
+ def _copy_extra_assets(
16
+ source_dir: Path | None,
17
+ extra_dir: Path,
18
+ ) -> None:
19
+ """Copy info.json and TabSyn-style *.npy from source_dir into extra_dir if present."""
20
+ if source_dir is None or not source_dir.is_dir():
21
+ return
22
+ extra_dir.mkdir(parents=True, exist_ok=True)
23
+ info = source_dir / "info.json"
24
+ if info.exists():
25
+ shutil.copy2(info, extra_dir / "info.json")
26
+ for name in (
27
+ "X_num_train.npy",
28
+ "X_cat_train.npy",
29
+ "y_train.npy",
30
+ "X_num_test.npy",
31
+ "X_cat_test.npy",
32
+ "y_test.npy",
33
+ ):
34
+ p = source_dir / name
35
+ if p.exists():
36
+ shutil.copy2(p, extra_dir / name)
37
+
38
+
39
+ def _persist_frames_to_store(
40
+ store: ArtifactStore,
41
+ ref: DatasetRef,
42
+ df_train: pd.DataFrame,
43
+ df_test: pd.DataFrame,
44
+ extra_source_dir: Path | None,
45
+ ) -> tuple[pd.Series, pd.Series]:
46
+ label_name = df_train.columns[-1]
47
+ X_train, y_train = df_train.iloc[:, :-1], df_train.iloc[:, -1]
48
+ X_test, y_test = df_test.iloc[:, :-1], df_test.iloc[:, -1]
49
+ y_train.name = label_name
50
+ y_test.name = label_name
51
+
52
+ train_dir = store.open_path(ref.train_relpath)
53
+ test_dir = store.open_path(ref.test_relpath)
54
+ extra_dir = store.open_path(ref.extra_relpath)
55
+ train_dir.mkdir(parents=True, exist_ok=True)
56
+ test_dir.mkdir(parents=True, exist_ok=True)
57
+ extra_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ df_train.to_csv(train_dir / "train_full.csv", index=False)
60
+ df_test.to_csv(test_dir / "test_full.csv", index=False)
61
+ X_train.to_csv(train_dir / "x_train.csv", index=False)
62
+ y_train.to_csv(train_dir / "y_train.csv", index=False, header=True)
63
+ X_test.to_csv(test_dir / "x_test.csv", index=False)
64
+ y_test.to_csv(test_dir / "y_test.csv", index=False, header=True)
65
+
66
+ shutil.copy2(train_dir / "x_train.csv", extra_dir / "x_train.csv")
67
+ shutil.copy2(train_dir / "y_train.csv", extra_dir / "y_train.csv")
68
+ shutil.copy2(test_dir / "x_test.csv", extra_dir / "x_test.csv")
69
+ shutil.copy2(test_dir / "y_test.csv", extra_dir / "y_test.csv")
70
+
71
+ _copy_extra_assets(extra_source_dir, extra_dir)
72
+ return y_train, y_test
73
+
74
+
75
+ def write_dataset_artifact(
76
+ store: ArtifactStore,
77
+ *,
78
+ input_csv: str | Path,
79
+ dataset_name: str,
80
+ test_size: float = 0.2,
81
+ seed: int = 42,
82
+ dataset_version: str | None = None,
83
+ extra_source_dir: str | Path | None = None,
84
+ ) -> DatasetRef:
85
+ """
86
+ Split input_csv into train/ and test/ under datasets/<name>/<version>/,
87
+ optional extra/ from extra_source_dir (e.g. TabSyn npy + info.json).
88
+ """
89
+ input_csv = Path(input_csv)
90
+ version = dataset_version or new_split_id()
91
+ ref = DatasetRef(dataset_name=dataset_name, dataset_version=version)
92
+
93
+ df = pd.read_csv(input_csv)
94
+ print(f"Loaded data with shape: {df.shape}")
95
+
96
+ df_train, df_test, _, _, _, _ = compute_train_test_split(
97
+ df, test_size=test_size, seed=seed
98
+ )
99
+
100
+ src = Path(extra_source_dir) if extra_source_dir else input_csv.parent
101
+ y_train, y_test = _persist_frames_to_store(store, ref, df_train, df_test, src)
102
+
103
+ print("Train label distribution:\n", y_train.value_counts(normalize=True))
104
+ print("Test label distribution:\n", y_test.value_counts(normalize=True))
105
+ print(f"Saved dataset artifact under {ref.root_relpath}")
106
+ return ref
107
+
108
+
109
+ def write_dataset_artifact_presplit(
110
+ store: ArtifactStore,
111
+ *,
112
+ train_csv: str | Path,
113
+ test_csv: str | Path,
114
+ dataset_name: str,
115
+ dataset_version: str | None = None,
116
+ extra_source_dir: str | Path | None = None,
117
+ ) -> DatasetRef:
118
+ """
119
+ Write user-provided train/test CSVs under datasets/<name>/<version>/
120
+ without performing a random split.
121
+ """
122
+ train_csv = Path(train_csv)
123
+ test_csv = Path(test_csv)
124
+ version = dataset_version or new_split_id()
125
+ ref = DatasetRef(dataset_name=dataset_name, dataset_version=version)
126
+
127
+ df_train = pd.read_csv(train_csv)
128
+ df_test = pd.read_csv(test_csv)
129
+ print(f"Loaded presplit train {df_train.shape}, test {df_test.shape}")
130
+ sanity_check_train_test(df_train, df_test)
131
+
132
+ src = Path(extra_source_dir) if extra_source_dir else train_csv.parent
133
+ y_train, y_test = _persist_frames_to_store(store, ref, df_train, df_test, src)
134
+
135
+ print("Train label distribution:\n", y_train.value_counts(normalize=True))
136
+ print("Test label distribution:\n", y_test.value_counts(normalize=True))
137
+ print(f"Saved dataset artifact under {ref.root_relpath}")
138
+ return ref
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+
5
+
6
+ def _utc_stamp() -> str:
7
+ return datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
8
+
9
+
10
+ def new_split_id() -> str:
11
+ return f"split-{_utc_stamp()}"
12
+
13
+
14
+ def new_train_id() -> str:
15
+ return f"train-{_utc_stamp()}"
16
+
17
+
18
+ def new_eval_id() -> str:
19
+ return f"eval-{_utc_stamp()}"
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from katabatic.artifacts.base import ArtifactStore
7
+
8
+
9
+ class LocalArtifactStore(ArtifactStore):
10
+ def __init__(self, root: str | Path = "artifacts") -> None:
11
+ self.root = Path(root)
12
+
13
+ def _full(self, path: str) -> Path:
14
+ return self.root / path
15
+
16
+ def save_json(self, path: str, data: dict) -> None:
17
+ full = self._full(path)
18
+ full.parent.mkdir(parents=True, exist_ok=True)
19
+ full.write_text(json.dumps(data, indent=2), encoding="utf-8")
20
+
21
+ def load_json(self, path: str) -> dict:
22
+ return json.loads(self._full(path).read_text(encoding="utf-8"))
23
+
24
+ def save_bytes(self, path: str, data: bytes) -> None:
25
+ full = self._full(path)
26
+ full.parent.mkdir(parents=True, exist_ok=True)
27
+ full.write_bytes(data)
28
+
29
+ def open_path(self, path: str) -> Path:
30
+ return self._full(path)
31
+
32
+ def exists(self, path: str) -> bool:
33
+ return self._full(path).exists()
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+
7
+ def artifact_path_segment(name: str) -> str:
8
+ """
9
+ Sanitize a logical name for use inside artifact directory / file segments.
10
+ Allows lowercase letters, digits, hyphen; coerces other chars to hyphen.
11
+ """
12
+ s = name.strip().lower()
13
+ s = re.sub(r"[^a-z0-9-]+", "-", s)
14
+ s = re.sub(r"-{2,}", "-", s).strip("-")
15
+ return s or "unnamed"
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DatasetRef:
20
+ dataset_name: str
21
+ dataset_version: str
22
+ """Root relative to artifact store: datasets/<name>/<version>"""
23
+
24
+ @property
25
+ def root_relpath(self) -> str:
26
+ return f"datasets/{self.dataset_name}/{self.dataset_version}"
27
+
28
+ @property
29
+ def train_relpath(self) -> str:
30
+ return f"{self.root_relpath}/train"
31
+
32
+ @property
33
+ def test_relpath(self) -> str:
34
+ return f"{self.root_relpath}/test"
35
+
36
+ @property
37
+ def extra_relpath(self) -> str:
38
+ return f"{self.root_relpath}/extra"
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ModelRef:
43
+ model_name: str
44
+ dataset_name: str
45
+ dataset_version: str
46
+ train_run_id: str
47
+
48
+ @property
49
+ def root_relpath(self) -> str:
50
+ m = artifact_path_segment(self.model_name)
51
+ d = artifact_path_segment(self.dataset_name)
52
+ return f"models/{m}_{d}_{self.train_run_id}"
53
+
54
+ @property
55
+ def state_relpath(self) -> str:
56
+ return f"{self.root_relpath}/state"
57
+
58
+ @property
59
+ def synthetic_relpath(self) -> str:
60
+ return f"{self.root_relpath}/synthetic"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class EvaluationRef:
65
+ evaluation_type: str
66
+ eval_run_id: str
67
+ model_name: str
68
+ dataset_name: str
69
+ dataset_version: str
70
+ train_run_id: str
71
+ test_dataset_version: str
72
+
73
+ @property
74
+ def root_relpath(self) -> str:
75
+ """Mirrors :attr:`ModelRef.root_relpath` stem under ``evaluations/``."""
76
+ m = artifact_path_segment(self.model_name)
77
+ d = artifact_path_segment(self.dataset_name)
78
+ return f"evaluations/{m}_{d}_{self.train_run_id}"
79
+
80
+ @property
81
+ def metrics_relpath(self) -> str:
82
+ et = artifact_path_segment(self.evaluation_type)
83
+ return f"{self.root_relpath}/{et}_metrics.json"
84
+
85
+ @property
86
+ def report_relpath(self) -> str:
87
+ et = artifact_path_segment(self.evaluation_type)
88
+ return f"{self.root_relpath}/{et}_report.csv"
@@ -0,0 +1,3 @@
1
+ from .commands import init_model
2
+
3
+ __all__ = ["init_model"]
@@ -0,0 +1,3 @@
1
+ from .model_init import init_model
2
+
3
+ __all__ = ["init_model"]
@@ -0,0 +1,201 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+
5
+
6
+ def _find_models_dict_bounds(text: str) -> tuple[int, int, str]:
7
+ """
8
+ Return (open_brace_idx, close_brace_idx, indent) for the _models dict.
9
+ Uses brace counting and skips over strings.
10
+ """
11
+ m = re.search(r"_models\s*:\s*Dict\[[^\]]*\]\s*=\s*{", text)
12
+ if not m:
13
+ m = re.search(r"_models\s*=\s*{", text)
14
+ if not m:
15
+ raise ValueError("Could not find `_models = { ... }` in registry.py")
16
+
17
+ open_idx = text.find("{", m.start())
18
+ if open_idx == -1:
19
+ raise ValueError("Could not find opening `{` for _models dictionary")
20
+
21
+ line_start = text.rfind("\n", 0, open_idx) + 1
22
+ indent = re.match(r"[ \t]*", text[line_start:open_idx]).group(0)
23
+
24
+ depth = 0
25
+ i = open_idx
26
+ in_str: str | None = None
27
+ escaped = False
28
+ while i < len(text):
29
+ ch = text[i]
30
+ if in_str:
31
+ if escaped:
32
+ escaped = False
33
+ elif ch == "\\":
34
+ escaped = True
35
+ elif ch == in_str:
36
+ in_str = None
37
+ else:
38
+ if ch in ("'", '"'):
39
+ in_str = ch
40
+ elif ch == "{":
41
+ depth += 1
42
+ elif ch == "}":
43
+ depth -= 1
44
+ if depth == 0:
45
+ return open_idx, i, indent
46
+ i += 1
47
+
48
+ raise ValueError("Unbalanced braces in _models dictionary")
49
+
50
+
51
+ def _serialize_list_str(xs: list[str]) -> str:
52
+ return "[" + ", ".join(repr(s) for s in xs) + "]"
53
+
54
+
55
+ def _find_project_root() -> Path:
56
+ """Return repo root when run from a checkout, else cwd."""
57
+ for candidate in [Path.cwd(), *Path.cwd().parents]:
58
+ if (candidate / "pyproject.toml").is_file() and (
59
+ candidate / "katabatic"
60
+ ).is_dir():
61
+ return candidate
62
+ return Path.cwd()
63
+
64
+
65
+ def init_model(model_name: str, dependencies: list[str] | None = None) -> None:
66
+ """Initialize a new model structure aligned with the base `Model` API.
67
+
68
+ Args:
69
+ model_name: Name of the model to create (e.g. "Rlig" or "rlig")
70
+ dependencies: Optional list of Python package dependencies for the model.
71
+ These will be embedded in the subclass' get_required_dependencies().
72
+ """
73
+ dir_name = re.sub(r"(?<!^)(?=[A-Z])", "_", model_name).lower()
74
+ if not re.fullmatch(r"[a-z][a-z0-9_]*", dir_name):
75
+ raise ValueError(
76
+ "Invalid model name. Use letters, numbers and underscores; start with a letter."
77
+ )
78
+ class_name = "".join(word.capitalize() for word in dir_name.split("_"))
79
+
80
+ from katabatic.models.registry import ModelRegistry
81
+
82
+ models_dir = Path(__file__).parents[2] / "models"
83
+ new_model_dir = models_dir / dir_name
84
+
85
+ if new_model_dir.exists():
86
+ raise ValueError(f"Model directory {dir_name} already exists")
87
+
88
+ if dir_name in ModelRegistry.get_available_models():
89
+ raise ValueError(f"Model {dir_name} is already registered")
90
+
91
+ new_model_dir.mkdir(parents=True)
92
+
93
+ # __init__.py
94
+ (new_model_dir / "__init__.py").write_text(
95
+ f'''"""Module for {class_name} model."""
96
+ from .models import {class_name}
97
+
98
+ __all__ = ["{class_name}"]
99
+ '''
100
+ )
101
+
102
+ deps = dependencies or []
103
+
104
+ (new_model_dir / "models.py").write_text(
105
+ f'''"""Implementation of {class_name} model."""
106
+ from typing import Any, Union
107
+ import numpy as np
108
+ import pandas as pd
109
+
110
+ from katabatic.models.base_model import Model
111
+ from . import utils as _utils
112
+
113
+
114
+ class {class_name}(Model):
115
+ """Implementation of {class_name} model."""
116
+
117
+ def __init__(self):
118
+ super().__init__()
119
+ # Validate runtime dependencies early to fail fast
120
+ self.check_dependencies()
121
+
122
+ @classmethod
123
+ def get_required_dependencies(cls) -> list[str]:
124
+ # Generated by init_model; adjust as your model evolves.
125
+ return {deps!r}
126
+
127
+ def train(self, *args, **kwargs) -> "Model":
128
+ \"\"\"Train the model on the given data.\"\"\"
129
+ raise NotImplementedError
130
+
131
+ def evaluate(self, *args, **kwargs) -> float:
132
+ \"\"\"Evaluate the model performance and return a score.\"\"\"
133
+ raise NotImplementedError
134
+
135
+ def sample(self, *args, **kwargs) -> Union[np.ndarray, pd.DataFrame]:
136
+ \"\"\"Generate synthetic samples.\"\"\"
137
+ raise NotImplementedError
138
+ '''
139
+ )
140
+
141
+ (new_model_dir / "utils.py").write_text(
142
+ f'''"""Utility functions for {class_name} model."""
143
+
144
+ # Add your utility functions here.
145
+ '''
146
+ )
147
+
148
+ registry_path = models_dir / "registry.py"
149
+ registry_content = registry_path.read_text()
150
+
151
+ open_idx, close_idx, indent = _find_models_dict_bounds(registry_content)
152
+
153
+ entry_indent = indent + " "
154
+ new_entry = (
155
+ f"{entry_indent}'{dir_name}': {{\n"
156
+ f"{entry_indent} 'module': 'katabatic.models.{dir_name}.models',\n"
157
+ f"{entry_indent} 'class': '{class_name}',\n"
158
+ f"{entry_indent} 'dependencies': {_serialize_list_str(deps)},\n"
159
+ f"{entry_indent} 'extra': '{dir_name}'\n"
160
+ f"{entry_indent}}}"
161
+ )
162
+
163
+ # Preserve the indented closing brace line (close_idx points at `}` only).
164
+ line_start = registry_content.rfind("\n", 0, close_idx) + 1
165
+ before = registry_content[:line_start].rstrip()
166
+ after = registry_content[line_start:]
167
+
168
+ needs_comma = not before.endswith("{")
169
+ if needs_comma and not before.endswith(","):
170
+ before = before + ","
171
+
172
+ if not before.endswith("\n"):
173
+ before = before + "\n"
174
+
175
+ new_registry = before + new_entry + ",\n" + after
176
+
177
+ registry_path.write_text(new_registry)
178
+
179
+ scripts_dir = _find_project_root() / "scripts"
180
+ scripts_dir.mkdir(parents=True, exist_ok=True)
181
+ setup_script_path = scripts_dir / f"setup_{dir_name}.sh"
182
+ setup_script_path.write_text(
183
+ f"""#!/bin/bash
184
+ # Setup script for {class_name} model dependencies
185
+
186
+ pip install katabatic[{dir_name}]
187
+ """
188
+ )
189
+ os.chmod(setup_script_path, 0o755) # nosec B103: executable bit on generated setup script, intended
190
+
191
+ print(f"""Successfully:
192
+ 1. Created {class_name} model structure in {new_model_dir}
193
+ 2. Added model to registry
194
+ 3. Created setup script at {setup_script_path}
195
+
196
+ Next steps:
197
+ 1. Add your model's dependencies to pyproject.toml in:
198
+ - [tool.poetry.dependencies] (as optional = true)
199
+ - [tool.poetry.extras] {dir_name} = [...]
200
+ 2. Implement train/evaluate/sample in {new_model_dir}/models.py
201
+ 3. Add any utility functions in {new_model_dir}/utils.py""")
@@ -0,0 +1,101 @@
1
+ """Pin Jupyter kernelspec for notebooks to the project virtualenv."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ DEFAULT_KERNEL = "katabatic-venv"
11
+ DEFAULT_DISPLAY = "Python (Katabatic .venv)"
12
+
13
+
14
+ def find_project_root(start: Path | None = None) -> Path:
15
+ """Walk up from *start* (default cwd) to find the Katabatic project root."""
16
+ for candidate in [start or Path.cwd(), *(start or Path.cwd()).parents]:
17
+ if (candidate / "pyproject.toml").is_file() and (
18
+ candidate / "katabatic"
19
+ ).is_dir():
20
+ return candidate
21
+ return start or Path.cwd()
22
+
23
+
24
+ def install_kernelspec(
25
+ *,
26
+ venv_prefix: Path,
27
+ python_exe: Path,
28
+ name: str,
29
+ display: str,
30
+ ) -> Path:
31
+ subprocess.run(
32
+ [
33
+ str(python_exe),
34
+ "-m",
35
+ "ipykernel",
36
+ "install",
37
+ "--prefix",
38
+ str(venv_prefix),
39
+ "--name",
40
+ name,
41
+ "--display-name",
42
+ display,
43
+ ],
44
+ check=True,
45
+ )
46
+ return venv_prefix / "share" / "jupyter" / "kernels" / name
47
+
48
+
49
+ def patch_notebook(path: Path, *, name: str, display: str) -> None:
50
+ nb = json.loads(path.read_text(encoding="utf-8"))
51
+ meta = nb.setdefault("metadata", {})
52
+ meta["kernelspec"] = {
53
+ "display_name": display,
54
+ "language": "python",
55
+ "name": name,
56
+ }
57
+ path.write_text(json.dumps(nb, indent=1), encoding="utf-8")
58
+
59
+
60
+ def run_pin_notebook_kernel(
61
+ notebooks: list[str | Path],
62
+ *,
63
+ repo_root: Path | None = None,
64
+ kernel_name: str = DEFAULT_KERNEL,
65
+ display_name: str = DEFAULT_DISPLAY,
66
+ ) -> int:
67
+ root = (repo_root or find_project_root()).resolve()
68
+ py = root / ".venv" / "bin" / "python"
69
+ if not py.is_file():
70
+ print(f"error: expected venv python at {py}", file=sys.stderr)
71
+ print(" run: poetry install", file=sys.stderr)
72
+ return 1
73
+
74
+ print(f"Installing kernelspec {kernel_name!r} into {root / '.venv'} ...")
75
+ dest = install_kernelspec(
76
+ venv_prefix=root / ".venv",
77
+ python_exe=py,
78
+ name=kernel_name,
79
+ display=display_name,
80
+ )
81
+ print(f" -> {dest}")
82
+
83
+ patched = 0
84
+ for nb_path in notebooks:
85
+ nb = Path(nb_path).resolve()
86
+ if nb.suffix != ".ipynb":
87
+ print(f"skip (not .ipynb): {nb}", file=sys.stderr)
88
+ continue
89
+ patch_notebook(nb, name=kernel_name, display=display_name)
90
+ print(f"Patched kernelspec in {nb}")
91
+ patched += 1
92
+
93
+ if patched == 0:
94
+ print("error: no notebooks were patched", file=sys.stderr)
95
+ return 1
96
+
97
+ print("\nIn Cursor: reload window, then Select Kernel → pick", repr(display_name))
98
+ print(
99
+ "If the picker still spins, use Command Palette → Python: Select Interpreter → .venv"
100
+ )
101
+ return 0
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from katabatic.artifacts.local import LocalArtifactStore
6
+ from katabatic.datasets.compatibility import check_dataset_for_model
7
+ from katabatic.datasets.registry import DatasetRegistry
8
+
9
+
10
+ def register_dataset_cli(
11
+ dataset_name: str,
12
+ csv_path: str,
13
+ *,
14
+ target_column: str | None = None,
15
+ artifact_root: str | None = None,
16
+ check_model: str | None = None,
17
+ ) -> None:
18
+ root = Path(artifact_root or "artifacts")
19
+ store = LocalArtifactStore(root)
20
+ reg = DatasetRegistry(store)
21
+ entry = reg.register_if_absent(dataset_name, csv_path, target_column=target_column)
22
+ print(
23
+ f"Registered dataset {dataset_name!r} at {store.root / 'registry' / 'datasets.json'}"
24
+ )
25
+ print(
26
+ f" task={entry['task']!r}, n_rows={entry['n_rows']}, target={entry['target_column']!r}"
27
+ )
28
+ if check_model:
29
+ ok, msg = check_dataset_for_model(entry, check_model)
30
+ print(
31
+ f" compatibility with {check_model!r}: {msg}" + ("" if ok else " (failed)")
32
+ )
33
+ if not ok:
34
+ raise SystemExit(1)