jupytermind 0.3.0
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.
- package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
- package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
- package/.github/skills/ai-data-scientist/SKILL.md +330 -0
- package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
- package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
- package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
- package/.github/skills/ai-materials-scientist/manifest.json +58 -0
- package/.github/skills/ai-scientist/SKILL.md +69 -0
- package/.github/skills/ai-scientist/manifest.json +61 -0
- package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
- package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
- package/.github/skills/japanese-prose/NOTICE.md +17 -0
- package/.github/skills/japanese-prose/SKILL.md +111 -0
- package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
- package/.github/skills/japanese-prose/references/scoring.md +24 -0
- package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
- package/.github/skills/japanese-prose/scripts/core.py +192 -0
- package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
- package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
- package/.github/skills/japanese-prose/scripts/lint.py +378 -0
- package/.github/skills/japanese-prose/scripts/outline.py +68 -0
- package/.github/skills/japanese-prose/scripts/terms.py +112 -0
- package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
- package/.github/skills/presentation-planner/SKILL.md +257 -0
- package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
- package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
- package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
- package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
- package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
- package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
- package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
- package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
- package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
- package/.github/skills/tech-writer/SKILL.md +434 -0
- package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
- package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
- package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
- package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
- package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
- package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
- package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
- package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
- package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
- package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
- package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
- package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
- package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
- package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
- package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
- package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
- package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
- package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
- package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
- package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
- package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
- package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
- package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
- package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
- package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
- package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
- package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
- package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
- package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
- package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
- package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
- package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
- package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
- package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
- package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
- package/.github/skills/tech-writer/references/style-constitution.md +104 -0
- package/.github/skills/tech-writer/scripts/lint.py +412 -0
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/ai-data-scientist.js +123 -0
- package/package.json +41 -0
- package/pyproject.toml +45 -0
- package/src/ai_chemistry_scientist/__init__.py +0 -0
- package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
- package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
- package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
- package/src/ai_chemistry_scientist/dispatch.py +369 -0
- package/src/ai_chemistry_scientist/docking_score.py +97 -0
- package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
- package/src/ai_chemistry_scientist/evidence.py +41 -0
- package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
- package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
- package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
- package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
- package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
- package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
- package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
- package/src/ai_chemistry_scientist/validation.py +70 -0
- package/src/ai_data_scientist/__init__.py +0 -0
- package/src/ai_data_scientist/analysis_assumptions.py +121 -0
- package/src/ai_data_scientist/anomaly_detection.py +39 -0
- package/src/ai_data_scientist/automl.py +109 -0
- package/src/ai_data_scientist/cleaning.py +56 -0
- package/src/ai_data_scientist/cli.py +90 -0
- package/src/ai_data_scientist/clustering.py +54 -0
- package/src/ai_data_scientist/dashboard.py +33 -0
- package/src/ai_data_scientist/data_definition.py +100 -0
- package/src/ai_data_scientist/data_quality.py +164 -0
- package/src/ai_data_scientist/dataset_validation.py +135 -0
- package/src/ai_data_scientist/dependency_pins.py +60 -0
- package/src/ai_data_scientist/eda.py +82 -0
- package/src/ai_data_scientist/experiment_evaluation.py +635 -0
- package/src/ai_data_scientist/explainability.py +340 -0
- package/src/ai_data_scientist/feature_engineering.py +163 -0
- package/src/ai_data_scientist/gate_config.py +32 -0
- package/src/ai_data_scientist/ingestion.py +127 -0
- package/src/ai_data_scientist/insight_engine.py +180 -0
- package/src/ai_data_scientist/japanese_nlp.py +43 -0
- package/src/ai_data_scientist/jupyter_launcher.py +137 -0
- package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
- package/src/ai_data_scientist/language_router.py +28 -0
- package/src/ai_data_scientist/lifecycle.py +221 -0
- package/src/ai_data_scientist/mcp_gateway.py +113 -0
- package/src/ai_data_scientist/mcp_runtime.py +194 -0
- package/src/ai_data_scientist/mcp_transport.py +53 -0
- package/src/ai_data_scientist/ml_modeling.py +451 -0
- package/src/ai_data_scientist/model_tuning.py +104 -0
- package/src/ai_data_scientist/notebook_audit.py +574 -0
- package/src/ai_data_scientist/project_manager.py +243 -0
- package/src/ai_data_scientist/report_export.py +73 -0
- package/src/ai_data_scientist/sensitivity.py +445 -0
- package/src/ai_data_scientist/signal_analysis.py +201 -0
- package/src/ai_data_scientist/skill_packaging.py +40 -0
- package/src/ai_data_scientist/stats_analysis.py +88 -0
- package/src/ai_data_scientist/text_nlp.py +44 -0
- package/src/ai_data_scientist/timeseries.py +68 -0
- package/src/ai_data_scientist/visualization.py +708 -0
- package/src/ai_genomics_scientist/__init__.py +1 -0
- package/src/ai_genomics_scientist/differential_expression.py +147 -0
- package/src/ai_genomics_scientist/dispatch.py +267 -0
- package/src/ai_genomics_scientist/evidence.py +45 -0
- package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
- package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
- package/src/ai_genomics_scientist/sequence_features.py +111 -0
- package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
- package/src/ai_genomics_scientist/validation.py +83 -0
- package/src/ai_genomics_scientist/variant_effect.py +147 -0
- package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
- package/src/ai_materials_scientist/__init__.py +0 -0
- package/src/ai_materials_scientist/calphad.py +117 -0
- package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
- package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
- package/src/ai_materials_scientist/dispatch.py +100 -0
- package/src/ai_materials_scientist/evidence.py +84 -0
- package/src/ai_materials_scientist/fem.py +279 -0
- package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
- package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
- package/src/ai_materials_scientist/phase_field.py +167 -0
- package/src/ai_materials_scientist/validation.py +70 -0
- package/src/ai_scientist/__init__.py +1 -0
- package/src/ai_scientist/completion_gate.py +15 -0
- package/src/ai_scientist/data_analysis.py +46 -0
- package/src/ai_scientist/evidence_registry.py +99 -0
- package/src/ai_scientist/experimental_design.py +20 -0
- package/src/ai_scientist/language.py +14 -0
- package/src/ai_scientist/latex_renderer.py +41 -0
- package/src/ai_scientist/literature_review.py +37 -0
- package/src/ai_scientist/manifest.py +87 -0
- package/src/ai_scientist/manuscript.py +94 -0
- package/src/ai_scientist/mcp_config.py +76 -0
- package/src/ai_scientist/mcp_external.py +42 -0
- package/src/ai_scientist/mcp_failures.py +23 -0
- package/src/ai_scientist/mcp_gateway.py +38 -0
- package/src/ai_scientist/mcp_managed.py +180 -0
- package/src/ai_scientist/npm_packaging.py +49 -0
- package/src/ai_scientist/orchestrator.py +133 -0
- package/src/ai_scientist/peer_review.py +60 -0
- package/src/ai_scientist/phase_gate.py +74 -0
- package/src/ai_scientist/phase_state.py +230 -0
- package/src/ai_scientist/presentation.py +56 -0
- package/src/ai_scientist/project_config.py +31 -0
- package/src/ai_scientist/project_handle.py +74 -0
- package/src/ai_scientist/reproducibility.py +20 -0
- package/src/ai_scientist/research_planning.py +20 -0
- package/src/ai_scientist/skill_invocation.py +21 -0
- package/src/ai_scientist/tdd_gate.py +99 -0
- package/src/ai_structural_biology_scientist/__init__.py +0 -0
- package/src/ai_structural_biology_scientist/contact_map.py +87 -0
- package/src/ai_structural_biology_scientist/dispatch.py +269 -0
- package/src/ai_structural_biology_scientist/evidence.py +43 -0
- package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
- package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
- package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
- package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
- package/src/ai_structural_biology_scientist/validation.py +100 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Project resolution and notebook lifecycle management.
|
|
2
|
+
|
|
3
|
+
Implements DES-AIDS-003: project identifier validation (ADR-0005),
|
|
4
|
+
notebook creation/reuse, and a single-writer queue that serializes
|
|
5
|
+
concurrent notebook writes (ADR-0004).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import tempfile
|
|
13
|
+
import threading
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import nbformat
|
|
18
|
+
|
|
19
|
+
_SLUG_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
|
20
|
+
|
|
21
|
+
# Captured once, at import time, before any skill code can os.chdir() into a
|
|
22
|
+
# notebook/dataset directory. This anchors resolve_project's default
|
|
23
|
+
# projects_root to a stable location (REQ-AIDS-044 / DES-AIDS-032), instead
|
|
24
|
+
# of re-resolving "projects" relative to whatever the cwd happens to be at
|
|
25
|
+
# call time (which previously created nested
|
|
26
|
+
# projects/<slug>/notebooks/projects/<slug> paths when the kernel cwd drifted
|
|
27
|
+
# into a project's own notebooks directory).
|
|
28
|
+
_IMPORT_TIME_CWD = Path.cwd()
|
|
29
|
+
|
|
30
|
+
_PROJECTS_ROOT_ENV_VAR = "AI_DATA_SCIENTIST_PROJECTS_ROOT"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _default_projects_root() -> Path:
|
|
34
|
+
"""Resolve the stable default projects root.
|
|
35
|
+
|
|
36
|
+
Prefers the ``AI_DATA_SCIENTIST_PROJECTS_ROOT`` environment variable when
|
|
37
|
+
set (for callers that want to pin an explicit workspace root); otherwise
|
|
38
|
+
falls back to ``<import-time cwd>/projects``, which stays constant for
|
|
39
|
+
the lifetime of the process regardless of later ``os.chdir`` calls.
|
|
40
|
+
"""
|
|
41
|
+
env_root = os.environ.get(_PROJECTS_ROOT_ENV_VAR)
|
|
42
|
+
if env_root:
|
|
43
|
+
return Path(env_root).resolve()
|
|
44
|
+
return (_IMPORT_TIME_CWD / "projects").resolve()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def next_execution_count(notebook) -> int:
|
|
48
|
+
"""Compute the next monotonically increasing execution_count for ``notebook``.
|
|
49
|
+
|
|
50
|
+
Shared by any writer that appends an "executed" code cell (mcp_gateway's
|
|
51
|
+
run_and_record, visualization's record_chart) so every such cell carries
|
|
52
|
+
a real, non-null, incrementing execution_count instead of leaving it
|
|
53
|
+
unset (REQ-AIDS-009's evidentiary-cell acceptance criterion).
|
|
54
|
+
"""
|
|
55
|
+
existing = [
|
|
56
|
+
cell.get("execution_count") for cell in notebook.cells if cell.get("cell_type") == "code"
|
|
57
|
+
]
|
|
58
|
+
return max((count for count in existing if isinstance(count, int)), default=0) + 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InvalidProjectNameError(ValueError):
|
|
62
|
+
"""Raised when a project name does not satisfy the ADR-0005 slug policy."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StablePathResolutionError(FileNotFoundError):
|
|
66
|
+
"""Raised when a path resolves under neither cwd nor the stable workspace root."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ProjectHandle:
|
|
71
|
+
"""Resolved, validated project identity and its notebook path."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
root: Path
|
|
75
|
+
notebook_path: Path
|
|
76
|
+
data_dir: Path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# A process-wide lock per notebook path, guaranteeing a single writer at a
|
|
80
|
+
# time regardless of how many callers invoke enqueue_write concurrently.
|
|
81
|
+
_write_locks: dict[Path, threading.Lock] = {}
|
|
82
|
+
_write_locks_guard = threading.Lock()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _lock_for(path: Path) -> threading.Lock:
|
|
86
|
+
with _write_locks_guard:
|
|
87
|
+
lock = _write_locks.get(path)
|
|
88
|
+
if lock is None:
|
|
89
|
+
lock = threading.Lock()
|
|
90
|
+
_write_locks[path] = lock
|
|
91
|
+
return lock
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# @id CODE-AIDS-028
|
|
95
|
+
# @implements REQ-AIDS-028
|
|
96
|
+
# @design DES-AIDS-003
|
|
97
|
+
# @id CODE-AIDS-052
|
|
98
|
+
# @implements REQ-AIDS-044
|
|
99
|
+
# @design DES-AIDS-032
|
|
100
|
+
def resolve_project(name: str, projects_root: Path | str | None = None) -> ProjectHandle:
|
|
101
|
+
"""Validate ``name`` and resolve its on-disk project handle.
|
|
102
|
+
|
|
103
|
+
Rejects path traversal / non-slug names before any filesystem access,
|
|
104
|
+
per ADR-0005.
|
|
105
|
+
|
|
106
|
+
When ``projects_root`` is omitted, the default root is stable across
|
|
107
|
+
process working-directory changes: it honors the
|
|
108
|
+
``AI_DATA_SCIENTIST_PROJECTS_ROOT`` environment variable when set, and
|
|
109
|
+
otherwise anchors to the directory this module was imported from, not
|
|
110
|
+
the caller's current working directory at call time (REQ-AIDS-044).
|
|
111
|
+
Passing an explicit ``projects_root`` is unchanged from before.
|
|
112
|
+
"""
|
|
113
|
+
if not _SLUG_PATTERN.match(name):
|
|
114
|
+
raise InvalidProjectNameError(
|
|
115
|
+
f"'{name}' is not a valid project name. Allowed pattern: "
|
|
116
|
+
f"lowercase ASCII letters, digits and single hyphens, e.g. 'sales-2024' "
|
|
117
|
+
f"(プロジェクト名は小文字英数字とハイフンのみ使用できます: 例 'sales-2024')."
|
|
118
|
+
)
|
|
119
|
+
root = Path(projects_root).resolve() if projects_root is not None else _default_projects_root()
|
|
120
|
+
project_dir = (root / name).resolve()
|
|
121
|
+
if project_dir.parent != root:
|
|
122
|
+
# Defense in depth: even a slug-valid name must stay inside projects_root.
|
|
123
|
+
raise InvalidProjectNameError(f"'{name}' resolves outside the projects directory.")
|
|
124
|
+
notebook_path = project_dir / "notebooks" / f"{name}.ipynb"
|
|
125
|
+
data_dir = project_dir / "data"
|
|
126
|
+
return ProjectHandle(
|
|
127
|
+
name=name, root=project_dir, notebook_path=notebook_path, data_dir=data_dir
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# @id CODE-AIDS-055
|
|
132
|
+
# @implements REQ-AIDS-049
|
|
133
|
+
# @design DES-AIDS-037
|
|
134
|
+
def ensure_data_dir(handle: ProjectHandle) -> Path:
|
|
135
|
+
"""Create ``handle.data_dir`` (and any missing parents) if absent; return it."""
|
|
136
|
+
handle.data_dir.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
return handle.data_dir
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# @id CODE-AIDS-056
|
|
141
|
+
# @implements REQ-AIDS-047
|
|
142
|
+
# @design DES-AIDS-035
|
|
143
|
+
def resolve_stable_path(path: Path | str) -> Path:
|
|
144
|
+
"""Resolve ``path`` against cwd, falling back to the stable workspace root.
|
|
145
|
+
|
|
146
|
+
Mirrors ``_default_projects_root``'s stability rule for any caller (such
|
|
147
|
+
as ``notebook_audit.audit_notebook``) that receives a relative path which
|
|
148
|
+
may have been computed against the workspace root but is later evaluated
|
|
149
|
+
from a kernel whose working directory has drifted into a project's own
|
|
150
|
+
notebook directory (REQ-AIDS-047).
|
|
151
|
+
|
|
152
|
+
An absolute ``path`` is returned resolved as-is (existence is left to the
|
|
153
|
+
caller, matching prior behavior). A relative ``path`` is first checked
|
|
154
|
+
against the current working directory; if that candidate does not exist,
|
|
155
|
+
it is re-checked against ``_IMPORT_TIME_CWD`` (the same stable base
|
|
156
|
+
``_default_projects_root`` anchors to). If neither candidate exists,
|
|
157
|
+
``StablePathResolutionError`` is raised naming both attempted locations.
|
|
158
|
+
"""
|
|
159
|
+
candidate = Path(path)
|
|
160
|
+
if candidate.is_absolute():
|
|
161
|
+
return candidate.resolve()
|
|
162
|
+
|
|
163
|
+
cwd_candidate = (Path.cwd() / candidate).resolve()
|
|
164
|
+
if cwd_candidate.exists():
|
|
165
|
+
return cwd_candidate
|
|
166
|
+
|
|
167
|
+
stable_candidate = (_IMPORT_TIME_CWD / candidate).resolve()
|
|
168
|
+
if stable_candidate.exists():
|
|
169
|
+
return stable_candidate
|
|
170
|
+
|
|
171
|
+
raise StablePathResolutionError(
|
|
172
|
+
f"Could not resolve '{path}' relative to the current working directory "
|
|
173
|
+
f"({cwd_candidate}) or the stable workspace root ({stable_candidate})."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# @id CODE-AIDS-002
|
|
178
|
+
# @implements REQ-AIDS-002
|
|
179
|
+
# @design DES-AIDS-003
|
|
180
|
+
def ensure_notebook(handle: ProjectHandle) -> Path:
|
|
181
|
+
"""Create the project notebook if missing; otherwise reuse it."""
|
|
182
|
+
handle.notebook_path.parent.mkdir(parents=True, exist_ok=True)
|
|
183
|
+
if not handle.notebook_path.exists():
|
|
184
|
+
notebook = nbformat.v4.new_notebook()
|
|
185
|
+
with handle.notebook_path.open("w", encoding="utf-8") as fh:
|
|
186
|
+
nbformat.write(notebook, fh)
|
|
187
|
+
return handle.notebook_path
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# @id CODE-AIDS-011
|
|
191
|
+
# @implements REQ-AIDS-011
|
|
192
|
+
# @design DES-AIDS-003
|
|
193
|
+
# @id CODE-AIDS-029
|
|
194
|
+
# @implements REQ-AIDS-029 REQ-AIDS-059
|
|
195
|
+
# @design DES-AIDS-003
|
|
196
|
+
def enqueue_write(handle: ProjectHandle, cell_mutation) -> Path:
|
|
197
|
+
"""Serialize a notebook mutation through a per-notebook single-writer lock.
|
|
198
|
+
|
|
199
|
+
``cell_mutation`` is a callable receiving the loaded ``NotebookNode`` and
|
|
200
|
+
mutating it in place (e.g. appending a cell). The notebook is re-read and
|
|
201
|
+
re-written under the lock so every writer observes the latest on-disk
|
|
202
|
+
state, guaranteeing no cell from a concurrent writer is lost, and the
|
|
203
|
+
result always round-trips through ``nbformat.validate``.
|
|
204
|
+
|
|
205
|
+
The write itself is atomic (GitHub #27): the mutated notebook is
|
|
206
|
+
serialized to a string with ``nbformat.writes`` *before* anything on
|
|
207
|
+
disk is touched, so a mutation that ``nbformat.validate`` accepts but
|
|
208
|
+
that fails at JSON-serialization time (e.g. non-JSON-serializable
|
|
209
|
+
metadata) raises without ever truncating or corrupting the existing
|
|
210
|
+
file. The serialized string is then written to a temporary file in the
|
|
211
|
+
same directory, flushed and fsynced, and atomically swapped into place
|
|
212
|
+
with ``os.replace`` so a crash or error mid-write never leaves a
|
|
213
|
+
partially written notebook on disk.
|
|
214
|
+
|
|
215
|
+
**Concurrent-write risk with Jupyter MCP (GitHub #34, REQ-AIDS-059)**:
|
|
216
|
+
this lock only serializes concurrent callers of this function within
|
|
217
|
+
the current process; it does not coordinate with a separate Jupyter
|
|
218
|
+
MCP session that has the same notebook file open in memory. If such an
|
|
219
|
+
MCP session later saves its own in-memory copy, it can silently
|
|
220
|
+
overwrite whatever this function already wrote to disk. Prefer routing
|
|
221
|
+
writes through the active MCP session when one is open against this
|
|
222
|
+
notebook, or pause MCP-side saves while calling this function directly.
|
|
223
|
+
"""
|
|
224
|
+
lock = _lock_for(handle.notebook_path)
|
|
225
|
+
with lock:
|
|
226
|
+
notebook = nbformat.read(handle.notebook_path, as_version=4)
|
|
227
|
+
cell_mutation(notebook)
|
|
228
|
+
nbformat.validate(notebook)
|
|
229
|
+
serialized = nbformat.writes(notebook)
|
|
230
|
+
directory = handle.notebook_path.parent
|
|
231
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
232
|
+
dir=directory, prefix=f".{handle.notebook_path.name}.", suffix=".tmp"
|
|
233
|
+
)
|
|
234
|
+
try:
|
|
235
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
236
|
+
fh.write(serialized)
|
|
237
|
+
fh.flush()
|
|
238
|
+
os.fsync(fh.fileno())
|
|
239
|
+
os.replace(tmp_name, handle.notebook_path)
|
|
240
|
+
except BaseException:
|
|
241
|
+
Path(tmp_name).unlink(missing_ok=True)
|
|
242
|
+
raise
|
|
243
|
+
return handle.notebook_path
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Report export module.
|
|
2
|
+
|
|
3
|
+
Implements DES-AIDS-024 (REQ-AIDS-025, REQ-AIDS-033, ADR-0007): exports the
|
|
4
|
+
current project notebook to PDF, HTML, or slide form using a local
|
|
5
|
+
nbconvert-based conversion tool, without invoking any Jupyter MCP
|
|
6
|
+
execution call.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import nbformat
|
|
15
|
+
from nbconvert import HTMLExporter, PDFExporter, SlidesExporter
|
|
16
|
+
|
|
17
|
+
from ai_data_scientist.project_manager import ProjectHandle
|
|
18
|
+
|
|
19
|
+
_SUPPORTED_FORMATS = ("html", "pdf", "slides")
|
|
20
|
+
_EXPORTERS = {"html": HTMLExporter, "pdf": PDFExporter, "slides": SlidesExporter}
|
|
21
|
+
_EXTENSIONS = {"html": "html", "pdf": "pdf", "slides": "slides.html"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ReportPath:
|
|
26
|
+
path: Path
|
|
27
|
+
format: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# @id CODE-AIDS-025
|
|
31
|
+
# @implements REQ-AIDS-025
|
|
32
|
+
# @design DES-AIDS-024
|
|
33
|
+
# @id CODE-AIDS-033
|
|
34
|
+
# @implements REQ-AIDS-033
|
|
35
|
+
# @design DES-AIDS-024
|
|
36
|
+
def export_report(
|
|
37
|
+
handle: ProjectHandle, report_format: str = "html", name: str | None = None
|
|
38
|
+
) -> ReportPath:
|
|
39
|
+
"""Export ``handle``'s notebook to ``report_format`` via nbconvert only.
|
|
40
|
+
|
|
41
|
+
Reads the already-executed notebook and renders it with a local
|
|
42
|
+
nbconvert exporter only; no Jupyter MCP client is invoked, satisfying
|
|
43
|
+
REQ-AIDS-033's read-only, non-MCP export boundary.
|
|
44
|
+
"""
|
|
45
|
+
if report_format not in _SUPPORTED_FORMATS:
|
|
46
|
+
raise ValueError(f"Unsupported report format: {report_format!r}")
|
|
47
|
+
|
|
48
|
+
notebook = nbformat.read(handle.notebook_path, as_version=4)
|
|
49
|
+
exporter = _EXPORTERS[report_format]()
|
|
50
|
+
try:
|
|
51
|
+
body, _resources = exporter.from_notebook_node(notebook)
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
if report_format == "pdf" and "xelatex" in str(exc).lower():
|
|
54
|
+
raise RuntimeError(
|
|
55
|
+
"PDF export requires a system TeX/xelatex installation, which "
|
|
56
|
+
"is not bundled with this package. See the 'PDF export "
|
|
57
|
+
"prerequisites' section of README.md for install instructions, "
|
|
58
|
+
"or use report_format='html' instead."
|
|
59
|
+
) from exc
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
reports_dir = handle.root / "reports"
|
|
63
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
report_name = name or handle.name
|
|
65
|
+
extension = _EXTENSIONS[report_format]
|
|
66
|
+
report_path = reports_dir / f"{report_name}.{extension}"
|
|
67
|
+
|
|
68
|
+
mode = "wb" if isinstance(body, bytes) else "w"
|
|
69
|
+
encoding = None if isinstance(body, bytes) else "utf-8"
|
|
70
|
+
with report_path.open(mode, encoding=encoding) as fh:
|
|
71
|
+
fh.write(body)
|
|
72
|
+
|
|
73
|
+
return ReportPath(path=report_path, format=report_format)
|