Skip to content

Commit b3175dc

Browse files
committed
📚 tutorials: fetch include/ helpers if not present
Readers who copy-paste cells into their own notebook, rather than cloning the repo, had no include/ folder, so setup_tutorial.py, input.yaml and the shared task/workflow modules were missing and every notebook failed on its first include/ reference. Each module's setup cell now downloads setup_tutorial.py when it is absent; setup_tutorial.py then fetches the remaining include/ files via the GitHub contents API. Both checks are guarded, so a repo clone and the docs build make no network call. Module 0, which has no setup cell, fetches its two example inputs directly. The source ref is pinned to the PR branch for now (TODO in setup_tutorial.py); switch it to the release tag on merge.
1 parent c731942 commit b3175dc

11 files changed

Lines changed: 143 additions & 10 deletions

File tree

‎docs/source/tutorials/include/setup_tutorial.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525
"""
2626

2727
import hashlib
28+
import json
2829
import os
2930
import pathlib
3031
import shutil
3132
import sys
3233
import time
34+
import urllib.request
3335
from contextlib import suppress
3436

3537
from aiida import load_profile
@@ -41,6 +43,29 @@
4143
from aiida.manage.configuration import create_profile, get_config
4244
from aiida.orm import Computer, InstalledCode, load_code, load_computer
4345

46+
47+
def _ensure_tutorial_helpers() -> None:
48+
"""Fetch any missing tutorial helper files into ``include/``.
49+
50+
Lets a notebook running outside the tutorial repository (for example, cells
51+
pasted into your own notebook) import the shared helpers. No-op when they
52+
are already present, as in a repo clone or the docs build.
53+
"""
54+
include_dir = pathlib.Path('include')
55+
if all((include_dir / name).exists() for name in ('workflows.py', 'tasks.py', 'input.yaml')):
56+
return
57+
# TODO: switch to 'aiidateam/aiida-core' at the release tag once PR #7205 merges.
58+
repo, ref = 'GeigerJ2/aiida-core', 'docs/integrate-tutorials'
59+
api = f'https://api.github.com/repos/{repo}/contents/docs/source/tutorials/include?ref={ref}'
60+
include_dir.mkdir(exist_ok=True)
61+
for entry in json.loads(urllib.request.urlopen(api).read()):
62+
if entry['type'] == 'file' and not (include_dir / entry['name']).exists():
63+
urllib.request.urlretrieve(entry['download_url'], include_dir / entry['name'])
64+
65+
66+
_ensure_tutorial_helpers()
67+
68+
4469
# Derive a short suffix from the mtimes of all setup scripts: stable across
4570
# all modules in one build, but bumped whenever any setup logic changes,
4671
# so stale profiles from older builds don't get reused.

‎docs/source/tutorials/module0.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ seed: 42 # RNG seed for the initial perturbation
8585
```{code-cell} ipython3
8686
# In a Jupyter notebook, a line starting with `!` runs a shell command.
8787
# Make a scratch directory to work in and copy the example input into it.
88+
# (Outside the tutorial repo, first fetch the example inputs into include/.)
89+
from pathlib import Path
90+
import urllib.request
91+
92+
_base = 'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include'
93+
Path('include').mkdir(exist_ok=True)
94+
for _name in ('input.yaml', 'input_bad.yaml'):
95+
if not (Path('include') / _name).exists():
96+
urllib.request.urlretrieve(f'{_base}/{_name}', Path('include') / _name)
97+
8898
!mkdir -p /tmp/aiida-tutorial
8999
!cp include/input.yaml /tmp/aiida-tutorial/input.yaml
90100
```

‎docs/source/tutorials/module1.md‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,32 @@ It sets up a lightweight local profile with sensible defaults for all three: SQL
6464
For more advanced high-throughput production setups, see the {ref}`installation guide <installation>`.
6565

6666
This tutorial, though, runs in its own **isolated sandbox profile**, kept separate from any profile you already have, so the data you create here never mixes with your real work and every module reproduces exactly.
67-
Run the cell below to create it (later modules load the same one):
67+
68+
The cell below creates that profile and loads AiiDA's `%verdi` Jupyter magic.
69+
If you are running outside the tutorial repository (for example, cells pasted into your own notebook), it first downloads `setup_tutorial.py`, which in turn fetches the other `include/` helpers it needs; inside the repo, as in these rendered docs, that step is skipped.
70+
Every module runs this same cell, so the data you create now is still available in later modules:
6871

6972
```{code-cell} ipython3
70-
# Create (or load) the tutorial's isolated sandbox profile. It stays separate
71-
# from any AiiDA profile you already have, so nothing here touches your real
72-
# work. Every module loads this same profile, so data you create now is still
73-
# there in later modules.
74-
# `%load_ext aiida` enables the `%verdi` magic used throughout the tutorial.
75-
#
76-
# Prefer to use your own existing profile? Replace the `%run` line with:
77-
# from aiida import load_profile
78-
# load_profile()
73+
# Set up the tutorial's isolated sandbox profile.
74+
from pathlib import Path
75+
76+
if not Path('include/setup_tutorial.py').exists():
77+
import urllib.request
78+
79+
Path('include').mkdir(exist_ok=True)
80+
urllib.request.urlretrieve(
81+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
82+
'include/setup_tutorial.py',
83+
)
84+
7985
%load_ext aiida
8086
%run -i include/setup_tutorial.py
8187
```
8288

89+
:::{tip}
90+
Prefer to use an AiiDA profile you already have? Replace the `%run` line with `from aiida import load_profile` followed by `load_profile()`.
91+
:::
92+
8393
You can verify that the profile is set up correctly with `verdi status`:
8494

8595
```{code-cell} ipython3

‎docs/source/tutorials/module2.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ uv pip install aiida-core aiida-shell
5555

5656
```{code-cell} ipython3
5757
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
58+
from pathlib import Path
59+
60+
if not Path('include/setup_tutorial.py').exists():
61+
import urllib.request
62+
63+
Path('include').mkdir(exist_ok=True)
64+
urllib.request.urlretrieve(
65+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
66+
'include/setup_tutorial.py',
67+
)
68+
5869
%load_ext aiida
5970
%run -i include/setup_tutorial.py
6071
```

‎docs/source/tutorials/module3a.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ uv pip install aiida-core aiida-shell aiida-workgraph
5959

6060
```{code-cell} ipython3
6161
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
62+
from pathlib import Path
63+
64+
if not Path('include/setup_tutorial.py').exists():
65+
import urllib.request
66+
67+
Path('include').mkdir(exist_ok=True)
68+
urllib.request.urlretrieve(
69+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
70+
'include/setup_tutorial.py',
71+
)
72+
6273
%load_ext aiida
6374
%run -i include/setup_tutorial.py
6475
```

‎docs/source/tutorials/module3b.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ uv pip install aiida-core aiida-shell aiida-workgraph
4747

4848
```{code-cell} ipython3
4949
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
50+
from pathlib import Path
51+
52+
if not Path('include/setup_tutorial.py').exists():
53+
import urllib.request
54+
55+
Path('include').mkdir(exist_ok=True)
56+
urllib.request.urlretrieve(
57+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
58+
'include/setup_tutorial.py',
59+
)
60+
5061
%load_ext aiida
5162
%run -i include/setup_tutorial.py
5263
```

‎docs/source/tutorials/module4.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ uv pip install aiida-core aiida-shell
5555

5656
```{code-cell} ipython3
5757
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
58+
from pathlib import Path
59+
60+
if not Path('include/setup_tutorial.py').exists():
61+
import urllib.request
62+
63+
Path('include').mkdir(exist_ok=True)
64+
urllib.request.urlretrieve(
65+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
66+
'include/setup_tutorial.py',
67+
)
68+
5869
%load_ext aiida
5970
%run -i include/setup_tutorial.py
6071
```

‎docs/source/tutorials/module5.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ It reuses the sweep data created in {ref}`Module 2 <tutorial:module2>`.
4848

4949
```{code-cell} ipython3
5050
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
51+
from pathlib import Path
52+
53+
if not Path('include/setup_tutorial.py').exists():
54+
import urllib.request
55+
56+
Path('include').mkdir(exist_ok=True)
57+
urllib.request.urlretrieve(
58+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
59+
'include/setup_tutorial.py',
60+
)
61+
5162
%load_ext aiida
5263
%run -i include/setup_tutorial.py
5364
```

‎docs/source/tutorials/module6a.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ uv pip install aiida-core aiida-shell aiida-workgraph
4343

4444
```{code-cell} ipython3
4545
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
46+
from pathlib import Path
47+
48+
if not Path('include/setup_tutorial.py').exists():
49+
import urllib.request
50+
51+
Path('include').mkdir(exist_ok=True)
52+
urllib.request.urlretrieve(
53+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
54+
'include/setup_tutorial.py',
55+
)
56+
4657
%load_ext aiida
4758
%run -i include/setup_tutorial.py
4859
```

‎docs/source/tutorials/module6b.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ uv pip install aiida-core aiida-shell aiida-workgraph
4141

4242
```{code-cell} ipython3
4343
# Set up the tutorial's isolated sandbox profile (see Module 1 for details).
44+
from pathlib import Path
45+
46+
if not Path('include/setup_tutorial.py').exists():
47+
import urllib.request
48+
49+
Path('include').mkdir(exist_ok=True)
50+
urllib.request.urlretrieve(
51+
'https://raw.githubusercontent.com/GeigerJ2/aiida-core/docs/integrate-tutorials/docs/source/tutorials/include/setup_tutorial.py',
52+
'include/setup_tutorial.py',
53+
)
54+
4455
%load_ext aiida
4556
%run -i include/setup_tutorial.py
4657
```

0 commit comments

Comments
 (0)