#!/usr/bin/env python # coding: utf-8 # # CLI # In[ ]: import os, pathlib, tempfile, shutil, atexit, hashlib, pprint from IPython.display import * from IPython import get_ipython # needed for `jupyter_execute` because magics? RTD = os.environ.get("READTHEDOCS") # The `jupyter lite` (or `jupyter-lite`) CLI provides tools for lifecycle of combining... # - **user-authored content** like _Notebooks_ and _Lab Extensions_. # - the core JupyterLite **static assets** # # ... into a **ready-to-[deploy](./deploying.md)** (and optionally **reproducible**) Jupyter sites which require no server. # ## Installation # In[ ]: get_ipython().system('pip install jupyterlite') get_ipython().system('jupyter lite --version') # ### Extras # Some extra features of different _addons_ have additional dependencies. # # ```bash # pip install jupyterlite[contents] # jupyter_server for better contents API # pip install jupyterlite[serve] # tornado for better local preview # pip install jupyterlite[lab] # a known-compatible jupyterlab (entails `contents` and `serve`) # # TODO: [archive] # use libarchive # ``` # ## The Lite Dir # # When you run `jupyter lite` commands, it assumes your current working directory is the partial contents of a JupyterLite site. You can override this with `--lite-dir`. By default, the built site will be created in `_output`, but can be overridden with `--output-dir`. # In[ ]: if "TMP_DIR" not in globals(): TMP_DIR = pathlib.Path(tempfile.mkdtemp(prefix="_my_lite_dir_")) def clean(): shutil.rmtree(TMP_DIR) atexit.register(clean) os.chdir(TMP_DIR) print(pathlib.Path.cwd()) # ### Well-known Files # # Some files in your `--lite-dir` that have special meaning: # | paths | file | if found | # |-----------------------------|---------------------|------------------------------------------------| # | `.`
`./lab`
`./retro` | `jupyter-lite.{json,ipynb}` | merge contents with static in `_output/{path}/jupyter-lite.{json,ipynb}` | # | `.`
`./lab`
`./retro` | `overrides.json` | merge with static `_output/{*}/jupyter-lite.json` || # | `./files/` | `*` | copy verbatim to `_output/files/*` and index in `/api/contents` | # ## Usage # ### Common Parameters # # | parameter | description | default | environment variable | # | ------------------------ | ----------------------------------------------------------------------------------- | ----------------------------- | --------------------------- | # | `--lite-dir` | configuration and content for the site | current working directory | `JUPYTERLITE_DIR` | # | `--output-dir` | where the hostable site will be created | `_output` | `JUPYTERLITE_OUTPUT_DIR` | # | `--cache-dir` | a cache directory for downloads | `/.cache` | `JUPYTERLITE_CACHE_DIR` | # | `--app-archive` | an alternate site to base off of | bundled | | # | `--files` | directory to copy to `_output/files/` and available as _Contents_ | `./files` | | # | `--ignore-files` | patterns that should _never_ be included in `/files/` (even if found in `lite-dir`) | various | | # | `--output-archive` | the path to the archive | `-jupyterlite.tgz` | `JUPYTERLAB_OUTPUT_ARCHIVE` | # | `--port` | port on `127.0.0.1` to serve the test server | `8000` | `JUPYTERLITE_PORT` | # | `--base-url` | the URL prefix to include before the site | `/` | `JUPYTERLITE_BASE_URL` | # | `--source-date-epoch` | optionally enable additional reproducible build measures (best-effort!) | | `SOURCE_DATE_EPOCH` | # | `--federated-extensions` | paths to folders, `pip`/`conda` packages with extensions [see note](#conda-packages)| | | # | `--ignore-sys-prefix` | don't copy any contents, such as install labextensions, from `sys.prefix` | `False` | | # All parameters may be configured via a `jupyter_lite_config.json` in the directory where `jupyter lite` is launched, or given via `--config`. # # ```{hint} # For an advanced example, see the [configuration](https://github.com/jupyterlite/jupyterlite/tree/main/examples) used for this documentation. # ``` # ### Help # # The CLI provides its own documentation, under `--help` (or `-h`). # In[ ]: get_ipython().system('jupyter lite --help') # ### Status # # Always safe to run, this command provides an overview of what JupyterLite has been doing. # In[ ]: get_ipython().system('jupyter lite status') # ### List # # Always safe to run, this command provides an overview of what JupyterLite _might_ do. # # > _TODO: improve on default output_ # In[ ]: get_ipython().system('jupyter lite list') # ### Init # # Copy all the static data to the `--output-dir`. # In[ ]: get_ipython().system('jupyter lite init') # ### Build # # Copy all the **user-authored content** to the `--output-dir`, and applies appropriate changes to e.g. generated Contents API responses. # # Special well-known files will be _merged_ appropriately, but generally, files that exist in the user directory will overwrite any existing content. # In[ ]: get_ipython().system('jupyter lite build') # ### Serve # # Serve the `--output-dir` on `http://127.0.0.1:{--port=8000}{--base-url=/}`. # # ```{warning} # This is _not_ a production server. Please consider _any_ of the [deployment](./deploying.md) options # before trying to make this something it isn't. # ``` # In[ ]: get_ipython().system('jupyter lite serve --help') # ### Check # # Use all available mechanisms to verify that the build folder conforms to schema, etc. # In[ ]: get_ipython().system('jupyter lite check') # ### Archive # # Turn the _output directory_ into a `.tgz` file. This is usually easier to move around than (sometimes) hundreds of files, and can be used as the baseline for future sites. # # > This command is _relatively_ expensive, and is skipped for documentation purposes # In[ ]: get_ipython().system('jupyter lite archive --help') if not RTD: get_ipython().system('jupyter lite archive') # But let's talk about a more _reproducible_ asset. # In[ ]: # we clean out the TMP_DIR for the reproducibility examples shutil.rmtree(TMP_DIR / "_output", ignore_errors=True) # # # #### Reproducible Archives # # > _🛠️ This feature is a **work-in-progress**, and should not be relied upon by any production workflows **Just Yet**._ # If `--source-date-epoch` is given, a number of measures will be taken to *try to ensure* that the output of `jupyter lite archive`, an npm-compatible `tgz` package, always returns a bit-for-bit [reproducible build](https://reproducible-builds.org). # # The most obvious change is that the modified time of each file "clamped" to that time. Some other changes: # - file ownership will be reset # - predictable sorting will be used # - additional checks will be applied # # ```{note} # This is a shortcut for setting the environment variable `SOURCE_DATE_EPOCH`: # # | platform | command | # |------------------|-------------------------------------------------------| # | Linux
MacOS | `export SOURCE_DATE_EPOCH=` | # | Windows | `set SOURCE_DATE_EPOCH=` | # | Python | `os.environ.update(SOURCE_DATE_EPOCH, )` | # ``` # In[ ]: if not "source_date_epoch" in globals(): from datetime import datetime source_date_epoch = int(datetime.utcnow().timestamp()) print("SOURCE_DATE_EPOCH is", source_date_epoch) # In[ ]: if not RTD: get_ipython().system('jupyter lite archive --source-date-epoch {source_date_epoch} --output-archive ./a.tgz') # If we clear out our `_output`... # In[ ]: if not RTD: shutil.rmtree(TMP_DIR / "_output", ignore_errors=True) pprint.pprint([*TMP_DIR.rglob("*")]) # ...and rebuild, we should always get the same file. # In[ ]: if not RTD: get_ipython().system('jupyter lite archive --source-date-epoch {source_date_epoch} --output-archive ./b.tgz') # In[ ]: if not RTD: a, b = [ hashlib.sha256((TMP_DIR / f"{x}.tgz").read_bytes()).hexdigest() for x in "ab" ] print("We built app archives with the SHA256SUMS of:\n", a, "\n", b) try: assert a == b, "We did not reproducibly build today.\n- {}\n- {}\n\n".format(a, b) except AssertionError as err: if shutil.which("diffoscope"): print("We did NOT reproducibly build today, checking in with `diffoscope`...") get_ipython().system('diffoscope a.tgz b.tgz') print("...but at least we tried REALLY hard!\n") # ## Miscellaneous # ### conda packages # # While `--federated-extensions` support the `.tar.bz2` created by most `conda` packages, there are some issues: # # - `anaconda.org` uses non-standard HTTP headers to S3 buckets to provide packages # - the `conda-forge` channel provides all of its builds as GitHub releases, and can be predictably transformed, e.g. # # ``` # https://anaconda.org/conda-forge/jupyterlab_widgets/1.0.0/download/noarch/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2 # | | # | +---------------------------------------------+ # v v v # https://github.com/conda-forge/releases/releases/download/noarch/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2 # ```