Skip to content

Commit

Permalink
Add tutorial-on-demand
Browse files Browse the repository at this point in the history
  • Loading branch information
athornton committed Sep 9, 2024
1 parent 9e3b491 commit 1c4647d
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 21 deletions.
12 changes: 4 additions & 8 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
platformdirs==4.2.2
platformdirs==4.3.2
# via jupyter-core
pluggy==1.5.0
# via pytest
Expand All @@ -206,13 +206,13 @@ pybtex==0.24.0
# sphinxcontrib-bibtex
pybtex-docutils==1.0.3
# via sphinxcontrib-bibtex
pydantic==2.9.0
pydantic==2.9.1
# via
# -c requirements/main.txt
# autodoc-pydantic
# documenteer
# pydantic-settings
pydantic-core==2.23.2
pydantic-core==2.23.3
# via
# -c requirements/main.txt
# pydantic
Expand Down Expand Up @@ -316,7 +316,7 @@ sphinx==8.0.2
# sphinxcontrib-youtube
# sphinxext-opengraph
# sphinxext-rediraffe
sphinx-autodoc-typehints==2.3.0
sphinx-autodoc-typehints==2.4.0
# via documenteer
sphinx-automodapi==0.17.0
# via documenteer
Expand Down Expand Up @@ -388,10 +388,6 @@ typing-extensions==4.12.2
# pydantic
# pydantic-core
# sqlalchemy
tzdata==2024.1
# via
# -c requirements/main.txt
# pydantic
uc-micro-py==1.0.3
# via linkify-it-py
urllib3==2.2.2
Expand Down
10 changes: 4 additions & 6 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ idna==3.8
# httpx
pycparser==2.22
# via cffi
pydantic==2.9.0
pydantic==2.9.1
# via
# -r requirements/main.in
# fastapi
# pydantic-settings
# safir
pydantic-core==2.23.2
pydantic-core==2.23.3
# via
# pydantic
# safir
Expand All @@ -67,7 +67,7 @@ pyyaml==6.0.2
# via
# -r requirements/main.in
# uvicorn
rubin-nublado-client @ git+https://github.com/lsst-sqre/nublado@0ab1f77f60489d1df22bfb04ec64ee703ba594f4#subdirectory=client
rubin-nublado-client @ git+https://github.com/lsst-sqre/nublado@76615a8053112360417b68723871e59b6ebe8463#subdirectory=client
# via -r requirements/main.in
safir==6.3.0
# via -r requirements/main.in
Expand All @@ -77,7 +77,7 @@ sniffio==1.3.1
# via
# anyio
# httpx
starlette==0.38.4
starlette==0.38.5
# via
# -r requirements/main.in
# fastapi
Expand All @@ -91,8 +91,6 @@ typing-extensions==4.12.2
# fastapi
# pydantic
# pydantic-core
tzdata==2024.1
# via pydantic
uritemplate==4.1.1
# via gidgethub
uvicorn==0.30.6
Expand Down
12 changes: 6 additions & 6 deletions requirements/tox.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ colorama==0.4.6
# via tox
distlib==0.3.8
# via virtualenv
filelock==3.15.4
filelock==3.16.0
# via
# tox
# virtualenv
Expand All @@ -18,7 +18,7 @@ packaging==24.1
# pyproject-api
# tox
# tox-uv
platformdirs==4.2.2
platformdirs==4.3.2
# via
# -c requirements/dev.txt
# tox
Expand All @@ -29,13 +29,13 @@ pluggy==1.5.0
# tox
pyproject-api==1.7.1
# via tox
tox==4.18.0
tox==4.18.1
# via
# -r requirements/tox.in
# tox-uv
tox-uv==1.11.2
tox-uv==1.11.3
# via -r requirements/tox.in
uv==0.4.6
uv==0.4.7
# via tox-uv
virtualenv==20.26.3
virtualenv==20.26.4
# via tox
8 changes: 7 additions & 1 deletion src/ghostwriter/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from .ensure_lab import ensure_running_lab
from .portal_query import portal_query
from .tutorial import tutorial_on_demand
from .vacuous import vacuous_hook

__all__ = ["ensure_running_lab", "portal_query", "vacuous_hook"]
__all__ = [
"ensure_running_lab",
"portal_query",
"tutorial_on_demand",
"vacuous_hook",
]
101 changes: 101 additions & 0 deletions src/ghostwriter/hooks/tutorial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Takes a portal query ID from the request path and calls the Lab endpoint
to template and write that query. We assume there's already a running lab,
which we can he sure of by putting ensure_running_lab in before this in the
hooks.
"""

import inspect
from string import Template
from urllib.parse import urljoin

import structlog
from httpx import AsyncClient

from ..models.substitution import Parameters

LOGGER = structlog.get_logger("ghostwriter")


async def tutorial_on_demand(params: Parameters) -> None:
"""Check out a particular tutorial notebook."""
client = params.client
LOGGER.debug("Logging in to hub", user=params.user)
await client.auth_to_hub()
LOGGER.debug("Authenticating to lab")
await client.auth_to_lab()
async with client.open_lab_session() as lab_session:
code = _get_code_from_template(params.path)
# once nublado-seeds is merged, we could have
# something like _get_code_from_nublado_seeds(params.client.http)
LOGGER.debug("Code for execution in Lab context", code=code)
await lab_session.run_python(code)
LOGGER.debug("Continuing to redirect")


def _get_user_endpoint(base_url: str, user: str) -> str:
return str(urljoin(base_url, f"/nb/user/{user}"))


def _get_code_from_template(client_path: str) -> str:
client_path = "/".join(client_path.strip("/").split("/")[1:])
code_template = _get_nbcheck_template()
return Template(code_template).substitute(path=client_path)


async def _get_code_from_nublado_seeds(http_client: AsyncClient) -> str:
"""Get the templated notebook from nublado-seeds, and then extract and
return the code contents.
"""
# These are constant for this sort of query
org = "lsst-sqre"
repo = "nublado-seeds"
directory = "tutorials-on-demand"
notebook = "fetch"

nb_url = f"github/{org}/{repo}/{directory}/{notebook}"

resp = await http_client.get(nb_url)
obj = resp.json()
return "\n".join(
x["source"] for x in obj["cells"] if x["cell_type"] == "code"
)


def _get_nbcheck_template() -> str:
# This should probably go into nublado-seeds. At least it's an isolated
# function, easy to swap out.
return inspect.cleandoc(
"""import os
import requests
from base64 import b64decode
from datetime import datetime, timezone
from pathlib import Path
topdir = Path(os.environ['HOME']) / "notebooks" / "tutorials-on-demand"
nb = topdir / "${path}.ipynb"
nb.parent.mkdir(exist_ok=True)
if nb.exists():
# Move extant notebook aside
now = datetime.datetime.now(timezone.utc).isoformat()
nbdir = nb.parent
newname = Path("${path}").name + "-" + now + ".ipynb"
nb.rename(nbdir / newname)
# Retrieve content.
# Owner, repo, and branch are constant in this context.
owner = "rubin-dp0"
repo = "tutorial-notebooks"
branch = "prod"
url = f"https://api.github.com/repos/{owner}/{repo}/contents/${path}"
url += f".ipynb?ref={branch}"
r=requests.get(url)
obj=r.json()
content_b64 = obj["content"]
# Turn that back into a UTF-8 string
content = b64decode(content_b64).decode()
# And write it into place
nb.write_text(content)
"""
)

0 comments on commit 1c4647d

Please sign in to comment.